diff --git a/core/automation/blocks.py b/core/automation/blocks.py index abcce3ff..161e4652 100644 --- a/core/automation/blocks.py +++ b/core/automation/blocks.py @@ -265,6 +265,8 @@ ACTIONS: list[dict] = [ "config_fields": [ {"key": "prune_ended", "type": "checkbox", "label": "Also remove ended/canceled shows from the watchlist", "default": True} ]}, + # ── Watchlist → Wishlist pipeline ──────────────────────────────────────── + # Stage 1 — scans that FILL the wishlist from what you follow. {"type": "video_scan_watchlist_people", "label": "Scan Watchlist People", "icon": "users", "scope": "video", "description": "For everyone you follow on the watchlist, wishlist every movie they acted in or directed that you don't already own — the whole back catalog plus anything upcoming (kept as 'monitored' until it's released). First run backlogs everything; later runs are fast.", "available": True}, {"type": "video_scan_watchlist_channels", "label": "Scan Watchlist Channels", "icon": "youtube", "scope": "video", @@ -274,17 +276,18 @@ ACTIONS: list[dict] = [ ]}, {"type": "video_scan_watchlist_playlists", "label": "Scan Watchlist Playlists", "icon": "list", "scope": "video", "description": "For every YouTube playlist you follow, wishlist its videos you don't have yet (and anything later added to it) — mirrors the whole list. Each playlist becomes its own show in the library (playlist-as-show). Shorts excluded. Pair with a schedule.", "available": True}, - {"type": "video_download_movie_wishlist", "label": "Download Movie Wishlist", "icon": "download", "scope": "video", + # Stage 2 — processors that DRAIN the wishlist by downloading. + {"type": "video_process_movie_wishlist", "label": "Process Movie Wishlist", "icon": "download", "scope": "video", "description": "Auto-grab your wished, released MOVIES from Soulseek: searches each, picks the best release per your quality profile, and downloads it (the monitor finishes + organises it). Skips unreleased ('monitored') ones and any already downloading. Needs the Movie library folder + slskd set. Pair with an hourly schedule.", "available": True, "config_fields": [ {"key": "max_concurrent", "type": "number", "label": "Max simultaneous searches", "default": 3, "min": 1} ]}, - {"type": "video_download_episode_wishlist", "label": "Download Episode Wishlist", "icon": "download", "scope": "video", + {"type": "video_process_episode_wishlist", "label": "Process Episode Wishlist", "icon": "download", "scope": "video", "description": "Auto-grab your wished EPISODES from Soulseek: searches each, picks the best release per your quality profile, and downloads it. Fed by the 'Wishlist Today's Airings' scan. Needs the TV library folder + slskd set. Pair with an hourly schedule.", "available": True, "config_fields": [ {"key": "max_concurrent", "type": "number", "label": "Max simultaneous searches", "default": 3, "min": 1} ]}, - {"type": "video_process_youtube_wishlist", "label": "Download YouTube Wishlist", "icon": "download", "scope": "video", + {"type": "video_process_youtube_wishlist", "label": "Process YouTube Wishlist", "icon": "download", "scope": "video", "description": "Download wished YouTube videos (yt-dlp), organised as a Plex 'TV by date' show (channel/year/date). Queues the WHOLE wishlist — the setting only limits how many download at the same time; each finished one starts the next, so it all drains. A completed download leaves the wishlist. Needs the YouTube library folder set on Settings → Downloads.", "available": True, "config_fields": [ {"key": "max_concurrent", "type": "number", "label": "Max simultaneous downloads", "default": 3, "min": 1} diff --git a/core/automation/handlers/registration.py b/core/automation/handlers/registration.py index 64b9011b..178c6011 100644 --- a/core/automation/handlers/registration.py +++ b/core/automation/handlers/registration.py @@ -232,41 +232,40 @@ def register_all(deps: AutomationDeps) -> None: 'video_add_airing_episodes', lambda config: auto_video_add_airing_episodes(config, deps), ) - # Watchlist-people scan: wishlist every un-owned movie the followed people acted in or - # directed (back catalog + upcoming). + # ── Watchlist → Wishlist pipeline ───────────────────────────────────────── + # Stage 1 — SCANS that fill the wishlist from what you follow. + # People: wishlist every un-owned movie followed actors/directors made (catalog + upcoming). engine.register_action_handler( 'video_scan_watchlist_people', lambda config: auto_video_scan_watchlist_people(config, deps), ) - # Watchlist-channels scan: wishlist new long-form uploads from followed YouTube - # channels (forward-looking + last-N safety net). + # Channels: new long-form uploads from followed YouTube channels (forward + last-N net). engine.register_action_handler( 'video_scan_watchlist_channels', lambda config: auto_video_scan_watchlist_channels(config, deps), ) - # Watchlist-playlists scan: mirror followed YouTube playlists into the wishlist - # (whole list + new additions; playlist-as-show). + # Playlists: mirror followed YouTube playlists (whole list + new additions; playlist-as-show). engine.register_action_handler( 'video_scan_watchlist_playlists', lambda config: auto_video_scan_watchlist_playlists(config, deps), ) - # Drain side: enqueue wished YouTube videos into the download queue (yt-dlp worker). + # Stage 2 — PROCESSORS that drain the wishlist by downloading. Movie/episode go through + # slskd (search → pick best → grab); the guard skips an hourly tick while a drain is still + # working. YouTube goes through yt-dlp (queue all, a few concurrent). engine.register_action_handler( - 'video_process_youtube_wishlist', - lambda config: auto_video_process_youtube_wishlist(config, deps), - ) - # Soulseek drain: auto-grab wished movies / episodes (search → pick best → download). - # The guard skips an hourly tick while a drain is still working (searches are slow). - engine.register_action_handler( - 'video_download_movie_wishlist', + 'video_process_movie_wishlist', lambda config: auto_video_process_wishlist(config, deps, media_type='movie'), lambda: is_running('movie'), ) engine.register_action_handler( - 'video_download_episode_wishlist', + 'video_process_episode_wishlist', lambda config: auto_video_process_wishlist(config, deps, media_type='episode'), lambda: is_running('episode'), ) + engine.register_action_handler( + 'video_process_youtube_wishlist', + lambda config: auto_video_process_youtube_wishlist(config, deps), + ) # Progress + history callbacks: the engine invokes these around # each handler run. Lift the closures from diff --git a/core/automation/handlers/video_process_wishlist.py b/core/automation/handlers/video_process_wishlist.py index e89c71c4..bbe1f6dc 100644 --- a/core/automation/handlers/video_process_wishlist.py +++ b/core/automation/handlers/video_process_wishlist.py @@ -1,4 +1,4 @@ -"""Automation handlers: ``video_download_movie_wishlist`` / ``video_download_episode_wishlist``. +"""Automation handlers: ``video_process_movie_wishlist`` / ``video_process_episode_wishlist``. The Soulseek counterpart of the YouTube wishlist drain — the piece that finally makes the people/airing scans pay off. For wished, RELEASED movies (and aired episodes) it searches diff --git a/core/automation/handlers/video_scan_watchlist_playlists.py b/core/automation/handlers/video_scan_watchlist_playlists.py index d06627e7..c6cbbbac 100644 --- a/core/automation/handlers/video_scan_watchlist_playlists.py +++ b/core/automation/handlers/video_scan_watchlist_playlists.py @@ -11,7 +11,7 @@ Organisation: the playlist becomes its own "show" (playlist-as-show) — its vid wishlisted under the playlist's title, so the download worker files them as ``Playlist Name / Season YEAR / Playlist Name - DATE - Title`` (matches the ytdl-sub ``tv_show_name``-on-a-playlist convention). All other plumbing is shared with channels: the -same wishlist rows, the same "Download YouTube Wishlist" drain, quality + org template. +same wishlist rows, the same "Process YouTube Wishlist" drain, quality + org template. Edge: a video in both a followed channel AND a followed playlist is wishlisted once (dedup by video id); whichever scan touched it last sets its show-name. Accepted. diff --git a/core/automation_engine.py b/core/automation_engine.py index 49b965c5..a8a7b9c9 100644 --- a/core/automation_engine.py +++ b/core/automation_engine.py @@ -239,67 +239,62 @@ SYSTEM_AUTOMATIONS = [ 'action_config': {'mode': 'deep', 'media_type': 'movie'}, 'owned_by': 'video', }, - # Watchlist scans — keep the wishlist fed from the people + channels you follow. - # People: daily (filmographies change slowly) at 03:00, after the airing/deep-scan - # jobs so they don't overlap. No-ops cleanly if you follow nobody. + # ── Watchlist → Wishlist pipeline ───────────────────────────────────────── + # Stage 1: SCANS that FILL the wishlist from what you follow. (The airing-episodes + # scan above is the show equivalent.) All no-op cleanly if you follow nobody. { - 'name': 'Auto-Scan Watchlist People', - 'trigger_type': 'daily_time', - 'trigger_config': {'time': '03:00'}, + 'name': 'Auto-Scan Watchlist People', # followed actors/directors → wished movies + 'trigger_type': 'daily_time', # daily; filmographies change slowly + 'trigger_config': {'time': '03:00'}, # after airing/deep-scan jobs, no overlap 'action_type': 'video_scan_watchlist_people', 'owned_by': 'video', }, - # Channels: every 6h (YouTube posts at all hours). No-ops cleanly if you follow none. { - 'name': 'Auto-Scan Watchlist Channels', + 'name': 'Auto-Scan Watchlist Channels', # followed YouTube channels → wished videos 'trigger_type': 'schedule', - 'trigger_config': {'interval': 6, 'unit': 'hours'}, + 'trigger_config': {'interval': 6, 'unit': 'hours'}, # YouTube posts at all hours 'action_type': 'video_scan_watchlist_channels', 'action_config': {'backfill_count': 10}, 'initial_delay': 1200, 'owned_by': 'video', }, - # Playlists: every 6h, mirror the whole list (playlist-as-show). No-ops if you follow none. { - 'name': 'Auto-Scan Watchlist Playlists', + 'name': 'Auto-Scan Watchlist Playlists', # followed playlists (mirror-all) → wished videos 'trigger_type': 'schedule', 'trigger_config': {'interval': 6, 'unit': 'hours'}, 'action_type': 'video_scan_watchlist_playlists', 'initial_delay': 1320, 'owned_by': 'video', }, - # Drain side: download wished YouTube videos hourly (queues the whole wishlist, runs a - # few at a time). Skips quietly until a YouTube library folder is set. + # Stage 2: PROCESSORS that DRAIN the wishlist by downloading. Hourly; each skips + # quietly until its library folder (+ slskd, for movie/episode) is configured. { - 'name': 'Auto-Download YouTube Wishlist', + 'name': 'Auto-Process Movie Wishlist', # wished movies → slskd search/pick/grab 'trigger_type': 'schedule', 'trigger_config': {'interval': 1, 'unit': 'hours'}, - 'action_type': 'video_process_youtube_wishlist', - 'action_config': {'max_concurrent': 3}, - 'initial_delay': 1500, - 'owned_by': 'video', - }, - # Soulseek drains: auto-grab wished movies / episodes hourly (search → pick best → - # download). A guard skips the tick while a drain is still working. Skip quietly until - # the library folder + slskd are set. - { - 'name': 'Auto-Download Movie Wishlist', - 'trigger_type': 'schedule', - 'trigger_config': {'interval': 1, 'unit': 'hours'}, - 'action_type': 'video_download_movie_wishlist', + 'action_type': 'video_process_movie_wishlist', 'action_config': {'max_concurrent': 3}, 'initial_delay': 1620, 'owned_by': 'video', }, { - 'name': 'Auto-Download Episode Wishlist', + 'name': 'Auto-Process Episode Wishlist', # wished episodes → slskd search/pick/grab 'trigger_type': 'schedule', 'trigger_config': {'interval': 1, 'unit': 'hours'}, - 'action_type': 'video_download_episode_wishlist', + 'action_type': 'video_process_episode_wishlist', 'action_config': {'max_concurrent': 3}, 'initial_delay': 1740, 'owned_by': 'video', }, + { + 'name': 'Auto-Process YouTube Wishlist', # wished YouTube videos → yt-dlp download + 'trigger_type': 'schedule', + 'trigger_config': {'interval': 1, 'unit': 'hours'}, + 'action_type': 'video_process_youtube_wishlist', + 'action_config': {'max_concurrent': 3}, + 'initial_delay': 1500, + 'owned_by': 'video', + }, ] diff --git a/tests/automation/test_handler_registration.py b/tests/automation/test_handler_registration.py index 67216c81..4c729389 100644 --- a/tests/automation/test_handler_registration.py +++ b/tests/automation/test_handler_registration.py @@ -63,9 +63,9 @@ EXPECTED_ACTION_NAMES = frozenset({ 'video_scan_watchlist_people', 'video_scan_watchlist_channels', 'video_scan_watchlist_playlists', + 'video_process_movie_wishlist', + 'video_process_episode_wishlist', 'video_process_youtube_wishlist', - 'video_download_movie_wishlist', - 'video_download_episode_wishlist', 'video_clean_search_history', 'video_clean_completed_downloads', 'video_full_cleanup', @@ -83,8 +83,8 @@ EXPECTED_GUARDED_ACTIONS = frozenset({ 'deep_scan_library', 'run_duplicate_cleaner', 'start_quality_scan', - 'video_download_movie_wishlist', - 'video_download_episode_wishlist', + 'video_process_movie_wishlist', + 'video_process_episode_wishlist', }) diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js index f6a92a49..cbf5e3c9 100644 --- a/webui/static/stats-automations.js +++ b/webui/static/stats-automations.js @@ -1777,8 +1777,9 @@ const _autoIcons = { video_scan_library: '\uD83C\uDFAC', video_scan_server: '\uD83D\uDD04', video_update_database: '\uD83D\uDDC4\uFE0F', video_add_airing_episodes: '\uD83D\uDCFA', video_deep_scan_movies: '\uD83C\uDFAC', video_deep_scan_tv: '\uD83D\uDCFA', video_scan_watchlist_people: '\uD83C\uDFAD', video_scan_watchlist_channels: '\uD83D\uDCE1', - video_scan_watchlist_playlists: '\uD83C\uDFB5', video_process_youtube_wishlist: '\u2B07\uFE0F', - video_download_movie_wishlist: '\uD83C\uDFAC', video_download_episode_wishlist: '\uD83D\uDCFA', + video_scan_watchlist_playlists: '\uD83C\uDFB5', + video_process_movie_wishlist: '\uD83C\uDFAC', video_process_episode_wishlist: '\uD83D\uDCFA', + video_process_youtube_wishlist: '\u2B07\uFE0F', }; // --- Inspiration Templates --- @@ -3376,9 +3377,9 @@ function _autoFormatAction(type) { video_scan_watchlist_people: 'Scan Watchlist People', video_scan_watchlist_channels: 'Scan Watchlist Channels', video_scan_watchlist_playlists: 'Scan Watchlist Playlists', - video_process_youtube_wishlist: 'Download YouTube Wishlist', - video_download_movie_wishlist: 'Download Movie Wishlist', - video_download_episode_wishlist: 'Download Episode Wishlist', + video_process_movie_wishlist: 'Process Movie Wishlist', + video_process_episode_wishlist: 'Process Episode Wishlist', + video_process_youtube_wishlist: 'Process YouTube Wishlist', }; return labels[type] || type || 'Unknown'; }