From 151bb7b54217ddaa727777901d501b8ef6c8648b Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 26 Jun 2026 16:39:28 -0700 Subject: [PATCH] video: hourly 'Update Video Database' safety net (the second-trigger pattern) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/automation/blocks.py | 2 ++ core/automation/handlers/registration.py | 7 +++++++ core/automation_engine.py | 12 ++++++++++++ tests/test_video_automations_builder.py | 10 ++++++++++ webui/static/stats-automations.js | 5 +++-- webui/static/video/video-automations.js | 2 +- 6 files changed, 35 insertions(+), 3 deletions(-) diff --git a/core/automation/blocks.py b/core/automation/blocks.py index 536003ac..600dd3a1 100644 --- a/core/automation/blocks.py +++ b/core/automation/blocks.py @@ -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. diff --git a/core/automation/handlers/registration.py b/core/automation/handlers/registration.py index e78893fc..cc4641ba 100644 --- a/core/automation/handlers/registration.py +++ b/core/automation/handlers/registration.py @@ -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', diff --git a/core/automation_engine.py b/core/automation_engine.py index 3f3fa1e4..30cfdf3e 100644 --- a/core/automation_engine.py +++ b/core/automation_engine.py @@ -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 diff --git a/tests/test_video_automations_builder.py b/tests/test_video_automations_builder.py index e059c26f..48554fc0 100644 --- a/tests/test_video_automations_builder.py +++ b/tests/test_video_automations_builder.py @@ -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. diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js index 63d39d95..94825836 100644 --- a/webui/static/stats-automations.js +++ b/webui/static/stats-automations.js @@ -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', diff --git a/webui/static/video/video-automations.js b/webui/static/video/video-automations.js index 91628c22..6f6b2ddb 100644 --- a/webui/static/video/video-automations.js +++ b/webui/static/video/video-automations.js @@ -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', ];