Porównaj commity

...

18 Commity

Autor SHA1 Wiadomość Data
Petitminion ba5b657b61 lint
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Petitminion 4fc73c1430 lint
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Ciarán Ainsworth 97e24bcaa6 Apply 12 suggestion(s) to 4 file(s)
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Ciarán Ainsworth 1b15fea1ab Apply 1 suggestion(s) to 1 file(s)
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Ciarán Ainsworth b624fea2fa Apply 1 suggestion(s) to 1 file(s)
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Ciarán Ainsworth e028e8788b Apply 1 suggestion(s) to 1 file(s)
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Ciarán Ainsworth 67f74d40a6 Add ListenBrainz sync documentation
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Petitminion 547bd6f371 lint
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Petitminion 05ec6f6d0f tests
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Petitminion a03cc1db24 lint
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Petitminion 2a364d5785 add favorite sync
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Petitminion 5bc0171694 delete test
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Petitminion 37acfa475d loads of things
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Petitminion f45fd1e465 various reviews
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Petitminion 17c4a92f77 lint
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Petitminion 6414302899 implement listening and favorite sync with listenbrainz
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2658>
2024-04-16 11:01:29 +00:00
Ciarán Ainsworth 94a5b9e696
chore(deps): bump py3-pillow in Dockerfile 2024-04-14 15:32:26 +02:00
Bruno-Van-den-Bosch d673e77dff Translated using Weblate (Dutch)
Currently translated at 99.8% (2177 of 2181 strings)

Translation: Funkwhale/Funkwhale Web
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/front/nl/
2024-04-12 13:50:31 +00:00
19 zmienionych plików z 774 dodań i 34 usunięć

Wyświetl plik

@ -39,7 +39,7 @@ RUN set -eux; \
zlib-dev \ zlib-dev \
py3-cryptography=41.0.7-r0 \ py3-cryptography=41.0.7-r0 \
py3-lxml=4.9.3-r1 \ py3-lxml=4.9.3-r1 \
py3-pillow=10.2.0-r0 \ py3-pillow=10.3.0-r0 \
py3-psycopg2=2.9.9-r0 \ py3-psycopg2=2.9.9-r0 \
py3-watchfiles=0.19.0-r1 \ py3-watchfiles=0.19.0-r1 \
python3-dev python3-dev
@ -99,7 +99,7 @@ RUN set -eux; \
libxslt \ libxslt \
py3-cryptography=41.0.7-r0 \ py3-cryptography=41.0.7-r0 \
py3-lxml=4.9.3-r1 \ py3-lxml=4.9.3-r1 \
py3-pillow=10.2.0-r0 \ py3-pillow=10.3.0-r0 \
py3-psycopg2=2.9.9-r0 \ py3-psycopg2=2.9.9-r0 \
py3-watchfiles=0.19.0-r1 \ py3-watchfiles=0.19.0-r1 \
python3 \ python3 \

Wyświetl plik

@ -303,6 +303,23 @@ LISTENING_CREATED = "listening_created"
""" """
Called when a track is being listened Called when a track is being listened
""" """
LISTENING_SYNC = "listening_sync"
"""
Called by the task manager to trigger listening sync
"""
FAVORITE_CREATED = "favorite_created"
"""
Called when a track is being favorited
"""
FAVORITE_DELETED = "favorite_deleted"
"""
Called when a favorited track is being unfavorited
"""
FAVORITE_SYNC = "favorite_sync"
"""
Called by the task manager to trigger favorite sync
"""
SCAN = "scan" SCAN = "scan"
""" """

Wyświetl plik

@ -276,6 +276,7 @@ LOCAL_APPS = (
# Your stuff: custom apps go here # Your stuff: custom apps go here
"funkwhale_api.instance", "funkwhale_api.instance",
"funkwhale_api.audio", "funkwhale_api.audio",
"funkwhale_api.contrib.listenbrainz",
"funkwhale_api.music", "funkwhale_api.music",
"funkwhale_api.requests", "funkwhale_api.requests",
"funkwhale_api.favorites", "funkwhale_api.favorites",
@ -949,6 +950,16 @@ CELERY_BEAT_SCHEDULE = {
), ),
"options": {"expires": 60 * 60}, "options": {"expires": 60 * 60},
}, },
"listenbrainz.trigger_listening_sync_with_listenbrainz": {
"task": "listenbrainz.trigger_listening_sync_with_listenbrainz",
"schedule": crontab(day_of_week="*", minute="0", hour="3"),
"options": {"expires": 60 * 60 * 24},
},
"listenbrainz.trigger_favorite_sync_with_listenbrainz": {
"task": "listenbrainz.trigger_favorite_sync_with_listenbrainz",
"schedule": crontab(day_of_week="*", minute="0", hour="3"),
"options": {"expires": 60 * 60 * 24},
},
} }
if env.str("TYPESENSE_API_KEY", default=None): if env.str("TYPESENSE_API_KEY", default=None):

