diff --git a/core/automation/blocks.py b/core/automation/blocks.py index 7547bd68..474f375e 100644 --- a/core/automation/blocks.py +++ b/core/automation/blocks.py @@ -272,6 +272,8 @@ ACTIONS: list[dict] = [ # them on the video builder only; the music blocks above are untouched. {"type": "video_clean_search_history", "label": "Clean Search History", "icon": "trash-2", "scope": "video", "description": "Remove old searches from Soulseek", "available": True}, + {"type": "video_clean_completed_downloads", "label": "Clean Completed Downloads", "icon": "check-square", "scope": "video", + "description": "Clear completed downloads and empty directories", "available": True}, ] diff --git a/core/automation/handlers/registration.py b/core/automation/handlers/registration.py index 8698daa4..f9fa9c1d 100644 --- a/core/automation/handlers/registration.py +++ b/core/automation/handlers/registration.py @@ -160,6 +160,10 @@ def register_all(deps: AutomationDeps) -> None: 'video_clean_search_history', lambda config: auto_clean_search_history(config, deps), ) + engine.register_action_handler( + 'video_clean_completed_downloads', + lambda config: auto_clean_completed_downloads(config, deps), + ) engine.register_action_handler( 'clean_completed_downloads', lambda config: auto_clean_completed_downloads(config, deps), diff --git a/core/automation_engine.py b/core/automation_engine.py index 35a3a551..7f01bae4 100644 --- a/core/automation_engine.py +++ b/core/automation_engine.py @@ -193,6 +193,14 @@ SYSTEM_AUTOMATIONS = [ 'initial_delay': 600, 'owned_by': 'video', }, + { + 'name': 'Clean Completed Downloads', + 'trigger_type': 'schedule', + 'trigger_config': {'interval': 5, 'unit': 'minutes'}, + 'action_type': 'video_clean_completed_downloads', + 'initial_delay': 300, + 'owned_by': 'video', + }, # Video twin of music's 'Auto-Deep Scan Library', split into TWO because Movies # and TV are independent libraries — a TV scan never pulls in new movies and # vice-versa. Fixed weekly deep scan (re-read + prune removed) at 02:00 server- diff --git a/tests/automation/test_handler_registration.py b/tests/automation/test_handler_registration.py index bb996ffd..9a220239 100644 --- a/tests/automation/test_handler_registration.py +++ b/tests/automation/test_handler_registration.py @@ -61,6 +61,7 @@ EXPECTED_ACTION_NAMES = frozenset({ 'video_update_database', 'video_add_airing_episodes', 'video_clean_search_history', + 'video_clean_completed_downloads', }) # Action names that MUST register a guard (duplicate-run prevention). diff --git a/tests/automation/test_video_maintenance_twins.py b/tests/automation/test_video_maintenance_twins.py index 80e9de89..b2af55b3 100644 --- a/tests/automation/test_video_maintenance_twins.py +++ b/tests/automation/test_video_maintenance_twins.py @@ -48,8 +48,7 @@ def test_video_clean_search_history_seeds_one_video_owned_system_row(): assert row["trigger_config"] == {"interval": 1, "unit": "hours"} -def test_video_clean_search_history_reuses_the_music_handler(): - # Registered to the SAME handler function as the music action (identical behaviour). +def _registered_handlers(): from core.automation.handlers import register_all class _Eng: @@ -60,10 +59,36 @@ def test_video_clean_search_history_reuses_the_music_handler(): def register_progress_callbacks(self, *a, **k): pass - import core.automation.handlers.registration as reg - eng = _Eng() - # Build minimal deps via the test helper the registration suite uses. from tests.automation.test_handler_registration import _build_deps + eng = _Eng() register_all(_build_deps(eng)) - assert "video_clean_search_history" in eng.handlers - assert "clean_search_history" in eng.handlers + return eng.handlers + + +def test_video_clean_search_history_reuses_the_music_handler(): + # Registered to the SAME handler function as the music action (identical behaviour). + handlers = _registered_handlers() + assert "video_clean_search_history" in handlers + assert "clean_search_history" in handlers + + +# ── Phase 3: Clean Completed Downloads ────────────────────────────────────── + +def test_video_clean_completed_downloads_is_video_scoped_only(): + assert "video_clean_completed_downloads" in _action_types("video") + assert "video_clean_completed_downloads" not in _action_types("music") + # music original untouched + assert "clean_completed_downloads" in _action_types("music") + assert "clean_completed_downloads" not in _action_types("video") + + +def test_video_clean_completed_downloads_seeds_one_video_owned_system_row(): + rows = _system_by_action("video_clean_completed_downloads") + assert len(rows) == 1 and rows[0]["owned_by"] == "video" + assert rows[0]["trigger_config"] == {"interval": 5, "unit": "minutes"} + + +def test_video_clean_completed_downloads_reuses_the_music_handler(): + handlers = _registered_handlers() + assert "video_clean_completed_downloads" in handlers + assert "clean_completed_downloads" in handlers