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
This commit is contained in:
Petitminion 2025-03-18 01:36:32 +01:00
parent 0c2be3e576
commit 988937f34d
9 changed files with 43 additions and 20 deletions

View File

@ -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 # 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}) no_user_query = models.Q(**{f"{user_field}__isnull": True})
return ( return (

View File

@ -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 fields as common_fields
from funkwhale_api.common import serializers as common_serializers from funkwhale_api.common import serializers as common_serializers
from funkwhale_api.music import models as music_models 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 funkwhale_api.users import serializers as users_serializers
from . import filters, models from . import filters, models
@ -200,6 +201,7 @@ FETCH_OBJECT_CONFIG = {
"upload": {"queryset": music_models.Upload.objects.all(), "id_attr": "uuid"}, "upload": {"queryset": music_models.Upload.objects.all(), "id_attr": "uuid"},
"account": {"queryset": models.Actor.objects.all(), "id_attr": "full_username"}, "account": {"queryset": models.Actor.objects.all(), "id_attr": "full_username"},
"channel": {"queryset": audio_models.Channel.objects.all(), "id_attr": "uuid"}, "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) FETCH_OBJECT_FIELD = common_fields.GenericRelation(FETCH_OBJECT_CONFIG)

View File

@ -405,6 +405,7 @@ class Fetch(models.Model):
serializers.ChannelUploadSerializer, serializers.ChannelUploadSerializer,
], ],
contexts.FW.Library: [serializers.LibrarySerializer], contexts.FW.Library: [serializers.LibrarySerializer],
contexts.FW.Playlist: [serializers.PlaylistSerializer],
contexts.AS.Group: [serializers.ActorSerializer], contexts.AS.Group: [serializers.ActorSerializer],
contexts.AS.Person: [serializers.ActorSerializer], contexts.AS.Person: [serializers.ActorSerializer],
contexts.AS.Organization: [serializers.ActorSerializer], contexts.AS.Organization: [serializers.ActorSerializer],

View File

@ -940,7 +940,7 @@ OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES}
def get_additional_fields(data): def get_additional_fields(data):
UNSET = object() UNSET = object()
additional_fields = {} additional_fields = {}
for field in ["name", "summary"]: for field in ["name", "summary", "library", "audience", "published"]:
v = data.get(field, UNSET) v = data.get(field, UNSET)
if v == UNSET: if v == UNSET:
continue continue
@ -2399,7 +2399,6 @@ class PlaylistSerializer(jsonld.JsonLdSerializer):
"name": playlist.name, "name": playlist.name,
"attributedTo": playlist.actor.fid, "attributedTo": playlist.actor.fid,
"published": playlist.creation_date.isoformat(), "published": playlist.creation_date.isoformat(),
"audience": playlist.privacy_level,
"library": playlist.library.fid, "library": playlist.library.fid,
} }
payload["audience"] = ( payload["audience"] = (
@ -2430,9 +2429,15 @@ class PlaylistSerializer(jsonld.JsonLdSerializer):
"actor": actor, "actor": actor,
"name": validated_data["name"], "name": validated_data["name"],
"creation_date": validated_data["published"], "creation_date": validated_data["published"],
"privacy_level": validated_data["audience"],
"library": library, "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( playlist, created = playlists_models.Playlist.objects.update_or_create(
defaults=ap_to_fw_data, defaults=ap_to_fw_data,
**{ **{
@ -2445,16 +2450,8 @@ class PlaylistSerializer(jsonld.JsonLdSerializer):
return playlist return playlist
def validate(self, data): def update(self, instance, validated_data):
validated_data = super().validate(data) return self.create(validated_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
class PlaylistCollectionSerializer(PaginatedCollectionSerializer): class PlaylistCollectionSerializer(PaginatedCollectionSerializer):
@ -2474,6 +2471,8 @@ class PlaylistCollectionSerializer(PaginatedCollectionSerializer):
"tracks", "tracks",
), ),
"type": "Playlist", "type": "Playlist",
"library": playlist.library.fid,
"published": playlist.creation_date.isoformat(),
} }
r = super().to_representation(conf) r = super().to_representation(conf)
return r return r

View File

@ -365,6 +365,20 @@ def has_library_access(request, library):
return library.received_follows.filter(actor=actor, approved=True).exists() 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( class MusicLibraryViewSet(
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
): ):
@ -731,11 +745,13 @@ class PlaylistViewSet(
"track", "track",
), ),
"item_serializer": serializers.PlaylistTrackSerializer, "item_serializer": serializers.PlaylistTrackSerializer,
"library": playlist.library.fid,
} }
return get_collection_response( return get_collection_response(
conf=conf, conf=conf,
querystring=request.GET, querystring=request.GET,
collection_serializer=serializers.PlaylistCollectionSerializer(playlist), collection_serializer=serializers.PlaylistCollectionSerializer(playlist),
page_access_check=lambda: has_playlist_access(request, playlist),
) )

View File

@ -100,7 +100,7 @@ class Playlist(federation_models.FederationMixin):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return f"/library/playlists/{self.pk}" return f"/library/playlists/{self.uuid}"
def get_federation_id(self): def get_federation_id(self):
if self.fid: if self.fid:

View File

@ -39,7 +39,8 @@ class PlaylistSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Playlist model = models.Playlist
fields = ( fields = (
"id", "uuid",
"fid",
"name", "name",
"actor", "actor",
"modification_date", "modification_date",
@ -51,7 +52,7 @@ class PlaylistSerializer(serializers.ModelSerializer):
"is_playable", "is_playable",
"library", "library",
) )
read_only_fields = ["id", "modification_date", "creation_date"] read_only_fields = ["uuid", "fid", "modification_date", "creation_date"]
@extend_schema_field(OpenApiTypes.URI) @extend_schema_field(OpenApiTypes.URI)
def get_library(self, obj): def get_library(self, obj):

View File

@ -30,6 +30,7 @@ class PlaylistViewSet(
mixins.ListModelMixin, mixins.ListModelMixin,
viewsets.GenericViewSet, viewsets.GenericViewSet,
): ):
lookup_field = "uuid"
serializer_class = serializers.PlaylistSerializer serializer_class = serializers.PlaylistSerializer
queryset = ( queryset = (
models.Playlist.objects.all() models.Playlist.objects.all()

View File

@ -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] 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] 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] 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 ### Follow up
- [ ] Add the frontend playlist button in the new ui - [ ] Add the frontend playlist button in the new ui
- [ ] Finish library drop (delete libraries endpoints) - [ ] Playlist discovery : display playlist fid in the frontend
- [ ] 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)
- [ ] 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) - [ ] 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 - [ ] 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)