Wyświetl plik

@ -48,4 +48,5 @@ def get_activity(user, limit=20):
), ),
] ]
records = combined_recent(limit=limit, querysets=querysets) records = combined_recent(limit=limit, querysets=querysets)
return [r["object"] for r in records] return [r["object"] for r in records]

Wyświetl plik

@ -1,28 +1,31 @@
import liblistenbrainz import liblistenbrainz
from django.utils import timezone
import funkwhale_api import funkwhale_api
from config import plugins from config import plugins
from funkwhale_api.favorites import models as favorites_models
from funkwhale_api.history import models as history_models
from . import tasks
from .funkwhale_startup import PLUGIN from .funkwhale_startup import PLUGIN
@plugins.register_hook(plugins.LISTENING_CREATED, PLUGIN) @plugins.register_hook(plugins.LISTENING_CREATED, PLUGIN)
def submit_listen(listening, conf, **kwargs): def submit_listen(listening, conf, **kwargs):
user_token = conf["user_token"] user_token = conf["user_token"]
if not user_token: if not user_token and not conf["submit_listenings"]:
return return
logger = PLUGIN["logger"] logger = PLUGIN["logger"]
logger.info("Submitting listen to ListenBrainz") logger.info("Submitting listen to ListenBrainz")
client = liblistenbrainz.ListenBrainz() client = liblistenbrainz.ListenBrainz()
client.set_auth_token(user_token) client.set_auth_token(user_token)
listen = get_listen(listening.track) listen = get_lb_listen(listening)
client.submit_single_listen(listen) client.submit_single_listen(listen)
def get_listen(track): def get_lb_listen(listening):
track = listening.track
additional_info = { additional_info = {
"media_player": "Funkwhale", "media_player": "Funkwhale",
"media_player_version": funkwhale_api.__version__, "media_player_version": funkwhale_api.__version__,
@ -51,7 +54,83 @@ def get_listen(track):
return liblistenbrainz.Listen( return liblistenbrainz.Listen(
track_name=track.title, track_name=track.title,
artist_name=track.artist.name, artist_name=track.artist.name,
listened_at=int(timezone.now()), listened_at=listening.creation_date.timestamp(),
release_name=release_name, release_name=release_name,
additional_info=additional_info, additional_info=additional_info,
) )
@plugins.register_hook(plugins.FAVORITE_CREATED, PLUGIN)
def submit_favorite_creation(track_favorite, conf, **kwargs):
user_token = conf["user_token"]
if not user_token or not conf["submit_favorites"]:
return
logger = PLUGIN["logger"]
logger.info("Submitting favorite to ListenBrainz")
client = liblistenbrainz.ListenBrainz()
track = track_favorite.track
if not track.mbid:
logger.warning(
"This tracks doesn't have a mbid. Feedback will not be submitted to Listenbrainz"
)
return
client.submit_user_feedback(1, track.mbid)
@plugins.register_hook(plugins.FAVORITE_DELETED, PLUGIN)
def submit_favorite_deletion(track_favorite, conf, **kwargs):
user_token = conf["user_token"]
if not user_token or not conf["submit_favorites"]:
return
logger = PLUGIN["logger"]
logger.info("Submitting favorite deletion to ListenBrainz")
client = liblistenbrainz.ListenBrainz()
track = track_favorite.track
if not track.mbid:
logger.warning(
"This tracks doesn't have a mbid. Feedback will not be submitted to Listenbrainz"
)
return
client.submit_user_feedback(0, track.mbid)
@plugins.register_hook(plugins.LISTENING_SYNC, PLUGIN)
def sync_listenings_from_listenbrainz(user, conf):
user_name = conf["user_name"]
if not user_name or not conf["sync_listenings"]:
return
logger = PLUGIN["logger"]
logger.info("Getting listenings from ListenBrainz")
try:
last_ts = (
history_models.Listening.objects.filter(user=user)
.filter(source="Listenbrainz")
.latest("creation_date")
.values_list("creation_date", flat=True)
).timestamp()
except funkwhale_api.history.models.Listening.DoesNotExist:
tasks.import_listenbrainz_listenings(user, user_name, 0)
return
tasks.import_listenbrainz_listenings(user, user_name, last_ts)
@plugins.register_hook(plugins.FAVORITE_SYNC, PLUGIN)
def sync_favorites_from_listenbrainz(user, conf):
user_name = conf["user_name"]
if not user_name or not conf["sync_favorites"]:
return
try:
last_ts = (
favorites_models.TrackFavorite.objects.filter(user=user)
.filter(source="Listenbrainz")
.latest("creation_date")
.creation_date.timestamp()
)
except favorites_models.TrackFavorite.DoesNotExist:
tasks.import_listenbrainz_favorites(user, user_name, 0)
return
tasks.import_listenbrainz_favorites(user, user_name, last_ts)

Wyświetl plik

@ -3,7 +3,7 @@ from config import plugins
PLUGIN = plugins.get_plugin_config( PLUGIN = plugins.get_plugin_config(
name="listenbrainz", name="listenbrainz",
label="ListenBrainz", label="ListenBrainz",
description="A plugin that allows you to submit your listens to ListenBrainz.", description="A plugin that allows you to submit or sync your listens and favorites to ListenBrainz.",
homepage="https://docs.funkwhale.audio/users/builtinplugins.html#listenbrainz-plugin", # noqa homepage="https://docs.funkwhale.audio/users/builtinplugins.html#listenbrainz-plugin", # noqa
version="0.3", version="0.3",
user=True, user=True,
@ -13,6 +13,45 @@ PLUGIN = plugins.get_plugin_config(
"type": "text", "type": "text",
"label": "Your ListenBrainz user token", "label": "Your ListenBrainz user token",
"help": "You can find your user token in your ListenBrainz profile at https://listenbrainz.org/profile/", "help": "You can find your user token in your ListenBrainz profile at https://listenbrainz.org/profile/",
} },
{
"name": "user_name",
"type": "text",
"required": False,
"label": "Your ListenBrainz user name.",
"help": "Required for importing listenings and favorites with ListenBrainz \
but not to send activities",
},
{
"name": "submit_listenings",
"type": "boolean",
"default": True,
"label": "Enable listening submission to ListenBrainz",
"help": "If enabled, your listenings from Funkwhale will be imported into ListenBrainz.",
},
{
"name": "sync_listenings",
"type": "boolean",
"default": False,
"label": "Enable listenings sync",
"help": "If enabled, your listening from ListenBrainz will be imported into Funkwhale. This means they \
will be used along with Funkwhale listenings to filter out recently listened content or \
generate recommendations",
},
{
"name": "sync_favorites",
"type": "boolean",
"default": False,
"label": "Enable favorite sync",
"help": "If enabled, your favorites from ListenBrainz will be imported into Funkwhale. This means they \
will be used along with Funkwhale favorites (UI display, federation activity)",
},
{
"name": "submit_favorites",
"type": "boolean",
"default": False,
"label": "Enable favorite submission to ListenBrainz services",
"help": "If enabled, your favorites from Funkwhale will be submitted to ListenBrainz",
},
], ],
) )

