video: hourly 'Update Video Database' safety net (the second-trigger pattern)

dual triggers on one automation aren't a thing (one trigger per row, same as music), so
this adds a second automation sharing the handler — like Auto-Deep Scan TV/Movie already
do. 'Auto-Update Video Database (Hourly)' runs the same cheap incremental server-read on
an hourly schedule, so MANUAL library additions (which Plex auto-scans) appear within the
hour instead of waiting for the weekly deep scan; the existing after-scan one still gives
instant updates for SoulSync-initiated scans.

new action_type video_update_database_hourly (seeder keys on action_type) reusing
auto_video_update_database in incremental mode; registered, seeded (schedule 1h, 15m
initial delay), UI block, label/icon, sort order. tested.
This commit is contained in:
BoulderBadgeDad 2026-06-26 16:39:28 -07:00
parent 11c0c62a7e
commit 151bb7b542
6 changed files with 35 additions and 3 deletions

View file

@ -253,6 +253,8 @@ ACTIONS: list[dict] = [
{"value": "show", "label": "TV only"}],
"default": "all"}
]},
{"type": "video_update_database_hourly", "label": "Update Video Database (Hourly)", "icon": "database", "scope": "video",
"description": "The same incremental server-read on an hourly schedule, so manual library additions (which Plex auto-scans) appear within the hour instead of waiting for the weekly deep scan. Pair with a 1-hour Schedule trigger.", "available": True},
# Per-library deep-scan presets (the system 'Auto-Deep Scan TV/Movie Library' run
# these). Scope + deep mode are baked in by the registration wrapper, so no config
# fields — drag one in and it just deep-scans that library.

View file

@ -228,6 +228,13 @@ def register_all(deps: AutomationDeps) -> None:
'video_update_database',
lambda config: auto_video_update_database(config, deps),
)
# Same incremental update, but on an hourly SCHEDULE (not just after a SoulSync scan) —
# so manual library additions (which Plex auto-scans) show up within the hour instead of
# waiting for the weekly deep scan. A distinct action_type because the seeder keys on it.
engine.register_action_handler(
'video_update_database_hourly',
lambda config: auto_video_update_database({'mode': 'incremental', **config}, deps),
)
# Sonarr-style: wishlist every episode airing today (for followed shows).
engine.register_action_handler(
'video_add_airing_episodes',

View file

@ -170,6 +170,18 @@ SYSTEM_AUTOMATIONS = [
'action_config': {'mode': 'incremental'},
'owned_by': 'video',
},
# Safety net: re-read the server hourly too, so MANUAL library additions (which Plex
# auto-scans) appear within the hour instead of waiting for the weekly deep scan. Same
# cheap incremental read as the after-scan one.
{
'name': 'Auto-Update Video Database (Hourly)',
'trigger_type': 'schedule',
'trigger_config': {'interval': 1, 'unit': 'hours'},
'action_type': 'video_update_database_hourly',
'action_config': {'mode': 'incremental'},
'initial_delay': 900,
'owned_by': 'video',
},
# Sonarr-style: once a day at 1am (server-local), wishlist every episode airing
# today for the shows you follow (skipping ones already owned) so the day's episodes
# are queued overnight. A fixed wall-clock 'daily_time' (not a rolling 24h interval

View file

@ -120,6 +120,16 @@ def test_every_video_automation_has_a_friendly_label_and_icon():
assert not missing, "video actions missing a label/icon: " + ", ".join(sorted(missing))
def test_hourly_db_update_is_a_scheduled_safety_net():
# second trigger (a schedule) for the same incremental DB read, so manual library adds
# show up within the hour instead of waiting for the weekly deep scan.
import core.automation_engine as ae
row = next((a for a in ae.SYSTEM_AUTOMATIONS if a.get("action_type") == "video_update_database_hourly"), None)
assert row is not None
assert row["trigger_type"] == "schedule" and row["trigger_config"]["unit"] == "hours"
assert row["action_config"]["mode"] == "incremental" # cheap read, not a deep scan
def test_video_system_automations_are_sorted_by_pipeline_order():
# The API returns newest-created-first (jumbled); the page re-sorts by an
# explicit order so it reads scans → processors → library → maintenance.

View file

@ -1774,7 +1774,7 @@ const _autoIcons = {
full_cleanup: '\uD83E\uDDF9',
playlist_pipeline: '\uD83D\uDE80',
// Video side
video_scan_library: '\uD83C\uDFAC', video_scan_server: '\uD83D\uDD04', video_update_database: '\uD83D\uDDC4\uFE0F',
video_scan_library: '\uD83C\uDFAC', video_scan_server: '\uD83D\uDD04', video_update_database: '\uD83D\uDDC4\uFE0F', video_update_database_hourly: '\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',
@ -3375,7 +3375,8 @@ function _autoFormatAction(type) {
playlist_pipeline: 'Playlist Pipeline',
// Video side
video_scan_library: 'Scan Video Library', video_scan_server: 'Scan Video Server',
video_update_database: 'Update Video Database', video_add_airing_episodes: 'Wishlist Airing Episodes',
video_update_database: 'Update Video Database', video_update_database_hourly: 'Update Video Database (Hourly)',
video_add_airing_episodes: 'Wishlist Airing Episodes',
video_deep_scan_movies: 'Deep Scan Movie Library', video_deep_scan_tv: 'Deep Scan TV Library',
video_scan_watchlist_people: 'Scan Watchlist People',
video_scan_watchlist_channels: 'Scan Watchlist Channels',

View file

@ -36,7 +36,7 @@
// Stage 2 — processors that DRAIN it (download)
'video_process_movie_wishlist', 'video_process_episode_wishlist', 'video_process_youtube_wishlist',
// Library scan / sync
'video_scan_server', 'video_update_database', 'video_deep_scan_tv', 'video_deep_scan_movies',
'video_scan_server', 'video_update_database', 'video_update_database_hourly', 'video_deep_scan_tv', 'video_deep_scan_movies',
// Maintenance
'video_clean_search_history', 'video_clean_completed_downloads', 'video_full_cleanup', 'video_backup_database',
];