diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 977ab0bb1..76c251469 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -204,6 +204,7 @@ class APIActorSerializer(serializers.ModelSerializer): "type", "manually_approves_followers", "full_username", + "is_local", ] diff --git a/api/funkwhale_api/radios/radios.py b/api/funkwhale_api/radios/radios.py index 01a242216..08329c6ea 100644 --- a/api/funkwhale_api/radios/radios.py +++ b/api/funkwhale_api/radios/radios.py @@ -5,10 +5,11 @@ from django.db import connection from django.db.models import Q from rest_framework import serializers +from funkwhale_api.federation import models as federation_models +from funkwhale_api.federation import fields as federation_fields from funkwhale_api.moderation import filters as moderation_filters -from funkwhale_api.music.models import Artist, Track +from funkwhale_api.music.models import Artist, Library, Track, Upload from funkwhale_api.tags.models import Tag - from . import filters, models from .registries import registry @@ -271,3 +272,47 @@ class LessListenedRadio(SessionRadio): qs = super().get_queryset(**kwargs) listened = self.session.user.listenings.all().values_list("track", flat=True) return qs.exclude(pk__in=listened).order_by("?") + + +@registry.register(name="actor_content") +class ActorContentRadio(RelatedObjectRadio): + """ + Play content from given actor libraries + """ + + model = federation_models.Actor + related_object_field = federation_fields.ActorRelatedField(required=True) + + def get_related_object(self, value): + return value + + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs) + actor_uploads = Upload.objects.filter( + library__actor=self.session.related_object, + ) + return qs.filter(pk__in=actor_uploads.values("track")) + + def get_related_object_id_repr(self, obj): + return obj.full_username + + +@registry.register(name="library") +class LibraryRadio(RelatedObjectRadio): + """ + Play content from a given library + """ + + model = Library + related_object_field = serializers.UUIDField(required=True) + + def get_related_object(self, value): + return Library.objects.get(uuid=value) + + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs) + actor_uploads = Upload.objects.filter(library=self.session.related_object,) + return qs.filter(pk__in=actor_uploads.values("track")) + + def get_related_object_id_repr(self, obj): + return obj.uuid diff --git a/api/tests/radios/test_radios.py b/api/tests/radios/test_radios.py index 2aa60c36a..38a1ac831 100644 --- a/api/tests/radios/test_radios.py +++ b/api/tests/radios/test_radios.py @@ -47,6 +47,28 @@ def test_can_pick_by_weight(): assert picks[2] > picks[1] +def test_session_radio_excludes_previous_picks(factories): + tracks = factories["music.Track"].create_batch(5) + user = factories["users.User"]() + previous_choices = [] + for i in range(5): + TrackFavorite.add(track=random.choice(tracks), user=user) + + radio = radios.SessionRadio() + radio.radio_type = "favorites" + radio.start_session(user) + + for i in range(5): + pick = radio.pick(user=user, filter_playable=False) + assert pick in tracks + assert pick not in previous_choices + previous_choices.append(pick) + + with pytest.raises(ValueError): + # no more picks available + radio.pick(user=user, filter_playable=False) + + def test_can_get_choices_for_favorites_radio(factories): files = factories["music.Upload"].create_batch(10) tracks = [f.track for f in files] @@ -213,6 +235,77 @@ def test_can_start_tag_radio(factories): assert radio.pick(filter_playable=False) in good_tracks +def test_can_start_actor_content_radio(factories): + actor_library = factories["music.Library"](actor__local=True) + good_tracks = [ + factories["music.Upload"](playable=True, library=actor_library).track, + factories["music.Upload"](playable=True, library=actor_library).track, + factories["music.Upload"](playable=True, library=actor_library).track, + ] + factories["music.Upload"].create_batch(3, playable=True) + + radio = radios.ActorContentRadio() + session = radio.start_session( + actor_library.actor.user, related_object=actor_library.actor + ) + assert session.radio_type == "actor_content" + + for i in range(3): + assert radio.pick() in good_tracks + + +def test_can_start_actor_content_radio_from_api( + logged_in_api_client, preferences, factories +): + actor = factories["federation.Actor"]() + url = reverse("api:v1:radios:sessions-list") + + response = logged_in_api_client.post( + url, {"radio_type": "actor_content", "related_object_id": actor.full_username} + ) + + assert response.status_code == 201 + + session = models.RadioSession.objects.latest("id") + + assert session.radio_type == "actor_content" + assert session.related_object == actor + + +def test_can_start_library_radio(factories): + user = factories["users.User"]() + library = factories["music.Library"]() + good_tracks = [ + factories["music.Upload"](library=library).track, + factories["music.Upload"](library=library).track, + factories["music.Upload"](library=library).track, + ] + factories["music.Upload"].create_batch(3) + + radio = radios.LibraryRadio() + session = radio.start_session(user, related_object=library) + assert session.radio_type == "library" + + for i in range(3): + assert radio.pick(filter_playable=False) in good_tracks + + +def test_can_start_library_radio_from_api(logged_in_api_client, preferences, factories): + library = factories["music.Library"]() + url = reverse("api:v1:radios:sessions-list") + + response = logged_in_api_client.post( + url, {"radio_type": "library", "related_object_id": library.uuid} + ) + + assert response.status_code == 201 + + session = models.RadioSession.objects.latest("id") + + assert session.radio_type == "library" + assert session.related_object == library + + def test_can_start_artist_radio_from_api(logged_in_api_client, preferences, factories): artist = factories["music.Artist"]() url = reverse("api:v1:radios:sessions-list") diff --git a/changes/changelog.d/radio.enhancement b/changes/changelog.d/radio.enhancement new file mode 100644 index 000000000..a46a9cd92 --- /dev/null +++ b/changes/changelog.d/radio.enhancement @@ -0,0 +1 @@ +Added two new radios to play your own content or a given library tracks diff --git a/front/src/components/federation/LibraryWidget.vue b/front/src/components/federation/LibraryWidget.vue index 7d2418087..3bdc52089 100644 --- a/front/src/components/federation/LibraryWidget.vue +++ b/front/src/components/federation/LibraryWidget.vue @@ -16,7 +16,7 @@ { + axios.get(url, {params: params}).then((response) => { self.previousPage = response.data.previous self.nextPage = response.data.next self.isLoading = false diff --git a/front/src/components/library/Radios.vue b/front/src/components/library/Radios.vue index b03801d80..fcc14b807 100644 --- a/front/src/components/library/Radios.vue +++ b/front/src/components/library/Radios.vue @@ -10,6 +10,7 @@ Instance radios
+ diff --git a/front/src/components/radios/Card.vue b/front/src/components/radios/Card.vue index e72b9f1c1..55ccb4005 100644 --- a/front/src/components/radios/Card.vue +++ b/front/src/components/radios/Card.vue @@ -16,7 +16,7 @@
- + { return { + actor_content: { + name: 'Your content', + description: "Picks from your own libraries" + }, random: { name: 'Random', description: "Totally random picks, maybe you'll discover new things?" diff --git a/front/src/views/content/libraries/DetailArea.vue b/front/src/views/content/libraries/DetailArea.vue index 0a73c90b9..62928c05c 100644 --- a/front/src/views/content/libraries/DetailArea.vue +++ b/front/src/views/content/libraries/DetailArea.vue @@ -4,6 +4,7 @@

Current library

+
@@ -12,12 +13,14 @@