Wyświetl plik

@ -0,0 +1,165 @@
import datetime
import liblistenbrainz
from django.utils import timezone
from config import plugins
from funkwhale_api.favorites import models as favorites_models
from funkwhale_api.history import models as history_models
from funkwhale_api.music import models as music_models
from funkwhale_api.taskapp import celery
from funkwhale_api.users import models
from .funkwhale_startup import PLUGIN
@celery.app.task(name="listenbrainz.trigger_listening_sync_with_listenbrainz")
def trigger_listening_sync_with_listenbrainz():
now = timezone.now()
active_month = now - datetime.timedelta(days=30)
users = (
models.User.objects.filter(plugins__code="listenbrainz")
.filter(plugins__conf__sync_listenings=True)
.filter(last_activity__gte=active_month)
)
for user in users:
plugins.trigger_hook(
plugins.LISTENING_SYNC,
user=user,
confs=plugins.get_confs(user),
)
@celery.app.task(name="listenbrainz.trigger_favorite_sync_with_listenbrainz")
def trigger_favorite_sync_with_listenbrainz():
now = timezone.now()
active_month = now - datetime.timedelta(days=30)
users = (
models.User.objects.filter(plugins__code="listenbrainz")
.filter(plugins__conf__sync_listenings=True)
.filter(last_activity__gte=active_month)
)
for user in users:
plugins.trigger_hook(
plugins.FAVORITE_SYNC,
user=user,
confs=plugins.get_confs(user),
)
@celery.app.task(name="listenbrainz.import_listenbrainz_listenings")
def import_listenbrainz_listenings(user, user_name, since):
client = liblistenbrainz.ListenBrainz()
response = client.get_listens(username=user_name, min_ts=since, count=100)
listens = response["payload"]["listens"]
while listens:
add_lb_listenings_to_db(listens, user)
new_ts = max(
listens,
key=lambda obj: datetime.datetime.fromtimestamp(
obj.listened_at, timezone.utc
),
)
response = client.get_listens(username=user_name, min_ts=new_ts, count=100)
listens = response["payload"]["listens"]
def add_lb_listenings_to_db(listens, user):
logger = PLUGIN["logger"]
fw_listens = []
for listen in listens:
if (
listen.additional_info.get("submission_client")
and listen.additional_info.get("submission_client")
== "Funkwhale ListenBrainz plugin"
and history_models.Listening.objects.filter(
creation_date=datetime.datetime.fromtimestamp(
listen.listened_at, timezone.utc
)
).exists()
):
logger.info(
f"Listen with ts {listen.listened_at} skipped because already in db"
)
continue
mbid = (
listen.mbid_mapping
if hasattr(listen, "mbid_mapping")
else listen.recording_mbid
)
if not mbid:
logger.info("Received listening that doesn't have a mbid. Skipping...")
try:
track = music_models.Track.objects.get(mbid=mbid)
except music_models.Track.DoesNotExist:
logger.info(
"Received listening that doesn't exist in fw database. Skipping..."
)
continue
user = user
fw_listen = history_models.Listening(
creation_date=datetime.datetime.fromtimestamp(
listen.listened_at, timezone.utc
),
track=track,
user=user,
source="Listenbrainz",
)
fw_listens.append(fw_listen)
history_models.Listening.objects.bulk_create(fw_listens)
@celery.app.task(name="listenbrainz.import_listenbrainz_favorites")
def import_listenbrainz_favorites(user, user_name, since):
client = liblistenbrainz.ListenBrainz()
response = client.get_user_feedback(username=user_name)
offset = 0
while response["feedback"]:
count = response["count"]
offset = offset + count
last_sync = min(
response["feedback"],
key=lambda obj: datetime.datetime.fromtimestamp(
obj["created"], timezone.utc
),
)["created"]
add_lb_feedback_to_db(response["feedback"], user)
if last_sync <= since or count == 0:
return
response = client.get_user_feedback(username=user_name, offset=offset)
def add_lb_feedback_to_db(feedbacks, user):
logger = PLUGIN["logger"]
for feedback in feedbacks:
try:
track = music_models.Track.objects.get(mbid=feedback["recording_mbid"])
except music_models.Track.DoesNotExist:
logger.info(
"Received feedback track that doesn't exist in fw database. Skipping..."
)
continue
if feedback["score"] == 1:
favorites_models.TrackFavorite.objects.get_or_create(
user=user,
creation_date=datetime.datetime.fromtimestamp(
feedback["created"], timezone.utc
),
track=track,
source="Listenbrainz",
)
elif feedback["score"] == 0:
try:
favorites_models.TrackFavorite.objects.get(
user=user, track=track
).delete()
except favorites_models.TrackFavorite.DoesNotExist:
continue
elif feedback["score"] == -1:
logger.info("Funkwhale doesn't support disliked tracks")

