diff --git a/core/automation/blocks.py b/core/automation/blocks.py index 3a7ac4d4..e3e410fc 100644 --- a/core/automation/blocks.py +++ b/core/automation/blocks.py @@ -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}, ] diff --git a/core/automation/handlers/registration.py b/core/automation/handlers/registration.py index 133cd447..6d333c98 100644 --- a/core/automation/handlers/registration.py +++ b/core/automation/handlers/registration.py @@ -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( diff --git a/core/automation/handlers/video_scan_library.py b/core/automation/handlers/video_scan_library.py index aeacb636..47c96d59 100644 --- a/core/automation/handlers/video_scan_library.py +++ b/core/automation/handlers/video_scan_library.py @@ -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', diff --git a/tests/automation/test_handler_registration.py b/tests/automation/test_handler_registration.py index 1cbf4253..273a73be 100644 --- a/tests/automation/test_handler_registration.py +++ b/tests/automation/test_handler_registration.py @@ -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))