serialize playlists, add custom playlist endpoints

This commit is contained in:
Simon
2025-02-11 19:22:39 +07:00
parent 1035bb0947
commit 9022817096
4 changed files with 213 additions and 67 deletions

View File

@@ -49,3 +49,43 @@ class PlaylistListQuerySerializer(serializers.Serializer):
type = serializers.ChoiceField( type = serializers.ChoiceField(
choices=["regular", "custom"], required=False choices=["regular", "custom"], required=False
) )
class PlaylistSingleAddSerializer(serializers.Serializer):
"""single item to add"""
playlist_id = serializers.CharField()
playlist_subscribed = serializers.ChoiceField(choices=[True])
class PlaylistBulkAddSerializer(serializers.Serializer):
"""bulk add playlists serializers"""
data = PlaylistSingleAddSerializer(many=True)
class PlaylistSingleUpdate(serializers.Serializer):
"""update state of single playlist"""
playlist_subscribed = serializers.BooleanField()
class PlaylistListCustomPostSerializer(serializers.Serializer):
"""serialize list post custom playlist"""
playlist_name = serializers.CharField()
class PlaylistCustomPostSerializer(serializers.Serializer):
"""serialize playlist custom action"""
action = serializers.ChoiceField(
choices=["create", "remove", "up", "down", "top", "bottom"]
)
video_id = serializers.CharField()
class PlaylistDeleteQuerySerializer(serializers.Serializer):
"""serialize playlist delete query params"""
delete_videos = serializers.BooleanField(required=False)

View File

@@ -27,8 +27,7 @@ class QueryBuilder:
subscribed = self.request_params.get("subscribed") subscribed = self.request_params.get("subscribed")
if subscribed: if subscribed:
subed_bool = subscribed[0] == "true" must_list.append({"match": {"playlist_subscribed": subscribed}})
must_list.append({"match": {"playlist_subscribed": subed_bool}})
playlist_type = self.request_params.get("type") playlist_type = self.request_params.get("type")
if playlist_type: if playlist_type:

View File