Wyświetl plik

@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2023-12-09 14:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('favorites', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='trackfavorite',
name='source',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

Wyświetl plik

@ -12,6 +12,7 @@ class TrackFavorite(models.Model):
track = models.ForeignKey( track = models.ForeignKey(
Track, related_name="track_favorites", on_delete=models.CASCADE Track, related_name="track_favorites", on_delete=models.CASCADE
) )
source = models.CharField(max_length=100, null=True, blank=True)
class Meta: class Meta:
unique_together = ("track", "user") unique_together = ("track", "user")

Wyświetl plik

@ -4,6 +4,7 @@ from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from config import plugins
from funkwhale_api.activity import record from funkwhale_api.activity import record
from funkwhale_api.common import fields, permissions from funkwhale_api.common import fields, permissions
from funkwhale_api.music import utils as music_utils from funkwhale_api.music import utils as music_utils
@ -44,6 +45,11 @@ class TrackFavoriteViewSet(
instance = self.perform_create(serializer) instance = self.perform_create(serializer)
serializer = self.get_serializer(instance=instance) serializer = self.get_serializer(instance=instance)
headers = self.get_success_headers(serializer.data) headers = self.get_success_headers(serializer.data)
plugins.trigger_hook(
plugins.FAVORITE_CREATED,
track_favorite=serializer.instance,
confs=plugins.get_confs(self.request.user),
)
record.send(instance) record.send(instance)
return Response( return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers serializer.data, status=status.HTTP_201_CREATED, headers=headers
@ -76,6 +82,11 @@ class TrackFavoriteViewSet(
except (AttributeError, ValueError, models.TrackFavorite.DoesNotExist): except (AttributeError, ValueError, models.TrackFavorite.DoesNotExist):
return Response({}, status=400) return Response({}, status=400)
favorite.delete() favorite.delete()
plugins.trigger_hook(
plugins.FAVORITE_DELETED,
track_favorite=favorite,
confs=plugins.get_confs(self.request.user),
)
return Response([], status=status.HTTP_204_NO_CONTENT) return Response([], status=status.HTTP_204_NO_CONTENT)
@extend_schema( @extend_schema(

Wyświetl plik

@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2023-12-09 14:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('history', '0002_auto_20180325_1433'),
]
operations = [
migrations.AddField(
model_name='listening',
name='source',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

Wyświetl plik

@ -17,6 +17,7 @@ class Listening(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
session_key = models.CharField(max_length=100, null=True, blank=True) session_key = models.CharField(max_length=100, null=True, blank=True)
source = models.CharField(max_length=100, null=True, blank=True)
class Meta: class Meta:
ordering = ("-creation_date",) ordering = ("-creation_date",)

Wyświetl plik

@ -36,7 +36,6 @@ def delete_non_alnum_characters(text):
def resolve_recordings_to_fw_track(recordings): def resolve_recordings_to_fw_track(recordings):
""" """
Tries to match a troi recording entity to a fw track using the typesense index. Tries to match a troi recording entity to a fw track using the typesense index.
It will save the results in the match_mbid attribute of the Track table.
For test purposes : if multiple fw tracks are returned, we log the information For test purposes : if multiple fw tracks are returned, we log the information
but only keep the best result in db to avoid duplicates. but only keep the best result in db to avoid duplicates.
""" """

Wyświetl plik

@ -0,0 +1,333 @@
import datetime
import logging
import liblistenbrainz
import pytest
from django.urls import reverse
from django.utils import timezone
from config import plugins
from funkwhale_api.contrib.listenbrainz import funkwhale_ready
from funkwhale_api.favorites import models as favorites_models
from funkwhale_api.history import models as history_models
def test_listenbrainz_submit_listen(logged_in_client, mocker, factories):
config = plugins.get_plugin_config(
name="listenbrainz",
description="A plugin that allows you to submit or sync your listens and favorites to ListenBrainz.",
conf=[],
source=False,
)
handler = mocker.Mock()
plugins.register_hook(plugins.LISTENING_CREATED, config)(handler)
plugins.set_conf(
"listenbrainz",
{
"sync_listenings": True,
"sync_favorites": True,
"submit_favorites": True,
"sync_favorites": True,
"user_token": "blablabla",
},
user=logged_in_client.user,
)
plugins.enable_conf("listenbrainz", True, logged_in_client.user)
track = factories["music.Track"]()
url = reverse("api:v1:history:listenings-list")
logged_in_client.post(url, {"track": track.pk})
logged_in_client.get(url)
listening = history_models.Listening.objects.get(user=logged_in_client.user)
handler.assert_called_once_with(listening=listening, conf=None)
def test_sync_listenings_from_listenbrainz(factories, mocker, caplog):
logger = logging.getLogger("plugins")
caplog.set_level(logging.INFO)
logger.addHandler(caplog.handler)
user = factories["users.User"]()
factories["music.Track"](mbid="f89db7f8-4a1f-4228-a0a1-e7ba028b7476")
track = factories["music.Track"](mbid="54c60860-f43d-484e-b691-7ab7ec8de559")
factories["history.Listening"](
creation_date=datetime.datetime.fromtimestamp(1871, timezone.utc), track=track
)
conf = {
"user_name": user.username,
"user_token": "user_tolkien",
"sync_listenings": True,
}
listens = {
"payload": {
"count": 25,
"user_id": "-- the MusicBrainz ID of the user --",
"listens": [
liblistenbrainz.Listen(
track_name="test",
artist_name="artist_test",
recording_mbid="f89db7f8-4a1f-4228-a0a1-e7ba028b7476",
additional_info={"submission_client": "not funkwhale"},
listened_at=-3124224000,
),
liblistenbrainz.Listen(
track_name="test2",
artist_name="artist_test2",
recording_mbid="54c60860-f43d-484e-b691-7ab7ec8de559",
additional_info={
"submission_client": "Funkwhale ListenBrainz plugin"
},
listened_at=1871,
),
liblistenbrainz.Listen(
track_name="test3",
artist_name="artist_test3",
listened_at=0,
),
],
}
}
no_more_listen = {
"payload": {
"count": 25,
"user_id": "Bilbo",
"listens": [],
}
}
mocker.patch.object(
funkwhale_ready.tasks.liblistenbrainz.ListenBrainz,
"get_listens",
side_effect=[listens, no_more_listen],
)
funkwhale_ready.sync_listenings_from_listenbrainz(user, conf)
assert history_models.Listening.objects.filter(
track__mbid="f89db7f8-4a1f-4228-a0a1-e7ba028b7476"
).exists()
assert "Listen with ts 1871 skipped because already in db" in caplog.text
assert "Received listening that doesn't have a mbid. Skipping..." in caplog.text
def test_sync_favorites_from_listenbrainz(factories, mocker, caplog):
logger = logging.getLogger("plugins")
caplog.set_level(logging.INFO)
logger.addHandler(caplog.handler)
user = factories["users.User"]()
# track lb fav
factories["music.Track"](mbid="195565db-65f9-4d0d-b347-5f0c85509528")
# random track
factories["music.Track"]()
# track lb neutral
track = factories["music.Track"](mbid="c5af5351-dbbf-4481-b52e-a480b6c57986")
favorite = factories["favorites.TrackFavorite"](track=track, user=user)
# last_sync
track_last_sync = factories["music.Track"](
mbid="c878ef2f-c08d-4a81-a047-f2a9f978cec7"
)
factories["favorites.TrackFavorite"](track=track_last_sync, source="Listenbrainz")
conf = {
"user_name": user.username,
"user_token": "user_tolkien",
"sync_favorites": True,
}
feedbacks = {
"count": 5,
"feedback": [
{
"created": 1701116226,
"recording_mbid": "195565db-65f9-4d0d-b347-5f0c85509528",
"score": 1,
"user_id": user.username,
},
{
"created": 1701116214,
"recording_mbid": "c5af5351-dbbf-4481-b52e-a480b6c57986",
"score": 0,
"user_id": user.username,
},
{
# last sync
"created": 1690775094,
"recording_mbid": "c878ef2f-c08d-4a81-a047-f2a9f978cec7",
"score": -1,
"user_id": user.username,
},
{
"created": 1690775093,
"recording_mbid": "1fd02cf2-7247-4715-8862-c378ec1965d2",
"score": 1,
"user_id": user.username,
},
],
"offset": 0,
"total_count": 4,
}
empty_feedback = {"count": 0, "feedback": [], "offset": 0, "total_count": 0}
mocker.patch.object(
funkwhale_ready.tasks.liblistenbrainz.ListenBrainz,
"get_user_feedback",
side_effect=[feedbacks, empty_feedback],
)
funkwhale_ready.sync_favorites_from_listenbrainz(user, conf)
assert favorites_models.TrackFavorite.objects.filter(
track__mbid="195565db-65f9-4d0d-b347-5f0c85509528"
).exists()
with pytest.raises(favorites_models.TrackFavorite.DoesNotExist):
favorite.refresh_from_db()
def test_sync_favorites_from_listenbrainz_since(factories, mocker, caplog):
logger = logging.getLogger("plugins")
caplog.set_level(logging.INFO)
logger.addHandler(caplog.handler)
user = factories["users.User"]()
# track lb fav
factories["music.Track"](mbid="195565db-65f9-4d0d-b347-5f0c85509528")
# track lb neutral
track = factories["music.Track"](mbid="c5af5351-dbbf-4481-b52e-a480b6c57986")
favorite = factories["favorites.TrackFavorite"](track=track, user=user)
# track should be not synced
factories["music.Track"](mbid="1fd02cf2-7247-4715-8862-c378ec196000")
# last_sync
track_last_sync = factories["music.Track"](
mbid="c878ef2f-c08d-4a81-a047-f2a9f978cec7"
)
factories["favorites.TrackFavorite"](
track=track_last_sync,
user=user,
source="Listenbrainz",
creation_date=datetime.datetime.fromtimestamp(1690775094),
)
conf = {
"user_name": user.username,
"user_token": "user_tolkien",
"sync_favorites": True,
}
feedbacks = {
"count": 5,
"feedback": [
{
"created": 1701116226,
"recording_mbid": "195565db-65f9-4d0d-b347-5f0c85509528",
"score": 1,
"user_id": user.username,
},
{
"created": 1701116214,
"recording_mbid": "c5af5351-dbbf-4481-b52e-a480b6c57986",
"score": 0,
"user_id": user.username,
},
{
# last sync
"created": 1690775094,
"recording_mbid": "c878ef2f-c08d-4a81-a047-f2a9f978cec7",
"score": -1,
"user_id": user.username,
},
{
"created": 1690775093,
"recording_mbid": "1fd02cf2-7247-4715-8862-c378ec1965d2",
"score": 1,
"user_id": user.username,
},
],
"offset": 0,
"total_count": 4,
}
second_feedback = {
"count": 0,
"feedback": [
{
"created": 0,
"recording_mbid": "1fd02cf2-7247-4715-8862-c378ec196000",
"score": 1,
"user_id": user.username,
},
],
"offset": 0,
"total_count": 0,
}
mocker.patch.object(
funkwhale_ready.tasks.liblistenbrainz.ListenBrainz,
"get_user_feedback",
side_effect=[feedbacks, second_feedback],
)
funkwhale_ready.sync_favorites_from_listenbrainz(user, conf)
assert favorites_models.TrackFavorite.objects.filter(
track__mbid="195565db-65f9-4d0d-b347-5f0c85509528"
).exists()
assert not favorites_models.TrackFavorite.objects.filter(
track__mbid="1fd02cf2-7247-4715-8862-c378ec196000"
).exists()
with pytest.raises(favorites_models.TrackFavorite.DoesNotExist):
favorite.refresh_from_db()
def test_submit_favorites_to_listenbrainz(factories, mocker, caplog):
logger = logging.getLogger("plugins")
caplog.set_level(logging.INFO)
logger.addHandler(caplog.handler)
user = factories["users.User"]()
factories["music.Track"](mbid="195565db-65f9-4d0d-b347-5f0c85509528")
factories["music.Track"](mbid="54c60860-f43d-484e-b691-7ab7ec8de559")
track = factories["music.Track"](mbid="c5af5351-dbbf-4481-b52e-a480b6c57986")
favorite = factories["favorites.TrackFavorite"](track=track)
conf = {
"user_name": user.username,
"user_token": "user_tolkien",
"submit_favorites": True,
}
patch = mocker.patch.object(
funkwhale_ready.tasks.liblistenbrainz.ListenBrainz,
"submit_user_feedback",
return_value="Success",
)
funkwhale_ready.submit_favorite_creation(favorite, conf)
patch.assert_called_once_with(1, track.mbid)
def test_submit_favorites_deletion(factories, mocker, caplog):
logger = logging.getLogger("plugins")
caplog.set_level(logging.INFO)
logger.addHandler(caplog.handler)
user = factories["users.User"]()
factories["music.Track"](mbid="195565db-65f9-4d0d-b347-5f0c85509528")
factories["music.Track"](mbid="54c60860-f43d-484e-b691-7ab7ec8de559")
track = factories["music.Track"](mbid="c5af5351-dbbf-4481-b52e-a480b6c57986")
favorite = factories["favorites.TrackFavorite"](track=track)
conf = {
"user_name": user.username,
"user_token": "user_tolkien",
"submit_favorites": True,
}
patch = mocker.patch.object(
funkwhale_ready.tasks.liblistenbrainz.ListenBrainz,
"submit_user_feedback",
return_value="Success",
)
funkwhale_ready.submit_favorite_deletion(favorite, conf)
patch.assert_called_once_with(0, track.mbid)

Wyświetl plik

@ -0,0 +1 @@
Add favorite and listening sync ith Listenbrainz (#2079)

Wyświetl plik

@ -19,8 +19,8 @@ The **ListenBrainz** plugin enables you to submit ({term}`scrobble<Scrobbling>`)
::: :::
:::{tab-item} Desktop :::{tab-item} Mobile
:sync: desktop :sync: mobile
1. Log in to your account. 1. Log in to your account.
2. Select the cog icon ({fa}`cog`) or your avatar to open the {guilabel}`Options` menu. 2. Select the cog icon ({fa}`cog`) or your avatar to open the {guilabel}`Options` menu.
@ -36,3 +36,48 @@ The **ListenBrainz** plugin enables you to submit ({term}`scrobble<Scrobbling>`)
:::: ::::
That's it! You've set up the **ListenBrainz** plugin. When you listen to tracks, the plugin sends the information to ListenBrainz. That's it! You've set up the **ListenBrainz** plugin. When you listen to tracks, the plugin sends the information to ListenBrainz.
## Enable data synchronization
The ListenBrainz plugin supports synchronizing listenings and track favorites with Funkwhale. To enable support for synchronization:
::::{tab-set}
:::{tab-item} Desktop
:sync: desktop
1. Log in to your account.
2. Select the cog icon ({fa}`cog`) or your avatar to expand the user menu.
3. Select {guilabel}`Settings`.
4. Scroll down to the {guilabel}`Plugins` section.
5. Select {guilabel}`Manage plugins`.
6. Find the {guilabel}`ListenBrainz` plugin.
7. Enter {guilabel}`Your ListenBrainz user name`. You can find this on your ListenBrainz profile.
8. Select the data you want to synchronize. The following options are available:
- {guilabel}`Enable listenings submission to ListenBrainz`: submit your Funkwhale listens to ListenBrainz.
- {guilabel}`Enable listenings sync`: pull listening data from ListenBrainz into Funkwhale.
- {guilabel}`Enable favorite submission to ListenBrainz services`: submit your Funkwhale favorites activity to ListenBrainz.
- {guilabel}`Enable favorite sync`: pull favorites data from ListenBrainz into Funkwhale.
9. Select {guilabel}`Save`.
:::
:::{tab-item} Mobile
:sync: mobile
1. Log in to your account.
2. Select the cog icon ({fa}`cog`) or your avatar to open the {guilabel}`Options` menu.
3. Select {guilabel}`Settings`.
4. Scroll down to the {guilabel}`Plugins` section.
5. Select {guilabel}`Manage plugins`.
6. Find the {guilabel}`ListenBrainz` plugin.
7. Enter {guilabel}`Your ListenBrainz user name`. You can find this on your ListenBrainz profile.
8. Select the data you want to synchronize. The following options are available:
- {guilabel}`Enable listenings submission to ListenBrainz`: submit your Funkwhale listens to ListenBrainz.
- {guilabel}`Enable listenings sync`: pull listening data from ListenBrainz into Funkwhale.
- {guilabel}`Enable favorite submission to ListenBrainz services`: submit your Funkwhale favorites activity to ListenBrainz.
- {guilabel}`Enable favorite sync`: pull favorites data from ListenBrainz into Funkwhale.
9. Select {guilabel}`Save`.
:::
::::

Wyświetl plik

@ -3203,6 +3203,7 @@
"allow": "Toelaten", "allow": "Toelaten",
"deny": "Afwijzen", "deny": "Afwijzen",
"funkwhaleInstance": "Officiële Glitchtip server van Funkwhale", "funkwhaleInstance": "Officiële Glitchtip server van Funkwhale",
"message": "De stacksporen zullen gedeeld worden naar { 0 } om ons te helpen begrijpen waarom en hoe deze fout is gebeurd.",
"title": "Om de kwaliteit van onze diensten te verbeteren, zouden we informatie willen verzamelen van de crashes die gebeuren tijdens jouw sessie." "title": "Om de kwaliteit van onze diensten te verbeteren, zouden we informatie willen verzamelen van de crashes die gebeuren tijdens jouw sessie."
}, },
"serviceWorker": { "serviceWorker": {
@ -3214,6 +3215,28 @@
} }
}, },
"views": { "views": {
"ChooseInstance": {
"button": {
"submit": "Indienen"
},
"header": {
"chooseInstance": "Kies je server",
"failure": "Het is niet mogelijk om verbinding te maken met de opgegeven URL",
"suggestions": "Aanbevelingen"
},
"help": {
"notFunkwhaleServer": "Het opgegeven adres is geen Funkwhale-server",
"selectPod": "Selecteer met welke Funkwhale-server je wil verbinden. Voer zelf de URL in, of kies een van de suggesties.",
"serverDown": "De server is mogelijk niet beschikbaar"
},
"label": {
"url": "Server-URL"
},
"message": {
"currentConnection": "U bent nu verbonden met { 0 }. Als je doorgaat wordt je verbinding verbroken met je huidige instance en je lokale data zal worden verwijderd.",
"newUrl": "Je gebruikt nu de Funkwhale-server op { url }"
}
},
"Notifications": { "Notifications": {
"button": { "button": {
"read": "Alles markeren als gelezen", "read": "Alles markeren als gelezen",
@ -4587,28 +4610,6 @@
}, },
"title": "Radio" "title": "Radio"
} }
},
"ChooseInstance": {
"button": {
"submit": "Indienen"
},
"header": {
"chooseInstance": "Kies je server",
"failure": "Het is niet mogelijk om verbinding te maken met de opgegeven URL",
"suggestions": "Aanbevelingen"
},
"help": {
"notFunkwhaleServer": "Het opgegeven adres is geen Funkwhale-server",
"selectPod": "Selecteer met welke Funkwhale-server je wil verbinden. Voer zelf de URL in, of kies een van de suggesties.",
"serverDown": "De server is mogelijk niet beschikbaar"
},
"label": {
"url": "Server-URL"
},
"message": {
"currentConnection": "U bent nu verbonden met { 0 }. Als je doorgaat wordt je verbinding verbroken met je huidige instance en je lokale data zal worden verwijderd.",
"newUrl": "Je gebruikt nu de Funkwhale-server op { url }"
}
} }
} }
} }