video automations: rename Download->Process + group into a clear pipeline

- 'Download X Wishlist' -> 'Process X Wishlist' everywhere (label + action_type):
  matches the music side's 'process_wishlist', the already-process youtube action,
  and reads right (movie/episode do search+pick+download, not just download).
  action_types video_download_movie/episode_wishlist -> video_process_*.
- group the new automations as a two-stage pipeline in the seed list + builder
  palette: Stage 1 SCANS (people/channels/playlists) fill the wishlist, Stage 2
  PROCESSORS (movie/episode/youtube) drain it. icons/labels/drift test updated.

brand-new this session so no migration needed. 337 automation tests green.
This commit is contained in:
BoulderBadgeDad 2026-06-25 22:26:31 -07:00
parent b404420c76
commit 7d7d242e5d
7 changed files with 56 additions and 58 deletions

View file

@ -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}

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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',
},
]

View file

@ -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',
})

View file

@ -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';
}