@@ -9,6 +9,16 @@ urlpatterns = [
views.PlaylistApiListView.as_view(), views.PlaylistApiListView.as_view(),
name="api-playlist-list", name="api-playlist-list",
), ),
path(
"custom/",
views.PlaylistCustomApiListView.as_view(),
name="api-custom-playlist-list",
),
path(
"custom/<slug:playlist_id>/",
views.PlaylistCustomApiView.as_view(),
name="api-custom-playlist",
),
path( path(
"<slug:playlist_id>/", "<slug:playlist_id>/",
views.PlaylistApiView.as_view(), views.PlaylistApiView.as_view(),

View File

@@ -2,18 +2,25 @@
import uuid import uuid
from common.serializers import ErrorResponseSerializer from common.serializers import (
AsyncTaskResponseSerializer,
ErrorResponseSerializer,
)
from common.views_base import AdminWriteOnly, ApiBaseView from common.views_base import AdminWriteOnly, ApiBaseView
from download.src.subscriptions import PlaylistSubscription from download.src.subscriptions import PlaylistSubscription
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from playlist.serializers import ( from playlist.serializers import (
PlaylistBulkAddSerializer,
PlaylistCustomPostSerializer,
PlaylistDeleteQuerySerializer,
PlaylistListCustomPostSerializer,
PlaylistListQuerySerializer, PlaylistListQuerySerializer,
PlaylistListSerializer, PlaylistListSerializer,
PlaylistSerializer, PlaylistSerializer,
PlaylistSingleUpdate,
) )
from playlist.src.index import YoutubePlaylist from playlist.src.index import YoutubePlaylist
from playlist.src.query_building import QueryBuilder from playlist.src.query_building import QueryBuilder
from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from task.tasks import subscribe_to from task.tasks import subscribe_to
from user.src.user_config import UserConfig from user.src.user_config import UserConfig
@@ -61,46 +68,127 @@ class PlaylistApiListView(ApiBaseView):
return Response(response_serializer.data) return Response(response_serializer.data)
@extend_schema(
request=PlaylistBulkAddSerializer(),
responses={
200: OpenApiResponse(AsyncTaskResponseSerializer()),
400: OpenApiResponse(
ErrorResponseSerializer(), description="Bad request"
),
},
)
def post(self, request): def post(self, request):
"""subscribe/unsubscribe to list of playlists""" """async subscribe to list of playlists"""
data = request.data data_serializer = PlaylistBulkAddSerializer(data=request.data)
try: data_serializer.is_valid(raise_exception=True)
to_add = data["data"] validated_data = data_serializer.validated_data
except KeyError:
message = "missing expected data key"
print(message)
return Response({"message": message}, status=400)
data = data["data"] pending = [i["playlist_id"] for i in validated_data["data"]]
if isinstance(data, dict): if not pending:
custom_name = data.get("create") error = ErrorResponseSerializer({"error": "nothing to subscribe"})
if custom_name: return Response(error.data, status=400)
url_str = " ".join(pending)
task = subscribe_to.delay(url_str, expected_type="playlist")
message = {
"message": "playlist subscribe task started",
"task_id": task.id,
}
serializer = AsyncTaskResponseSerializer(message)
return Response(serializer.data)
class PlaylistCustomApiListView(ApiBaseView):
"""resolves to /api/playlist/custom/
POST: Create new custom playlist
"""
search_base = "ta_playlist/_search/"
permission_classes = [AdminWriteOnly]
@extend_schema(
request=PlaylistListCustomPostSerializer(),
responses={
200: OpenApiResponse(PlaylistSerializer()),
400: OpenApiResponse(
ErrorResponseSerializer(), description="Bad request"
),
},
)
def post(self, request):
"""create new custom playlist"""
serializer = PlaylistListCustomPostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data
custom_name = validated_data["playlist_name"]
playlist_id = f"TA_playlist_{uuid.uuid4()}" playlist_id = f"TA_playlist_{uuid.uuid4()}"
custom_playlist = YoutubePlaylist(playlist_id) custom_playlist = YoutubePlaylist(playlist_id)
custom_playlist.create(custom_name) custom_playlist.create(custom_name)
return Response(custom_playlist.json_data)
pending = [] response_serializer = PlaylistSerializer(custom_playlist.json_data)
for playlist_item in to_add:
playlist_id = playlist_item["playlist_id"]
if playlist_item["playlist_subscribed"]:
pending.append(playlist_id)
else:
self._unsubscribe(playlist_id)
if pending: return Response(response_serializer.data)
url_str = " ".join(pending)
subscribe_to.delay(url_str, expected_type="playlist")
return Response(data)
@staticmethod class PlaylistCustomApiView(ApiBaseView):
def _unsubscribe(playlist_id: str): """resolves to /api/playlist/custom/<playlist_id>/
"""unsubscribe""" POST: modify custom playlist
print(f"[{playlist_id}] unsubscribe from playlist") """
_ = PlaylistSubscription().change_subscribe(
playlist_id, subscribe_status=False search_base = "ta_playlist/_doc/"
permission_classes = [AdminWriteOnly]
@extend_schema(
request=PlaylistCustomPostSerializer(),
responses={
200: OpenApiResponse(PlaylistSerializer()),
400: OpenApiResponse(
ErrorResponseSerializer(), description="bad request"
),
404: OpenApiResponse(
ErrorResponseSerializer(), description="playlist not found"
),
},
) )
def post(self, request, playlist_id):
"""modify custom playlist"""
data_serializer = PlaylistCustomPostSerializer(data=request.data)
data_serializer.is_valid(raise_exception=True)
validated_data = data_serializer.validated_data
self.get_document(playlist_id)
if not self.response:
error = ErrorResponseSerializer({"error": "playlist not found"})
return Response(error.data, status=404)
if not self.response["playlist_type"] == "custom":
error = ErrorResponseSerializer(
{"error": f"playlist with ID {playlist_id} is not custom"}
)
return Response(error.data, status=400)
action = validated_data.get("action")
video_id = validated_data.get("video_id")
playlist = YoutubePlaylist(playlist_id)
if action == "create":
try:
playlist.add_video_to_playlist(video_id)
except TypeError:
error = ErrorResponseSerializer(
{"error": f"failed to add video {video_id} to playlist"}
)
return Response(error.data, status=400)
else:
hide = UserConfig(request.user.id).get_value("hide_watched")
playlist.move_video(video_id, action, hide_watched=hide)
response_serializer = PlaylistSerializer(playlist.json_data)
return Response(response_serializer.data)
class PlaylistApiView(ApiBaseView): class PlaylistApiView(ApiBaseView):
@@ -132,45 +220,54 @@ class PlaylistApiView(ApiBaseView):
return Response(response_serializer.data) return Response(response_serializer.data)
@extend_schema(
request=PlaylistSingleUpdate(),
responses={
200: OpenApiResponse(PlaylistSerializer()),
404: OpenApiResponse(
ErrorResponseSerializer(), description="playlist not found"
),
},
)
def post(self, request, playlist_id): def post(self, request, playlist_id):
"""post to custom playlist to add a video to list""" """update subscribed state of playlist"""
self.get_document(playlist_id) data_serializer = PlaylistSingleUpdate(data=request.data)
if not self.response["data"]: data_serializer.is_valid(raise_exception=True)
return Response({"error": "playlist not found"}, status=404) validated_data = data_serializer.validated_data
data = request.data self.get_document(playlist_id)
subscribed = data.get("playlist_subscribed") if not self.response:
if subscribed is not None: error = ErrorResponseSerializer({"error": "playlist not found"})
return Response(error.data, status=404)
subscribed = validated_data["playlist_subscribed"]
playlist_sub = PlaylistSubscription() playlist_sub = PlaylistSubscription()
json_data = playlist_sub.change_subscribe(playlist_id, subscribed) json_data = playlist_sub.change_subscribe(playlist_id, subscribed)
return Response(json_data, status=200)
if not self.response["data"]["playlist_type"] == "custom": response_serializer = PlaylistSerializer(json_data)
message = f"playlist with ID {playlist_id} is not custom" return Response(response_serializer.data)
return Response({"message": message}, status=400)
action = request.data.get("action")
if action not in self.valid_custom_actions:
message = f"invalid action: {action}"
return Response({"message": message}, status=400)
playlist = YoutubePlaylist(playlist_id)
video_id = request.data.get("video_id")
if action == "create":
playlist.add_video_to_playlist(video_id)
else:
hide = UserConfig(request.user.id).get_value("hide_watched")
playlist.move_video(video_id, action, hide_watched=hide)
return Response({"success": True}, status=status.HTTP_201_CREATED)
@extend_schema(
parameters=[PlaylistDeleteQuerySerializer],
responses={
204: OpenApiResponse(description="playlist deleted"),
},
)
def delete(self, request, playlist_id): def delete(self, request, playlist_id):
"""delete playlist""" """delete playlist"""
print(f"{playlist_id}: delete playlist") print(f"{playlist_id}: delete playlist")
delete_videos = request.GET.get("delete-videos", False)
query_serializer = PlaylistDeleteQuerySerializer(
data=request.query_params
)
query_serializer.is_valid(raise_exception=True)
validated_query = query_serializer.validated_data
delete_videos = validated_query.get("delete_videos", False)
if delete_videos: if delete_videos:
YoutubePlaylist(playlist_id).delete_videos_playlist() YoutubePlaylist(playlist_id).delete_videos_playlist()
else: else:
YoutubePlaylist(playlist_id).delete_metadata() YoutubePlaylist(playlist_id).delete_metadata()
return Response({"success": True}) return Response(status=204)