From 988937f34decd7da9fa036e0e629ea05bd8dd001 Mon Sep 17 00:00:00 2001 From: Petitminion Date: Tue, 18 Mar 2025 01:36:32 +0100 Subject: [PATCH] Support playlist fetch, this ill break the frontend (need to update playlist.uuid for endpoint instead of playlist.id) Done locally but will commit after discussion, we can also delete lookup_field = "uuid" from playlistviewset --- api/funkwhale_api/common/fields.py | 1 + .../federation/api_serializers.py | 2 ++ api/funkwhale_api/federation/models.py | 1 + api/funkwhale_api/federation/serializers.py | 25 +++++++++---------- api/funkwhale_api/federation/views.py | 16 ++++++++++++ api/funkwhale_api/playlists/models.py | 2 +- api/funkwhale_api/playlists/serializers.py | 5 ++-- api/funkwhale_api/playlists/views.py | 1 + .../playlist-library-federation/index.md | 10 +++++--- 9 files changed, 43 insertions(+), 20 deletions(-) diff --git a/api/funkwhale_api/common/fields.py b/api/funkwhale_api/common/fields.py index 81c3a9c1a..f262fd451 100644 --- a/api/funkwhale_api/common/fields.py +++ b/api/funkwhale_api/common/fields.py @@ -31,6 +31,7 @@ def privacy_level_query(user, lookup_field="privacy_level", user_field="user"): } ) # Federated TrackFavorite don't have an user associated with the trackfavorite.actor + # to do : if we implement the followers privacy_level this will become a problem no_user_query = models.Q(**{f"{user_field}__isnull": True}) return ( diff --git a/api/funkwhale_api/federation/api_serializers.py b/api/funkwhale_api/federation/api_serializers.py index 2c1d5ae49..a9a553c2c 100644 --- a/api/funkwhale_api/federation/api_serializers.py +++ b/api/funkwhale_api/federation/api_serializers.py @@ -13,6 +13,7 @@ from funkwhale_api.audio import models as audio_models from funkwhale_api.common import fields as common_fields from funkwhale_api.common import serializers as common_serializers from funkwhale_api.music import models as music_models +from funkwhale_api.playlists import models as playlist_models from funkwhale_api.users import serializers as users_serializers from . import filters, models @@ -200,6 +201,7 @@ FETCH_OBJECT_CONFIG = { "upload": {"queryset": music_models.Upload.objects.all(), "id_attr": "uuid"}, "account": {"queryset": models.Actor.objects.all(), "id_attr": "full_username"}, "channel": {"queryset": audio_models.Channel.objects.all(), "id_attr": "uuid"}, + "playlist": {"queryset": playlist_models.Playlist.objects.all(), "id_attr": "uuid"}, } FETCH_OBJECT_FIELD = common_fields.GenericRelation(FETCH_OBJECT_CONFIG) diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 270e6857a..b01355493 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -405,6 +405,7 @@ class Fetch(models.Model): serializers.ChannelUploadSerializer, ], contexts.FW.Library: [serializers.LibrarySerializer], + contexts.FW.Playlist: [serializers.PlaylistSerializer], contexts.AS.Group: [serializers.ActorSerializer], contexts.AS.Person: [serializers.ActorSerializer], contexts.AS.Organization: [serializers.ActorSerializer], diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index c9e146ed3..7dd8984d4 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -940,7 +940,7 @@ OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES} def get_additional_fields(data): UNSET = object() additional_fields = {} - for field in ["name", "summary"]: + for field in ["name", "summary", "library", "audience", "published"]: v = data.get(field, UNSET) if v == UNSET: continue @@ -2399,7 +2399,6 @@ class PlaylistSerializer(jsonld.JsonLdSerializer): "name": playlist.name, "attributedTo": playlist.actor.fid, "published": playlist.creation_date.isoformat(), - "audience": playlist.privacy_level, "library": playlist.library.fid, } payload["audience"] = ( @@ -2430,9 +2429,15 @@ class PlaylistSerializer(jsonld.JsonLdSerializer): "actor": actor, "name": validated_data["name"], "creation_date": validated_data["published"], - "privacy_level": validated_data["audience"], "library": library, } + if not actor.is_local: + ap_to_fw_data["privacy_level"] = ( + contexts.AS.Public + if validated_data["privacy_level"] == "everyone" + else "" + ) + playlist, created = playlists_models.Playlist.objects.update_or_create( defaults=ap_to_fw_data, **{ @@ -2445,16 +2450,8 @@ class PlaylistSerializer(jsonld.JsonLdSerializer): return playlist - def validate(self, data): - validated_data = super().validate(data) - if validated_data["audience"] not in [ - "https://www.w3.org/ns/activitystreams#Public", - "everyone", - ]: - raise serializers.ValidationError("Privacy_level must be everyone") - - validated_data["audience"] = "everyone" - return validated_data + def update(self, instance, validated_data): + return self.create(validated_data) class PlaylistCollectionSerializer(PaginatedCollectionSerializer): @@ -2474,6 +2471,8 @@ class PlaylistCollectionSerializer(PaginatedCollectionSerializer): "tracks", ), "type": "Playlist", + "library": playlist.library.fid, + "published": playlist.creation_date.isoformat(), } r = super().to_representation(conf) return r diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py index 7c75e3594..8509023c6 100644 --- a/api/funkwhale_api/federation/views.py +++ b/api/funkwhale_api/federation/views.py @@ -365,6 +365,20 @@ def has_library_access(request, library): return library.received_follows.filter(actor=actor, approved=True).exists() +def has_playlist_access(request, playlist): + if playlist.privacy_level == "everyone": + return True + if request.user.is_authenticated and request.user.is_superuser: + return True + + try: + actor = request.actor + except AttributeError: + return False + + return playlist.library.received_follows.filter(actor=actor, approved=True).exists() + + class MusicLibraryViewSet( FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet ): @@ -731,11 +745,13 @@ class PlaylistViewSet( "track", ), "item_serializer": serializers.PlaylistTrackSerializer, + "library": playlist.library.fid, } return get_collection_response( conf=conf, querystring=request.GET, collection_serializer=serializers.PlaylistCollectionSerializer(playlist), + page_access_check=lambda: has_playlist_access(request, playlist), ) diff --git a/api/funkwhale_api/playlists/models.py b/api/funkwhale_api/playlists/models.py index 1b306883a..bde7247a6 100644 --- a/api/funkwhale_api/playlists/models.py +++ b/api/funkwhale_api/playlists/models.py @@ -100,7 +100,7 @@ class Playlist(federation_models.FederationMixin): return self.name def get_absolute_url(self): - return f"/library/playlists/{self.pk}" + return f"/library/playlists/{self.uuid}" def get_federation_id(self): if self.fid: diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py index 0e7ff6667..eb541c3d6 100644 --- a/api/funkwhale_api/playlists/serializers.py +++ b/api/funkwhale_api/playlists/serializers.py @@ -39,7 +39,8 @@ class PlaylistSerializer(serializers.ModelSerializer): class Meta: model = models.Playlist fields = ( - "id", + "uuid", + "fid", "name", "actor", "modification_date", @@ -51,7 +52,7 @@ class PlaylistSerializer(serializers.ModelSerializer): "is_playable", "library", ) - read_only_fields = ["id", "modification_date", "creation_date"] + read_only_fields = ["uuid", "fid", "modification_date", "creation_date"] @extend_schema_field(OpenApiTypes.URI) def get_library(self, obj): diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py index 39f07d17f..9e052572b 100644 --- a/api/funkwhale_api/playlists/views.py +++ b/api/funkwhale_api/playlists/views.py @@ -30,6 +30,7 @@ class PlaylistViewSet( mixins.ListModelMixin, viewsets.GenericViewSet, ): + lookup_field = "uuid" serializer_class = serializers.PlaylistSerializer queryset = ( models.Playlist.objects.all() diff --git a/docs/specs/playlist-library-federation/index.md b/docs/specs/playlist-library-federation/index.md index 07b8ce011..abc18bb30 100644 --- a/docs/specs/playlist-library-federation/index.md +++ b/docs/specs/playlist-library-federation/index.md @@ -51,13 +51,15 @@ There is no other reason to share the playlit.library to remote. - [x] Support library.playlist_uploads in library scan -> add playlist_uploads in items in library federation viewset - [x] investigate library scan bug : don't delete old content of the lib (local cache?): we need to empty the playlist before the scan(not ideal but less work) - [x] check actor has only have three built-in libs and upload.playlist_libraries is private after migration +- [x] Playlist discovery : fetch federation endpoint for playlists +- [ ] Seem like the federation fetch (either with fetch endpoint or retreive_ap_obj) is deleting the `privacy_level` since `audience` can only be public or null. Avoid `privacy_level` to be updated if it a local playlist. ### Follow up - [ ] Add the frontend playlist button in the new ui -- [ ] Finish library drop (delete libraries endpoints) -- [ ] Playlist discovery : fetch federation endpoint for playlists -- [ ] Playlist discovery : add the playlist to my playlist collection = follow request to playlist -- [ ] Playlist Track activity (to avoid having to refetch the whole playlist) +- [ ] Playlist discovery : display playlist fid in the frontend - [ ] Document : The user that want to federate need to activate remote activities in it's user settings. Even if the library is public the playlist activities will not be sended to remote -> We need to implement a followers activity setting (#2362) - [ ] allow users to change the upload to another built-in lib, make sure the upload is not delete (we would loose the playlist_library relation) but only updated + +- [ ] Playlist discovery : add the playlist to my playlist collection = follow request to playlist +- [ ] Playlist Track activity (to avoid having to refetch the whole playlist)