Video deep scan: pure read/reconcile, not a Plex disk-scan trigger

A deep scan is the equivalent of music's full refresh — it READS the server's
current state into video.db and prunes what's gone. It should NOT tell Plex to
rescan its disk. The deep-scan action types were wired to auto_video_scan_library
(nudge Plex + read); point them at the read-only auto_video_update_database in
'deep' mode instead. Update-db phase wording no longer says "new" for a full re-read;
deep-scan block descriptions clarify it's a read, not a disk-scan. Registration test
asserts the deep scans route to the read-only handler and never nudge the server.
This commit is contained in:
BoulderBadgeDad 2026-06-21 13:27:55 -07:00
parent 14a32f6006
commit 5a53ffc8c2
4 changed files with 33 additions and 8 deletions

View file

@ -254,9 +254,9 @@ ACTIONS: list[dict] = [
# 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.
{"type": "video_deep_scan_tv", "label": "Deep Scan TV Library", "icon": "search", "scope": "video",
"description": "Deep-rescan the TV library: re-read every show and prune ones the server no longer has (never touches movies)", "available": True},
"description": "Full reconcile of the TV library: re-read every show from the server and drop ones it no longer has (a read, NOT a Plex disk-scan; never touches movies)", "available": True},
{"type": "video_deep_scan_movies", "label": "Deep Scan Movie Library", "icon": "search", "scope": "video",
"description": "Deep-rescan the Movie library: re-read every movie and prune ones the server no longer has (never touches TV)", "available": True},
"description": "Full reconcile of the Movie library: re-read every movie from the server and drop ones it no longer has (a read, NOT a Plex disk-scan; never touches TV)", "available": True},
{"type": "video_add_airing_episodes", "label": "Wishlist Today's Airings", "icon": "calendar", "scope": "video",
"description": "Sonarr-style: add every episode airing today (for shows you follow) to the wishlist, skipping ones you already own", "available": True},
]

View file

@ -179,16 +179,19 @@ def register_all(deps: AutomationDeps) -> None:
lambda config: auto_video_scan_library(config, deps),
)
# Per-library deep scans (the video twin of music's 'Auto-Deep Scan Library',
# split because Movies and TV are independent libraries). Distinct action types
# so the system seeder — which keys on action_type — treats them as two separate
# automations; both reuse the one handler, scoped via media_type in action_config.
# split because Movies and TV are independent libraries). A deep scan READS the
# server's current state into video.db + prunes what's gone (a full reconcile) —
# it does NOT tell Plex to rescan its disk, so it runs through the read-only
# update-database handler in 'deep' mode, not the nudge+read scan-library one.
# Distinct action types so the seeder (which keys on action_type) sees two
# separate automations; both reuse the one handler, scoped via media_type.
engine.register_action_handler(
'video_deep_scan_movies',
lambda config: auto_video_scan_library({**config, 'media_type': 'movie', 'mode': config.get('mode') or 'deep'}, deps),
lambda config: auto_video_update_database({**config, 'media_type': 'movie', 'mode': config.get('mode') or 'deep'}, deps),
)
engine.register_action_handler(
'video_deep_scan_tv',
lambda config: auto_video_scan_library({**config, 'media_type': 'show', 'mode': config.get('mode') or 'deep'}, deps),
lambda config: auto_video_update_database({**config, 'media_type': 'show', 'mode': config.get('mode') or 'deep'}, deps),
)
# Post-download chain: scan the server, then (on the scan-done event) update the DB.
engine.register_action_handler(

View file

@ -239,8 +239,11 @@ def auto_video_update_database(
or (config.get('_event_data') or {}).get('media_type')
or 'all')
lib_label = {'movie': 'Movie', 'show': 'TV'}.get(media_type, 'video')
# 'deep'/'full' re-read the whole library; 'incremental' only grabs new items.
phase = (f'Re-reading the {lib_label} library from the server…' if mode in ('deep', 'full')
else f'Reading new {lib_label} media into SoulSync…')
try:
deps.update_progress(automation_id, phase=f'Reading new {lib_label} media into SoulSync…', progress=40)
deps.update_progress(automation_id, phase=phase, progress=40)
result = run_video_scan(mode, media_type) or {}
if result.get('state') == 'in_progress':
deps.update_progress(automation_id, status='finished', phase='Skipped',

View file

@ -181,6 +181,25 @@ class TestActionHandlerRegistration:
assert not missing, f"register_all dropped: {missing}"
assert not extra, f"register_all added unexpected: {extra}"
def test_deep_scans_route_to_readonly_update_in_deep_mode(self, monkeypatch):
"""A deep scan is a full READ + reconcile (music's full-refresh equivalent) —
it must NOT nudge Plex to rescan its disk. So the deep-scan action types route
to the read-only update-database handler in 'deep' mode, scoped per library,
never to the nudge+read scan-library handler."""
import core.automation.handlers.registration as reg
seen = []
monkeypatch.setattr(reg, 'auto_video_update_database',
lambda config, deps: seen.append(config) or {'status': 'completed'})
monkeypatch.setattr(reg, 'auto_video_scan_library',
lambda *a, **k: pytest.fail('deep scan must not nudge the server'))
engine = _RecordingEngine()
register_all(_build_deps(engine))
engine.action_handlers['video_deep_scan_tv']['handler']({'_automation_id': 'x'})
engine.action_handlers['video_deep_scan_movies']['handler']({})
assert seen[0]['media_type'] == 'show' and seen[0]['mode'] == 'deep'
assert seen[1]['media_type'] == 'movie' and seen[1]['mode'] == 'deep'
def test_guarded_actions_have_a_guard(self):
engine = _RecordingEngine()
register_all(_build_deps(engine))