funkwhale/api/funkwhale_api/playlists/views.py

261 wiersze
10 KiB
Python
Czysty Zwykły widok Historia

2024-12-05 11:31:41 +00:00
import logging
from django.db import transaction
2018-06-10 08:55:16 +00:00
from django.db.models import Count
from drf_spectacular.utils import extend_schema
2025-01-03 18:17:25 +00:00
from rest_framework import exceptions, mixins, status, viewsets
2019-01-11 12:33:35 +00:00
from rest_framework.decorators import action
2024-12-05 11:31:41 +00:00
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.renderers import JSONRenderer
2018-06-10 08:55:16 +00:00
from rest_framework.response import Response
2018-06-10 08:55:16 +00:00
from funkwhale_api.common import fields, permissions
2025-01-03 18:17:25 +00:00
from funkwhale_api.federation import routes
2024-12-05 11:31:41 +00:00
from funkwhale_api.music import models as music_models
from funkwhale_api.music import serializers as music_serializers
2018-09-28 14:45:28 +00:00
from funkwhale_api.music import utils as music_utils
from funkwhale_api.users.oauth import permissions as oauth_permissions
2024-12-05 11:31:41 +00:00
from . import filters, models, parsers, renderers, serializers
logger = logging.getLogger(__name__)
2018-06-09 13:36:16 +00:00
class PlaylistViewSet(
2018-06-09 13:36:16 +00:00
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet,
):
serializer_class = serializers.PlaylistSerializer
queryset = (
2018-06-09 13:36:16 +00:00
models.Playlist.objects.all()
2025-01-03 18:17:25 +00:00
.select_related("actor__attachment_icon")
.annotate(tracks_count=Count("playlist_tracks", distinct=True))
2018-07-17 11:09:13 +00:00
.with_covers()
.with_duration()
)
permission_classes = [
oauth_permissions.ScopePermission,
permissions.OwnerPermission,
]
required_scope = "playlists"
anonymous_policy = "setting"
2018-06-09 13:36:16 +00:00
owner_checks = ["write"]
2025-01-03 18:17:25 +00:00
owner_field = "actor.user"
filterset_class = filters.PlaylistFilter
2018-06-09 13:36:16 +00:00
ordering_fields = ("id", "name", "creation_date", "modification_date")
2024-12-05 11:31:41 +00:00
parser_classes = [parsers.XspfParser, JSONParser, FormParser, MultiPartParser]
renderer_classes = [JSONRenderer, renderers.PlaylistXspfRenderer]
2025-01-03 18:17:25 +00:00
def update(self, request, *args, **kwargs):
playlist = self.get_object()
2024-12-05 11:31:41 +00:00
content_type = request.headers.get("Content-Type")
if content_type and "application/octet-stream" in content_type:
2025-01-03 18:17:25 +00:00
tracks = []
for track_data in request.data.get("tracks", []):
2024-12-05 11:31:41 +00:00
track_serializer = serializers.XspfTrackSerializer(data=track_data)
2025-01-03 18:17:25 +00:00
if track_serializer.is_valid():
tracks.append(track_serializer.validated_data)
else:
2024-12-05 11:31:41 +00:00
request.data["tracks"].remove(track_data)
logger.info(
f"Removing track {track_data} because we didn't find a match in db"
)
2025-01-03 18:17:25 +00:00
serializer = serializers.XspfSerializer(
playlist, data=request.data, partial=True
)
2024-12-05 11:31:41 +00:00
serializer.is_valid(raise_exception=True)
2025-01-03 18:17:25 +00:00
pl = serializer.save()
routes.outbox.dispatch(
{"type": "Update", "object": {"type": "Playlist"}},
context={"playlist": pl, "actor": playlist.actor},
)
2024-12-05 11:31:41 +00:00
return Response(serializers.PlaylistSerializer(pl).data, status=201)
2025-01-03 18:17:25 +00:00
response = super().update(request, *args, **kwargs)
routes.outbox.dispatch(
{"type": "Update", "object": {"type": "Playlist"}},
context={"playlist": self.get_object(), "actor": playlist.actor},
)
2024-12-05 11:31:41 +00:00
return response
2025-01-03 18:17:25 +00:00
def create(self, request, *args, **kwargs):
2024-12-05 11:31:41 +00:00
content_type = request.headers.get("Content-Type")
if content_type and "application/octet-stream" in content_type:
2025-01-03 18:17:25 +00:00
# We check if tracks are in the db, and exclude the ones we don't find
for track_data in list(request.data.get("tracks", [])):
2024-12-05 11:31:41 +00:00
track_serializer = serializers.XspfTrackSerializer(data=track_data)
2025-01-03 18:17:25 +00:00
if not track_serializer.is_valid():
2024-12-05 11:31:41 +00:00
request.data["tracks"].remove(track_data)
logger.info(
f"Removing track {track_data} because we didn't find a match in db"
)
2025-01-03 18:17:25 +00:00
serializer = serializers.XspfSerializer(data=request.data)
2024-12-05 11:31:41 +00:00
serializer.is_valid(raise_exception=True)
2025-01-03 18:17:25 +00:00
pl = serializer.save(request=request)
2024-12-05 11:31:41 +00:00
return Response(serializers.PlaylistSerializer(pl).data, status=201)
2025-01-03 18:17:25 +00:00
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
playlist = self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
routes.outbox.dispatch(
{"type": "Create", "object": {"type": "Playlist"}},
context={"playlist": playlist, "actor": playlist.actor},
)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
def destroy(self, request, *args, **kwargs):
playlist = self.get_object()
self.perform_destroy(playlist)
routes.outbox.dispatch(
{"type": "Delete", "object": {"type": "Playlist"}},
context={"playlist": playlist, "actor": playlist.actor},
)
return Response(status=status.HTTP_204_NO_CONTENT)
2022-09-25 13:57:22 +00:00
@extend_schema(responses=serializers.PlaylistTrackSerializer(many=True))
2019-01-11 12:33:35 +00:00
@action(methods=["get"], detail=True)
def tracks(self, request, *args, **kwargs):
playlist = self.get_object()
plts = playlist.playlist_tracks.all().for_nested_serialization(
music_utils.get_actor_from_request(request)
)
serializer = serializers.PlaylistTrackSerializer(plts, many=True)
2018-06-09 13:36:16 +00:00
data = {"count": len(plts), "results": serializer.data}
return Response(data, status=200)
2022-09-25 15:02:21 +00:00
@extend_schema(
operation_id="add_to_playlist", request=serializers.PlaylistAddManySerializer
)
2019-01-11 12:33:35 +00:00
@action(methods=["post"], detail=True)
@transaction.atomic
def add(self, request, *args, **kwargs):
playlist = self.get_object()
serializer = serializers.PlaylistAddManySerializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
plts = playlist.insert_many(
serializer.validated_data["tracks"],
serializer.validated_data["allow_duplicates"],
)
except exceptions.ValidationError as e:
2018-06-09 13:36:16 +00:00
payload = {"playlist": e.detail}
return Response(payload, status=400)
ids = [p.id for p in plts]
2018-06-09 13:36:16 +00:00
plts = (
models.PlaylistTrack.objects.filter(pk__in=ids)
.order_by("index")
.for_nested_serialization(music_utils.get_actor_from_request(request))
2018-06-09 13:36:16 +00:00
)
serializer = serializers.PlaylistTrackSerializer(plts, many=True)
2018-06-09 13:36:16 +00:00
data = {"count": len(plts), "results": serializer.data}
2025-01-03 18:17:25 +00:00
playlist.schedule_scan(playlist.actor, force=True)
return Response(data, status=201)
2022-09-25 13:57:22 +00:00
@extend_schema(operation_id="clear_playlist")
2019-01-11 12:33:35 +00:00
@action(methods=["delete"], detail=True)
2018-03-21 11:19:07 +00:00
@transaction.atomic
def clear(self, request, *args, **kwargs):
playlist = self.get_object()
playlist.playlist_tracks.all().delete()
2018-06-09 13:36:16 +00:00
playlist.save(update_fields=["modification_date"])
2025-01-03 18:17:25 +00:00
playlist.schedule_scan(playlist.actor)
2018-03-21 11:19:07 +00:00
return Response(status=204)
def get_queryset(self):
2018-09-28 14:45:28 +00:00
return self.queryset.filter(
2025-01-03 18:17:25 +00:00
fields.privacy_level_query(
self.request.user, "privacy_level", "actor__user"
)
).with_playable_plts(music_utils.get_actor_from_request(self.request))
def perform_create(self, serializer):
return serializer.save(
2025-01-03 18:17:25 +00:00
actor=self.request.user.actor,
privacy_level=serializer.validated_data.get(
2018-06-09 13:36:16 +00:00
"privacy_level", self.request.user.privacy_level
),
)
2022-09-25 13:57:22 +00:00
@extend_schema(operation_id="remove_from_playlist")
2020-07-27 13:31:49 +00:00
@action(methods=["post", "delete"], detail=True)
@transaction.atomic
def remove(self, request, *args, **kwargs):
playlist = self.get_object()
try:
index = int(request.data["index"])
assert index >= 0
except (KeyError, ValueError, AssertionError, TypeError):
return Response(status=400)
2020-07-27 13:31:49 +00:00
try:
plt = playlist.playlist_tracks.by_index(index)
except models.PlaylistTrack.DoesNotExist:
return Response(status=404)
plt.delete(update_indexes=True)
2025-01-03 18:17:25 +00:00
plt.playlist.schedule_scan(playlist.actor)
2020-07-27 13:31:49 +00:00
return Response(status=204)
2022-09-25 13:57:22 +00:00
@extend_schema(operation_id="reorder_track_in_playlist")
2020-07-27 13:31:49 +00:00
@action(methods=["post"], detail=True)
@transaction.atomic
def move(self, request, *args, **kwargs):
playlist = self.get_object()
try:
from_index = int(request.data["from"])
assert from_index >= 0
except (KeyError, ValueError, AssertionError, TypeError):
return Response({"detail": "invalid from index"}, status=400)
2020-07-27 13:31:49 +00:00
try:
to_index = int(request.data["to"])
assert to_index >= 0
except (KeyError, ValueError, AssertionError, TypeError):
return Response({"detail": "invalid to index"}, status=400)
2020-07-27 13:31:49 +00:00
try:
plt = playlist.playlist_tracks.by_index(from_index)
except models.PlaylistTrack.DoesNotExist:
return Response(status=404)
playlist.insert(plt, to_index)
2025-01-03 18:17:25 +00:00
plt.playlist.schedule_scan(playlist.actor)
2020-07-27 13:31:49 +00:00
return Response(status=204)
2024-12-05 11:31:41 +00:00
@extend_schema(operation_id="get_playlist_albums")
@action(methods=["get"], detail=True)
@transaction.atomic
def albums(self, request, *args, **kwargs):
playlist = self.get_object()
try:
albums_pks = playlist.playlist_tracks.values_list(
"track__album__pk", flat=True
).distinct()
except models.PlaylistTrack.DoesNotExist:
return Response(status=404)
releases = music_models.Album.objects.filter(pk__in=albums_pks)
serializer = music_serializers.AlbumSerializer(releases, many=True)
return Response(serializer.data, status=200)
@extend_schema(operation_id="get_playlist_artits")
@action(methods=["get"], detail=True)
@transaction.atomic
def artists(self, request, *args, **kwargs):
playlist = self.get_object()
try:
artists_pks = playlist.playlist_tracks.values_list(
"track__artist_credit__artist__pk", flat=True
).distinct()
except models.PlaylistTrack.DoesNotExist:
return Response(status=404)
artists = music_models.Artist.objects.filter(pk__in=artists_pks)
serializer = music_serializers.ArtistSerializer(artists, many=True)
2024-12-05 11:31:41 +00:00
return Response(serializer.data, status=200)