setSearchInput(e.target.value)}
- autoFocus
- />
- )}
-
- {channelAggsList && channelAggsList.length > 1 && (
-
{isGridView && (
@@ -449,6 +385,85 @@ const Download = () => {
{showQueueActions && (
+
Search & Filter
+
+ {channelAggsList && channelAggsList.length > 1 && (
+
+ )}
+
+
setSearchInput(e.target.value)}
+ />
+ {searchInput &&
}
+
Bulk actions
Applied filtered by status '{showIgnoredFilter}'
From b91408ada2a366e2cee8140ba42af1dce503cab1 Mon Sep 17 00:00:00 2001
From: Simon
Date: Thu, 10 Jul 2025 15:02:40 +0700
Subject: [PATCH 37/50] remove unused
---
backend/video/src/index.py | 7 -------
1 file changed, 7 deletions(-)
diff --git a/backend/video/src/index.py b/backend/video/src/index.py
index 636bc4bb..34e23a8c 100644
--- a/backend/video/src/index.py
+++ b/backend/video/src/index.py
@@ -10,7 +10,6 @@ from datetime import datetime
import requests
from channel.src import index as ta_channel
from common.src.env_settings import EnvironmentSettings
-from common.src.es_connect import ElasticWrap
from common.src.helper import get_duration_sec, get_duration_str, randomizor
from common.src.index_generic import YouTubeItem
from django.conf import settings
@@ -395,12 +394,6 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
return subtitles
- def update_media_url(self):
- """update only media_url in es for reindex channel rename"""
- data = {"doc": {"media_url": self.json_data["media_url"]}}
- path = f"{self.index_name}/_update/{self.youtube_id}"
- _, _ = ElasticWrap(path).post(data=data)
-
def index_new_video(youtube_id, video_type=VideoTypeEnum.VIDEOS):
"""combined classes to create new video in index"""
From 59e9ee7eed8afacc356804497dd7b269e44fa844 Mon Sep 17 00:00:00 2001
From: Simon
Date: Thu, 10 Jul 2025 15:26:15 +0700
Subject: [PATCH 38/50] complete playlist mapping
---
backend/appsettings/index_mapping.json | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/backend/appsettings/index_mapping.json b/backend/appsettings/index_mapping.json
index d1bbd500..b2e57cf8 100644
--- a/backend/appsettings/index_mapping.json
+++ b/backend/appsettings/index_mapping.json
@@ -461,6 +461,15 @@
"playlist_description": {
"type": "text"
},
+ "playlist_subscribed": {
+ "type": "boolean"
+ },
+ "playlist_type": {
+ "type": "keyword"
+ },
+ "playlist_active": {
+ "type": "boolean"
+ },
"playlist_name": {
"type": "text",
"analyzer": "english",
From bbfd3f44236872309321a2be8de5689f493f4e29 Mon Sep 17 00:00:00 2001
From: Simon
Date: Thu, 10 Jul 2025 16:34:00 +0700
Subject: [PATCH 39/50] implement dynamic obs overwrite
---
backend/common/src/index_generic.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/backend/common/src/index_generic.py b/backend/common/src/index_generic.py
index 116d4965..354f83ea 100644
--- a/backend/common/src/index_generic.py
+++ b/backend/common/src/index_generic.py
@@ -34,14 +34,19 @@ class YouTubeItem:
"""build youtube url"""
return self.yt_base + self.youtube_id
- def get_from_youtube(self):
+ def get_from_youtube(self, obs_overwrite: dict | None = None):
"""use yt-dlp to get meta data from youtube"""
print(f"{self.youtube_id}: get metadata from youtube")
obs_request = self.yt_obs.copy()
if self.config["downloads"]["extractor_lang"]:
langs = self.config["downloads"]["extractor_lang"]
langs_list = [i.strip() for i in langs.split(",")]
- obs_request["extractor_args"] = {"youtube": {"lang": langs_list}}
+ obs_request["extractor_args"] = {
+ "youtube": {"lang": langs_list}
+ } # type: ignore
+
+ if obs_overwrite:
+ obs_request.update(obs_overwrite)
url = self.build_yt_url()
self.youtube_meta, self.error = YtWrap(
From 90611dbe758d789a31c81cb6919a8b4437e2e92b Mon Sep 17 00:00:00 2001
From: Simon
Date: Thu, 10 Jul 2025 16:39:08 +0700
Subject: [PATCH 40/50] 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 = () => {
/>