From 90611dbe758d789a31c81cb6919a8b4437e2e92b Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 10 Jul 2025 16:39:08 +0700 Subject: [PATCH] add playlist sort order toggle, #171 --- backend/appsettings/index_mapping.json | 3 ++ .../config/management/commands/ta_startup.py | 35 +++++++++++++++++++ backend/playlist/serializers.py | 6 +++- backend/playlist/src/index.py | 26 ++++++++++++-- backend/playlist/views.py | 21 +++++++++-- .../api/actions/updatePlaylistSortOrder.ts | 10 ++++++ frontend/src/api/loader/loadPlaylistById.ts | 1 + frontend/src/pages/Playlist.tsx | 26 ++++++++++++++ 8 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 frontend/src/api/actions/updatePlaylistSortOrder.ts diff --git a/backend/appsettings/index_mapping.json b/backend/appsettings/index_mapping.json index b2e57cf8..72845492 100644 --- a/backend/appsettings/index_mapping.json +++ b/backend/appsettings/index_mapping.json @@ -506,6 +506,9 @@ "type": "date", "format": "epoch_second" }, + "playlist_sort_order": { + "type": "keyword" + }, "playlist_entries": { "properties": { "downloaded": { diff --git a/backend/config/management/commands/ta_startup.py b/backend/config/management/commands/ta_startup.py index 980354b1..4b75f05b 100644 --- a/backend/config/management/commands/ta_startup.py +++ b/backend/config/management/commands/ta_startup.py @@ -56,6 +56,7 @@ class Command(BaseCommand): self._mig_channel_tags() self._mig_video_channel_tags() self._mig_fix_download_channel_indexed() + self._mig_add_default_playlist_sort() def _make_folders(self): """make expected cache folders""" @@ -389,3 +390,37 @@ class Command(BaseCommand): self.stdout.write(response) sleep(60) raise CommandError(message) + + def _mig_add_default_playlist_sort(self) -> None: + """migrate from 0.5.4 to 0.5.5 set default playlist sortorder""" + self.stdout.write("[MIGRATION] set default playlist sort order") + path = "ta_playlist/_update_by_query" + data = { + "query": { + "bool": { + "must_not": [{"exists": {"field": "playlist_sort_order"}}] + } + }, + "script": { + "source": "ctx._source.playlist_sort_order = 'top'", + "lang": "painless", + }, + } + response, status_code = ElasticWrap(path).post(data) + if status_code in [200, 201]: + updated = response.get("updated") + if updated: + self.stdout.write( + self.style.SUCCESS(f" ✓ updated {updated} playlists") + ) + else: + self.stdout.write( + self.style.SUCCESS(" no playlists need updating") + ) + return + + message = " 🗙 failed to set default playlist sort order" + self.stdout.write(self.style.ERROR(message)) + self.stdout.write(response) + sleep(60) + raise CommandError(message) diff --git a/backend/playlist/serializers.py b/backend/playlist/serializers.py index 9362f5b6..741c770d 100644 --- a/backend/playlist/serializers.py +++ b/backend/playlist/serializers.py @@ -28,6 +28,7 @@ class PlaylistSerializer(serializers.Serializer): playlist_last_refresh = serializers.CharField() playlist_name = serializers.CharField() playlist_subscribed = serializers.BooleanField() + playlist_sort_order = serializers.ChoiceField(choices=["top", "bottom"]) playlist_thumbnail = serializers.CharField() playlist_type = serializers.ChoiceField(choices=["regular", "custom"]) _index = serializers.CharField(required=False) @@ -68,7 +69,10 @@ class PlaylistBulkAddSerializer(serializers.Serializer): class PlaylistSingleUpdate(serializers.Serializer): """update state of single playlist""" - playlist_subscribed = serializers.BooleanField() + playlist_subscribed = serializers.BooleanField(required=False) + playlist_sort_order = serializers.ChoiceField( + choices=["top", "bottom"], required=False + ) class PlaylistListCustomPostSerializer(serializers.Serializer): diff --git a/backend/playlist/src/index.py b/backend/playlist/src/index.py index 3533205a..5c892509 100644 --- a/backend/playlist/src/index.py +++ b/backend/playlist/src/index.py @@ -35,11 +35,17 @@ class YoutubePlaylist(YouTubeItem): self.get_from_es() if self.json_data: subscribed = self.json_data.get("playlist_subscribed") + playlist_sort_order = self.json_data.get("playlist_sort_order") else: subscribed = False + playlist_sort_order = "top" + + playlist_items = "::1" if playlist_sort_order == "top" else "::-1" if scrape or not self.json_data: - self.get_from_youtube() + self.get_from_youtube( + obs_overwrite={"playlist_items": playlist_items} + ) if not self.youtube_meta: self.json_data = False return @@ -48,8 +54,13 @@ class YoutubePlaylist(YouTubeItem): self._ensure_channel() ids_found = self.get_local_vids() self.get_entries(ids_found) - self.json_data["playlist_entries"] = self.all_members - self.json_data["playlist_subscribed"] = subscribed + self.json_data.update( + { + "playlist_entries": self.all_members, + "playlist_subscribed": subscribed, + "playlist_sort_order": playlist_sort_order, + } + ) def process_youtube_meta(self): """extract relevant fields from youtube""" @@ -190,6 +201,15 @@ class YoutubePlaylist(YouTubeItem): self.get_playlist_art() return True + def change_sort_order(self, new_sort_order): + """update sort order of playlist""" + playlist = YoutubePlaylist(self.youtube_id) + playlist.build_json() + playlist.json_data["playlist_sort_order"] = new_sort_order + playlist.upload_to_es() + + return playlist.json_data + def build_nav(self, youtube_id): """find next and previous in playlist of a given youtube_id""" cache_root = EnvironmentSettings().get_cache_root() diff --git a/backend/playlist/views.py b/backend/playlist/views.py index 6ec102ff..1ce9261a 100644 --- a/backend/playlist/views.py +++ b/backend/playlist/views.py @@ -240,9 +240,24 @@ class PlaylistApiView(ApiBaseView): error = ErrorResponseSerializer({"error": "playlist not found"}) return Response(error.data, status=404) - subscribed = validated_data["playlist_subscribed"] - playlist_sub = PlaylistSubscription() - json_data = playlist_sub.change_subscribe(playlist_id, subscribed) + subscribed = validated_data.get("playlist_subscribed") + sort_order = validated_data.get("playlist_sort_order") + + json_data = None + if subscribed is not None: + playlist_sub = PlaylistSubscription() + json_data = playlist_sub.change_subscribe(playlist_id, subscribed) + + if sort_order: + json_data = YoutubePlaylist(playlist_id).change_sort_order( + new_sort_order=sort_order + ) + + if not json_data: + error = ErrorResponseSerializer( + {"error": "expect playlist_subscribed or playlist_sort_order"} + ) + return Response(error.data, status=400) response_serializer = PlaylistSerializer(json_data) return Response(response_serializer.data) diff --git a/frontend/src/api/actions/updatePlaylistSortOrder.ts b/frontend/src/api/actions/updatePlaylistSortOrder.ts new file mode 100644 index 00000000..a7234da2 --- /dev/null +++ b/frontend/src/api/actions/updatePlaylistSortOrder.ts @@ -0,0 +1,10 @@ +import APIClient from '../../functions/APIClient'; + +const updatePlaylistSortOrder = async (playlistId: string, newSortOrder: 'top' | 'bottom') => { + return APIClient(`/api/playlist/${playlistId}/`, { + method: 'POST', + body: { playlist_sort_order: newSortOrder }, + }); +}; + +export default updatePlaylistSortOrder; diff --git a/frontend/src/api/loader/loadPlaylistById.ts b/frontend/src/api/loader/loadPlaylistById.ts index b800410f..5a339baa 100644 --- a/frontend/src/api/loader/loadPlaylistById.ts +++ b/frontend/src/api/loader/loadPlaylistById.ts @@ -14,6 +14,7 @@ export type PlaylistType = { playlist_channel_id: string; playlist_description: string; playlist_entries: PlaylistEntryType[]; + playlist_sort_order: 'top' | 'bottom'; playlist_id: string; playlist_last_refresh: string; playlist_name: string; diff --git a/frontend/src/pages/Playlist.tsx b/frontend/src/pages/Playlist.tsx index f8affa20..6d1227d4 100644 --- a/frontend/src/pages/Playlist.tsx +++ b/frontend/src/pages/Playlist.tsx @@ -31,6 +31,7 @@ import useIsAdmin from '../functions/useIsAdmin'; import { useUserConfigStore } from '../stores/UserConfigStore'; import { ApiResponseType } from '../functions/APIClient'; import NotFound from './NotFound'; +import updatePlaylistSortOrder from '../api/actions/updatePlaylistSortOrder'; export type VideoResponseType = { data?: VideoType[]; @@ -287,6 +288,31 @@ const Playlist = () => { /> )} +
+ Switch sort order: +
+ { + const newSortOrder = + playlist.playlist_sort_order === 'top' ? 'bottom' : 'top'; + await updatePlaylistSortOrder(playlist.playlist_id, newSortOrder); + setRefresh(true); + }} + /> + {playlist.playlist_sort_order === 'bottom' ? ( + + ) : ( + + )} +
+