From 902281709601df2e59758051e85b088f16f97e61 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 11 Feb 2025 19:22:39 +0700 Subject: [PATCH] serialize playlists, add custom playlist endpoints --- backend/playlist/serializers.py | 40 +++++ backend/playlist/src/query_building.py | 3 +- backend/playlist/urls.py | 10 ++ backend/playlist/views.py | 227 ++++++++++++++++++------- 4 files changed, 213 insertions(+), 67 deletions(-) diff --git a/backend/playlist/serializers.py b/backend/playlist/serializers.py index b4ae1c73..15d6d027 100644 --- a/backend/playlist/serializers.py +++ b/backend/playlist/serializers.py @@ -49,3 +49,43 @@ class PlaylistListQuerySerializer(serializers.Serializer): type = serializers.ChoiceField( 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) diff --git a/backend/playlist/src/query_building.py b/backend/playlist/src/query_building.py index 4751144f..bd3e7c80 100644 --- a/backend/playlist/src/query_building.py +++ b/backend/playlist/src/query_building.py @@ -27,8 +27,7 @@ class QueryBuilder: subscribed = self.request_params.get("subscribed") if subscribed: - subed_bool = subscribed[0] == "true" - must_list.append({"match": {"playlist_subscribed": subed_bool}}) + must_list.append({"match": {"playlist_subscribed": subscribed}}) playlist_type = self.request_params.get("type") if playlist_type: diff --git a/backend/playlist/urls.py b/backend/playlist/urls.py index c144e2e0..93f3ca65 100644 --- a/backend/playlist/urls.py +++ b/backend/playlist/urls.py @@ -9,6 +9,16 @@ urlpatterns = [ views.PlaylistApiListView.as_view(), name="api-playlist-list", ), + path( + "custom/", + views.PlaylistCustomApiListView.as_view(), + name="api-custom-playlist-list", + ), + path( + "custom//", + views.PlaylistCustomApiView.as_view(), + name="api-custom-playlist", + ), path( "/", views.PlaylistApiView.as_view(), diff --git a/backend/playlist/views.py b/backend/playlist/views.py index 4718dfdf..6ec102ff 100644 --- a/backend/playlist/views.py +++ b/backend/playlist/views.py @@ -2,18 +2,25 @@ import uuid -from common.serializers import ErrorResponseSerializer +from common.serializers import ( + AsyncTaskResponseSerializer, + ErrorResponseSerializer, +) from common.views_base import AdminWriteOnly, ApiBaseView from download.src.subscriptions import PlaylistSubscription from drf_spectacular.utils import OpenApiResponse, extend_schema from playlist.serializers import ( + PlaylistBulkAddSerializer, + PlaylistCustomPostSerializer, + PlaylistDeleteQuerySerializer, + PlaylistListCustomPostSerializer, PlaylistListQuerySerializer, PlaylistListSerializer, PlaylistSerializer, + PlaylistSingleUpdate, ) from playlist.src.index import YoutubePlaylist from playlist.src.query_building import QueryBuilder -from rest_framework import status from rest_framework.response import Response from task.tasks import subscribe_to from user.src.user_config import UserConfig @@ -61,46 +68,127 @@ class PlaylistApiListView(ApiBaseView): return Response(response_serializer.data) + @extend_schema( + request=PlaylistBulkAddSerializer(), + responses={ + 200: OpenApiResponse(AsyncTaskResponseSerializer()), + 400: OpenApiResponse( + ErrorResponseSerializer(), description="Bad request" + ), + }, + ) def post(self, request): - """subscribe/unsubscribe to list of playlists""" - data = request.data - try: - to_add = data["data"] - except KeyError: - message = "missing expected data key" - print(message) - return Response({"message": message}, status=400) + """async subscribe to list of playlists""" + data_serializer = PlaylistBulkAddSerializer(data=request.data) + data_serializer.is_valid(raise_exception=True) + validated_data = data_serializer.validated_data - data = data["data"] - if isinstance(data, dict): - custom_name = data.get("create") - if custom_name: - playlist_id = f"TA_playlist_{uuid.uuid4()}" - custom_playlist = YoutubePlaylist(playlist_id) - custom_playlist.create(custom_name) - return Response(custom_playlist.json_data) + pending = [i["playlist_id"] for i in validated_data["data"]] + if not pending: + error = ErrorResponseSerializer({"error": "nothing to subscribe"}) + return Response(error.data, status=400) - pending = [] - 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) + url_str = " ".join(pending) + task = subscribe_to.delay(url_str, expected_type="playlist") - if pending: - url_str = " ".join(pending) - subscribe_to.delay(url_str, expected_type="playlist") + message = { + "message": "playlist subscribe task started", + "task_id": task.id, + } + serializer = AsyncTaskResponseSerializer(message) - return Response(data) + return Response(serializer.data) - @staticmethod - def _unsubscribe(playlist_id: str): - """unsubscribe""" - print(f"[{playlist_id}] unsubscribe from playlist") - _ = PlaylistSubscription().change_subscribe( - playlist_id, subscribe_status=False - ) + +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()}" + custom_playlist = YoutubePlaylist(playlist_id) + custom_playlist.create(custom_name) + + response_serializer = PlaylistSerializer(custom_playlist.json_data) + + return Response(response_serializer.data) + + +class PlaylistCustomApiView(ApiBaseView): + """resolves to /api/playlist/custom// + POST: modify custom playlist + """ + + 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): @@ -132,45 +220,54 @@ class PlaylistApiView(ApiBaseView): 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): - """post to custom playlist to add a video to list""" + """update subscribed state of playlist""" + data_serializer = PlaylistSingleUpdate(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["data"]: - return Response({"error": "playlist not found"}, status=404) + if not self.response: + error = ErrorResponseSerializer({"error": "playlist not found"}) + return Response(error.data, status=404) - data = request.data - subscribed = data.get("playlist_subscribed") - if subscribed is not None: - playlist_sub = PlaylistSubscription() - json_data = playlist_sub.change_subscribe(playlist_id, subscribed) - return Response(json_data, status=200) + subscribed = validated_data["playlist_subscribed"] + playlist_sub = PlaylistSubscription() + json_data = playlist_sub.change_subscribe(playlist_id, subscribed) - if not self.response["data"]["playlist_type"] == "custom": - message = f"playlist with ID {playlist_id} is not custom" - 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) + response_serializer = PlaylistSerializer(json_data) + return Response(response_serializer.data) + @extend_schema( + parameters=[PlaylistDeleteQuerySerializer], + responses={ + 204: OpenApiResponse(description="playlist deleted"), + }, + ) def delete(self, request, 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: YoutubePlaylist(playlist_id).delete_videos_playlist() else: YoutubePlaylist(playlist_id).delete_metadata() - return Response({"success": True}) + return Response(status=204)