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/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 ee866a7a1..db68ed3c5 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", @@ -53,7 +54,7 @@ class PlaylistSerializer(serializers.ModelSerializer): "description", "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)