From a8319156cee296c5b577ca2058c849bb0ba96b0b Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:31:36 -0700 Subject: [PATCH] Lift /api/automations/blocks static config into core/automation/blocks.py The endpoint was returning a 200-line literal dict inline. Moved the three lists (TRIGGERS, ACTIONS, NOTIFICATIONS) to module-level constants in core/automation/blocks.py. Route shrinks to 7 lines. Data is now importable for tests + future docs. Added 8 shape tests so a typo in the dict (missing 'type', wrong field type, missing options on a select, etc.) gets caught by CI instead of breaking the builder UI silently. The `known_signals` field stays computed at request time via _collect_known_signals(database) since it's dynamic. No behavior change. Same response shape. 869 tests passing (was 861). Ruff clean. --- core/automation/blocks.py | 215 +++++++++++++++++++++ tests/automation/test_automation_blocks.py | 82 ++++++++ web_server.py | 201 +------------------ 3 files changed, 302 insertions(+), 196 deletions(-) create mode 100644 core/automation/blocks.py create mode 100644 tests/automation/test_automation_blocks.py diff --git a/core/automation/blocks.py b/core/automation/blocks.py new file mode 100644 index 00000000..c84d1b37 --- /dev/null +++ b/core/automation/blocks.py @@ -0,0 +1,215 @@ +"""Static block definitions for the automation builder UI. + +Returned verbatim by `/api/automations/blocks` (with `known_signals` +injected by the route from `signals.collect_known_signals`). + +Three top-level lists: +- `TRIGGERS` — WHEN blocks: schedule, daily/weekly time, app started, + event triggers (track_downloaded, batch_complete, etc.), signal_received, + webhook_received. +- `ACTIONS` — DO blocks: process_wishlist, scan_library, etc. +- `NOTIFICATIONS` — THEN blocks: discord/pushbullet/telegram/webhook, + plus fire_signal and run_script then-actions. +""" + +from __future__ import annotations + +TRIGGERS: list[dict] = [ + {"type": "schedule", "label": "Schedule", "icon": "clock", "description": "Run on a timer interval", "available": True, + "config_fields": [ + {"key": "interval", "type": "number", "label": "Every", "default": 6, "min": 1}, + {"key": "unit", "type": "select", "label": "Unit", + "options": [{"value": "minutes", "label": "Minutes"}, {"value": "hours", "label": "Hours"}, {"value": "days", "label": "Days"}], + "default": "hours"} + ]}, + {"type": "daily_time", "label": "Daily Time", "icon": "clock", "description": "Run every day at a specific time", "available": True, + "config_fields": [ + {"key": "time", "type": "time", "label": "At", "default": "03:00"} + ]}, + {"type": "weekly_time", "label": "Weekly Schedule", "icon": "calendar", "description": "Run on specific days of the week at a set time", "available": True, + "config_fields": [ + {"key": "time", "type": "time", "label": "At", "default": "03:00"}, + {"key": "days", "type": "multi_select", "label": "Days", + "options": [{"value": "mon", "label": "Mon"}, {"value": "tue", "label": "Tue"}, {"value": "wed", "label": "Wed"}, + {"value": "thu", "label": "Thu"}, {"value": "fri", "label": "Fri"}, {"value": "sat", "label": "Sat"}, {"value": "sun", "label": "Sun"}]} + ]}, + {"type": "app_started", "label": "App Started", "icon": "power", "description": "When SoulSync starts up", "available": True}, + {"type": "track_downloaded", "label": "Track Downloaded", "icon": "download", "description": "When a track finishes downloading", "available": True, + "has_conditions": True, + "condition_fields": ["artist", "title", "album", "quality"], + "variables": ["artist", "title", "album", "quality"]}, + {"type": "batch_complete", "label": "Batch Complete", "icon": "check-circle", "description": "When an album/playlist download finishes", "available": True, + "has_conditions": True, + "condition_fields": ["playlist_name"], + "variables": ["playlist_name", "total_tracks", "completed_tracks", "failed_tracks"]}, + {"type": "watchlist_new_release", "label": "New Release Found", "icon": "bell", "description": "When watchlist detects new music", "available": True, + "has_conditions": True, + "condition_fields": ["artist"], + "variables": ["artist", "new_tracks", "added_to_wishlist"]}, + {"type": "playlist_synced", "label": "Playlist Synced", "icon": "refresh", "description": "When a playlist sync completes", "available": True, + "has_conditions": True, + "condition_fields": ["playlist_name"], + "variables": ["playlist_name", "total_tracks", "matched_tracks", "synced_tracks", "failed_tracks"]}, + {"type": "playlist_changed", "label": "Playlist Changed", "icon": "edit", "description": "When a mirrored playlist detects track changes from source", "available": True, + "has_conditions": True, + "condition_fields": ["playlist_name"], + "variables": ["playlist_name", "old_count", "new_count", "added", "removed"]}, + {"type": "discovery_completed", "label": "Discovery Complete", "icon": "search", "description": "When playlist track discovery finishes", "available": True, + "has_conditions": True, + "condition_fields": ["playlist_name"], + "variables": ["playlist_name", "total_tracks", "discovered_count", "failed_count", "skipped_count"]}, + # Phase 3 triggers + {"type": "wishlist_processing_completed", "label": "Wishlist Processed", "icon": "check-circle", + "description": "When auto-wishlist processing finishes", "available": True, + "variables": ["tracks_processed", "tracks_found", "tracks_failed"]}, + {"type": "watchlist_scan_completed", "label": "Watchlist Scan Done", "icon": "check-circle", + "description": "When watchlist scan finishes", "available": True, + "variables": ["artists_scanned", "new_tracks_found", "tracks_added"]}, + {"type": "database_update_completed", "label": "Database Updated", "icon": "database", + "description": "When library database refresh finishes", "available": True, + "variables": ["total_artists", "total_albums", "total_tracks"]}, + {"type": "library_scan_completed", "label": "Library Scan Done", "icon": "hard-drive", + "description": "When media library scan finishes", "available": True, + "variables": ["server_type"]}, + {"type": "download_failed", "label": "Download Failed", "icon": "x-circle", + "description": "When a track permanently fails to download", "available": True, + "has_conditions": True, "condition_fields": ["artist", "title", "reason"], + "variables": ["artist", "title", "reason"]}, + {"type": "download_quarantined", "label": "File Quarantined", "icon": "alert-triangle", + "description": "When AcoustID verification fails", "available": True, + "has_conditions": True, "condition_fields": ["artist", "title"], + "variables": ["artist", "title", "reason"]}, + {"type": "wishlist_item_added", "label": "Wishlist Item Added", "icon": "plus-circle", + "description": "When a track is added to wishlist", "available": True, + "has_conditions": True, "condition_fields": ["artist", "title"], + "variables": ["artist", "title", "reason"]}, + {"type": "watchlist_artist_added", "label": "Artist Watched", "icon": "user-plus", + "description": "When an artist is added to watchlist", "available": True, + "has_conditions": True, "condition_fields": ["artist"], + "variables": ["artist", "artist_id"]}, + {"type": "watchlist_artist_removed", "label": "Artist Unwatched", "icon": "user-minus", + "description": "When an artist is removed from watchlist", "available": True, + "has_conditions": True, "condition_fields": ["artist"], + "variables": ["artist", "artist_id"]}, + {"type": "import_completed", "label": "Import Complete", "icon": "upload", + "description": "When album/track import finishes", "available": True, + "has_conditions": True, "condition_fields": ["artist", "album_name"], + "variables": ["track_count", "album_name", "artist"]}, + {"type": "mirrored_playlist_created", "label": "Playlist Mirrored", "icon": "copy", + "description": "When a new playlist is mirrored", "available": True, + "has_conditions": True, "condition_fields": ["playlist_name", "source"], + "variables": ["playlist_name", "source", "track_count"]}, + {"type": "quality_scan_completed", "label": "Quality Scan Done", "icon": "bar-chart", + "description": "When quality scan finishes", "available": True, + "variables": ["quality_met", "low_quality", "total_scanned"]}, + {"type": "duplicate_scan_completed", "label": "Duplicate Scan Done", "icon": "layers", + "description": "When duplicate cleaner finishes", "available": True, + "variables": ["files_scanned", "duplicates_found", "space_freed"]}, + # Signal trigger + {"type": "signal_received", "label": "Signal Received", "icon": "zap", + "description": "When another automation fires a named signal", "available": True, + "config_fields": [ + {"key": "signal_name", "type": "signal_input", "label": "Signal Name"} + ], + "variables": ["signal_name"]}, + # Webhook trigger + {"type": "webhook_received", "label": "Webhook Received", "icon": "globe", + "description": "When an external API request is received (POST /api/v1/request)", "available": True, + "variables": ["query", "request_id", "source"]}, +] + + +ACTIONS: list[dict] = [ + {"type": "process_wishlist", "label": "Process Wishlist", "icon": "list", "description": "Retry failed downloads from wishlist", "available": True, + "config_fields": [{"key": "category", "type": "select", "label": "Category", "options": [{"value": "all", "label": "All"}, {"value": "albums", "label": "Albums"}, {"value": "singles", "label": "Singles"}], "default": "all"}]}, + {"type": "scan_watchlist", "label": "Scan Watchlist", "icon": "eye", "description": "Check watched artists for new releases", "available": True}, + {"type": "scan_library", "label": "Scan Library", "icon": "refresh", "description": "Trigger media server library scan", "available": True}, + {"type": "refresh_mirrored", "label": "Refresh Mirrored Playlist", "icon": "copy", "description": "Re-fetch playlist from source and update mirror", "available": True, + "config_fields": [ + {"key": "playlist_id", "type": "mirrored_playlist_select", "label": "Playlist"}, + {"key": "all", "type": "checkbox", "label": "Refresh all mirrored playlists", "default": False} + ]}, + {"type": "sync_playlist", "label": "Sync Playlist", "icon": "sync", "description": "Sync mirrored playlist to media server", "available": True, + "config_fields": [ + {"key": "playlist_id", "type": "mirrored_playlist_select", "label": "Playlist"} + ]}, + {"type": "discover_playlist", "label": "Discover Playlist", "icon": "search", "description": "Find official Spotify/iTunes metadata for mirrored playlist tracks", "available": True, + "config_fields": [ + {"key": "playlist_id", "type": "mirrored_playlist_select", "label": "Playlist"}, + {"key": "all", "type": "checkbox", "label": "Discover all mirrored playlists", "default": False} + ]}, + {"type": "playlist_pipeline", "label": "Playlist Pipeline", "icon": "rocket", + "description": "Full lifecycle: refresh → discover → sync → download missing. One automation for the entire flow.", + "available": True, + "config_fields": [ + {"key": "playlist_id", "type": "mirrored_playlist_select", "label": "Playlist"}, + {"key": "all", "type": "checkbox", "label": "Process all mirrored playlists", "default": False}, + {"key": "skip_wishlist", "type": "checkbox", "label": "Skip wishlist processing", "default": False}, + ]}, + {"type": "notify_only", "label": "Notify Only", "icon": "bell", "description": "No action — just send notification", "available": True}, + # Phase 3 actions + {"type": "start_database_update", "label": "Update Database", "icon": "database", + "description": "Trigger library database refresh", "available": True, + "config_fields": [ + {"key": "full_refresh", "type": "checkbox", "label": "Full refresh (slower)", "default": False} + ]}, + {"type": "run_duplicate_cleaner", "label": "Run Duplicate Cleaner", "icon": "layers", + "description": "Scan for and remove duplicate files", "available": True}, + {"type": "clear_quarantine", "label": "Clear Quarantine", "icon": "trash", + "description": "Delete all quarantined files", "available": True}, + {"type": "cleanup_wishlist", "label": "Clean Up Wishlist", "icon": "filter", + "description": "Remove duplicate/owned tracks from wishlist", "available": True}, + {"type": "update_discovery_pool", "label": "Update Discovery", "icon": "compass", + "description": "Refresh discovery pool with new tracks", "available": True}, + {"type": "start_quality_scan", "label": "Run Quality Scan", "icon": "bar-chart", + "description": "Scan for low-quality audio files", "available": True, + "config_fields": [ + {"key": "scope", "type": "select", "label": "Scope", + "options": [{"value": "watchlist", "label": "Watchlist Artists"}, {"value": "library", "label": "Full Library"}], + "default": "watchlist"} + ]}, + {"type": "backup_database", "label": "Backup Database", "icon": "save", + "description": "Create timestamped database backup", "available": True}, + {"type": "refresh_beatport_cache", "label": "Refresh Beatport Cache", "icon": "music", + "description": "Scrape Beatport homepage and warm the cache", "available": True}, + {"type": "clean_search_history", "label": "Clean Search History", "icon": "trash-2", + "description": "Remove old searches from Soulseek", "available": True}, + {"type": "clean_completed_downloads", "label": "Clean Completed Downloads", "icon": "check-square", + "description": "Clear completed downloads and empty directories", "available": True}, + {"type": "full_cleanup", "label": "Full Cleanup", "icon": "trash", + "description": "Clear quarantine, download queue, import folder, and search history in one sweep", "available": True}, + {"type": "deep_scan_library", "label": "Deep Scan Library", "icon": "search", + "description": "Full library comparison without losing enrichment data", "available": True}, + {"type": "run_script", "label": "Run Script", "icon": "terminal", + "description": "Execute a script from the scripts folder", "available": True}, + {"type": "search_and_download", "label": "Search & Download", "icon": "download", + "description": "Search for a track and download the best match", "available": True, + "config_fields": [ + {"key": "query", "type": "text", "label": "Search Query", + "placeholder": "Artist - Track (leave empty to use trigger's query)"} + ]}, +] + + +NOTIFICATIONS: list[dict] = [ + {"type": "discord_webhook", "label": "Discord Webhook", "icon": "message", "description": "Send a Discord notification", "available": True, + "variables": ["time", "name", "run_count", "status"]}, + {"type": "pushbullet", "label": "Pushbullet", "icon": "push", "description": "Push notification to phone/desktop", "available": True, + "variables": ["time", "name", "run_count", "status"]}, + {"type": "telegram", "label": "Telegram", "icon": "message", "description": "Send a Telegram message", "available": True, + "variables": ["time", "name", "run_count", "status"]}, + {"type": "webhook", "label": "Webhook (POST)", "icon": "globe", "description": "Send a POST request to any URL", "available": True, + "variables": ["time", "name", "run_count", "status"]}, + # Signal fire action + {"type": "fire_signal", "label": "Fire Signal", "icon": "zap", + "description": "Fire a signal that other automations can listen for", "available": True, + "config_fields": [ + {"key": "signal_name", "type": "signal_input", "label": "Signal Name"} + ]}, + # Run script then-action + {"type": "run_script", "label": "Run Script", "icon": "terminal", + "description": "Execute a script after the action completes", "available": True, + "config_fields": [ + {"key": "script_name", "type": "script_select", "label": "Script"} + ]}, +] diff --git a/tests/automation/test_automation_blocks.py b/tests/automation/test_automation_blocks.py new file mode 100644 index 00000000..64d95cfa --- /dev/null +++ b/tests/automation/test_automation_blocks.py @@ -0,0 +1,82 @@ +"""Tests for core/automation/blocks.py — static block definitions for the builder UI. + +Catches accidental schema regressions in the builder block list (missing +`type`/`label`, malformed config_fields options, etc.). +""" + +from __future__ import annotations + +from core.automation import blocks + + +def _shape_check(items, allowed_types): + """Every item has type+label+description, plus type-specific shape rules.""" + seen_types = set() + for item in items: + assert 'type' in item, item + assert 'label' in item, item + assert isinstance(item.get('available'), bool), item + # No duplicate types within a list + assert item['type'] not in seen_types, f"Duplicate type {item['type']!r}" + seen_types.add(item['type']) + + if 'config_fields' in item: + for field in item['config_fields']: + assert 'key' in field + assert 'type' in field + assert field['type'] in allowed_types, f"Unknown field type {field['type']!r} in {item['type']}" + if field['type'] == 'select': + assert 'options' in field + for opt in field['options']: + assert 'value' in opt + assert 'label' in opt + + +_FIELD_TYPES = { + 'number', 'select', 'time', 'multi_select', 'checkbox', 'text', + 'mirrored_playlist_select', 'signal_input', 'script_select', +} + + +def test_triggers_shape(): + _shape_check(blocks.TRIGGERS, _FIELD_TYPES) + + +def test_actions_shape(): + _shape_check(blocks.ACTIONS, _FIELD_TYPES) + + +def test_notifications_shape(): + _shape_check(blocks.NOTIFICATIONS, _FIELD_TYPES) + + +def test_signal_received_trigger_present(): + types = {t['type'] for t in blocks.TRIGGERS} + assert 'signal_received' in types + + +def test_fire_signal_notification_present(): + types = {n['type'] for n in blocks.NOTIFICATIONS} + assert 'fire_signal' in types + + +def test_run_script_in_both_actions_and_notifications(): + """run_script can be either an action or a then-action — both lists own it.""" + action_types = {a['type'] for a in blocks.ACTIONS} + notif_types = {n['type'] for n in blocks.NOTIFICATIONS} + assert 'run_script' in action_types + assert 'run_script' in notif_types + + +def test_schedule_trigger_default_unit_is_hours(): + schedule = next(t for t in blocks.TRIGGERS if t['type'] == 'schedule') + unit_field = next(f for f in schedule['config_fields'] if f['key'] == 'unit') + assert unit_field['default'] == 'hours' + + +def test_event_triggers_with_conditions_have_condition_fields(): + for t in blocks.TRIGGERS: + if t.get('has_conditions'): + assert 'condition_fields' in t, f"{t['type']} marked has_conditions but no condition_fields" + assert isinstance(t['condition_fields'], list) + assert len(t['condition_fields']) > 0 diff --git a/web_server.py b/web_server.py index 80477b05..24fe6b10 100644 --- a/web_server.py +++ b/web_server.py @@ -6334,6 +6334,7 @@ def get_genre_whitelist_defaults(): # Automation route bodies live in core/automation/api.py — these routes are thin handlers. from core.automation import api as _auto_api +from core.automation import blocks as _auto_blocks from core.automation import signals as _auto_signals @@ -6505,202 +6506,10 @@ def list_available_scripts(): def get_automation_blocks(): """Return available block types for the automation builder sidebar.""" return jsonify({ - "triggers": [ - {"type": "schedule", "label": "Schedule", "icon": "clock", "description": "Run on a timer interval", "available": True, - "config_fields": [ - {"key": "interval", "type": "number", "label": "Every", "default": 6, "min": 1}, - {"key": "unit", "type": "select", "label": "Unit", - "options": [{"value": "minutes", "label": "Minutes"}, {"value": "hours", "label": "Hours"}, {"value": "days", "label": "Days"}], - "default": "hours"} - ]}, - {"type": "daily_time", "label": "Daily Time", "icon": "clock", "description": "Run every day at a specific time", "available": True, - "config_fields": [ - {"key": "time", "type": "time", "label": "At", "default": "03:00"} - ]}, - {"type": "weekly_time", "label": "Weekly Schedule", "icon": "calendar", "description": "Run on specific days of the week at a set time", "available": True, - "config_fields": [ - {"key": "time", "type": "time", "label": "At", "default": "03:00"}, - {"key": "days", "type": "multi_select", "label": "Days", - "options": [{"value": "mon", "label": "Mon"}, {"value": "tue", "label": "Tue"}, {"value": "wed", "label": "Wed"}, - {"value": "thu", "label": "Thu"}, {"value": "fri", "label": "Fri"}, {"value": "sat", "label": "Sat"}, {"value": "sun", "label": "Sun"}]} - ]}, - {"type": "app_started", "label": "App Started", "icon": "power", "description": "When SoulSync starts up", "available": True}, - {"type": "track_downloaded", "label": "Track Downloaded", "icon": "download", "description": "When a track finishes downloading", "available": True, - "has_conditions": True, - "condition_fields": ["artist", "title", "album", "quality"], - "variables": ["artist", "title", "album", "quality"]}, - {"type": "batch_complete", "label": "Batch Complete", "icon": "check-circle", "description": "When an album/playlist download finishes", "available": True, - "has_conditions": True, - "condition_fields": ["playlist_name"], - "variables": ["playlist_name", "total_tracks", "completed_tracks", "failed_tracks"]}, - {"type": "watchlist_new_release", "label": "New Release Found", "icon": "bell", "description": "When watchlist detects new music", "available": True, - "has_conditions": True, - "condition_fields": ["artist"], - "variables": ["artist", "new_tracks", "added_to_wishlist"]}, - {"type": "playlist_synced", "label": "Playlist Synced", "icon": "refresh", "description": "When a playlist sync completes", "available": True, - "has_conditions": True, - "condition_fields": ["playlist_name"], - "variables": ["playlist_name", "total_tracks", "matched_tracks", "synced_tracks", "failed_tracks"]}, - {"type": "playlist_changed", "label": "Playlist Changed", "icon": "edit", "description": "When a mirrored playlist detects track changes from source", "available": True, - "has_conditions": True, - "condition_fields": ["playlist_name"], - "variables": ["playlist_name", "old_count", "new_count", "added", "removed"]}, - {"type": "discovery_completed", "label": "Discovery Complete", "icon": "search", "description": "When playlist track discovery finishes", "available": True, - "has_conditions": True, - "condition_fields": ["playlist_name"], - "variables": ["playlist_name", "total_tracks", "discovered_count", "failed_count", "skipped_count"]}, - # Phase 3 triggers - {"type": "wishlist_processing_completed", "label": "Wishlist Processed", "icon": "check-circle", - "description": "When auto-wishlist processing finishes", "available": True, - "variables": ["tracks_processed", "tracks_found", "tracks_failed"]}, - {"type": "watchlist_scan_completed", "label": "Watchlist Scan Done", "icon": "check-circle", - "description": "When watchlist scan finishes", "available": True, - "variables": ["artists_scanned", "new_tracks_found", "tracks_added"]}, - {"type": "database_update_completed", "label": "Database Updated", "icon": "database", - "description": "When library database refresh finishes", "available": True, - "variables": ["total_artists", "total_albums", "total_tracks"]}, - {"type": "library_scan_completed", "label": "Library Scan Done", "icon": "hard-drive", - "description": "When media library scan finishes", "available": True, - "variables": ["server_type"]}, - {"type": "download_failed", "label": "Download Failed", "icon": "x-circle", - "description": "When a track permanently fails to download", "available": True, - "has_conditions": True, "condition_fields": ["artist", "title", "reason"], - "variables": ["artist", "title", "reason"]}, - {"type": "download_quarantined", "label": "File Quarantined", "icon": "alert-triangle", - "description": "When AcoustID verification fails", "available": True, - "has_conditions": True, "condition_fields": ["artist", "title"], - "variables": ["artist", "title", "reason"]}, - {"type": "wishlist_item_added", "label": "Wishlist Item Added", "icon": "plus-circle", - "description": "When a track is added to wishlist", "available": True, - "has_conditions": True, "condition_fields": ["artist", "title"], - "variables": ["artist", "title", "reason"]}, - {"type": "watchlist_artist_added", "label": "Artist Watched", "icon": "user-plus", - "description": "When an artist is added to watchlist", "available": True, - "has_conditions": True, "condition_fields": ["artist"], - "variables": ["artist", "artist_id"]}, - {"type": "watchlist_artist_removed", "label": "Artist Unwatched", "icon": "user-minus", - "description": "When an artist is removed from watchlist", "available": True, - "has_conditions": True, "condition_fields": ["artist"], - "variables": ["artist", "artist_id"]}, - {"type": "import_completed", "label": "Import Complete", "icon": "upload", - "description": "When album/track import finishes", "available": True, - "has_conditions": True, "condition_fields": ["artist", "album_name"], - "variables": ["track_count", "album_name", "artist"]}, - {"type": "mirrored_playlist_created", "label": "Playlist Mirrored", "icon": "copy", - "description": "When a new playlist is mirrored", "available": True, - "has_conditions": True, "condition_fields": ["playlist_name", "source"], - "variables": ["playlist_name", "source", "track_count"]}, - {"type": "quality_scan_completed", "label": "Quality Scan Done", "icon": "bar-chart", - "description": "When quality scan finishes", "available": True, - "variables": ["quality_met", "low_quality", "total_scanned"]}, - {"type": "duplicate_scan_completed", "label": "Duplicate Scan Done", "icon": "layers", - "description": "When duplicate cleaner finishes", "available": True, - "variables": ["files_scanned", "duplicates_found", "space_freed"]}, - # Signal trigger - {"type": "signal_received", "label": "Signal Received", "icon": "zap", - "description": "When another automation fires a named signal", "available": True, - "config_fields": [ - {"key": "signal_name", "type": "signal_input", "label": "Signal Name"} - ], - "variables": ["signal_name"]}, - # Webhook trigger - {"type": "webhook_received", "label": "Webhook Received", "icon": "globe", - "description": "When an external API request is received (POST /api/v1/request)", "available": True, - "variables": ["query", "request_id", "source"]}, - ], - "actions": [ - {"type": "process_wishlist", "label": "Process Wishlist", "icon": "list", "description": "Retry failed downloads from wishlist", "available": True, - "config_fields": [{"key": "category", "type": "select", "label": "Category", "options": [{"value": "all", "label": "All"}, {"value": "albums", "label": "Albums"}, {"value": "singles", "label": "Singles"}], "default": "all"}]}, - {"type": "scan_watchlist", "label": "Scan Watchlist", "icon": "eye", "description": "Check watched artists for new releases", "available": True}, - {"type": "scan_library", "label": "Scan Library", "icon": "refresh", "description": "Trigger media server library scan", "available": True}, - {"type": "refresh_mirrored", "label": "Refresh Mirrored Playlist", "icon": "copy", "description": "Re-fetch playlist from source and update mirror", "available": True, - "config_fields": [ - {"key": "playlist_id", "type": "mirrored_playlist_select", "label": "Playlist"}, - {"key": "all", "type": "checkbox", "label": "Refresh all mirrored playlists", "default": False} - ]}, - {"type": "sync_playlist", "label": "Sync Playlist", "icon": "sync", "description": "Sync mirrored playlist to media server", "available": True, - "config_fields": [ - {"key": "playlist_id", "type": "mirrored_playlist_select", "label": "Playlist"} - ]}, - {"type": "discover_playlist", "label": "Discover Playlist", "icon": "search", "description": "Find official Spotify/iTunes metadata for mirrored playlist tracks", "available": True, - "config_fields": [ - {"key": "playlist_id", "type": "mirrored_playlist_select", "label": "Playlist"}, - {"key": "all", "type": "checkbox", "label": "Discover all mirrored playlists", "default": False} - ]}, - {"type": "playlist_pipeline", "label": "Playlist Pipeline", "icon": "rocket", - "description": "Full lifecycle: refresh → discover → sync → download missing. One automation for the entire flow.", - "available": True, - "config_fields": [ - {"key": "playlist_id", "type": "mirrored_playlist_select", "label": "Playlist"}, - {"key": "all", "type": "checkbox", "label": "Process all mirrored playlists", "default": False}, - {"key": "skip_wishlist", "type": "checkbox", "label": "Skip wishlist processing", "default": False}, - ]}, - {"type": "notify_only", "label": "Notify Only", "icon": "bell", "description": "No action — just send notification", "available": True}, - # Phase 3 actions - {"type": "start_database_update", "label": "Update Database", "icon": "database", - "description": "Trigger library database refresh", "available": True, - "config_fields": [ - {"key": "full_refresh", "type": "checkbox", "label": "Full refresh (slower)", "default": False} - ]}, - {"type": "run_duplicate_cleaner", "label": "Run Duplicate Cleaner", "icon": "layers", - "description": "Scan for and remove duplicate files", "available": True}, - {"type": "clear_quarantine", "label": "Clear Quarantine", "icon": "trash", - "description": "Delete all quarantined files", "available": True}, - {"type": "cleanup_wishlist", "label": "Clean Up Wishlist", "icon": "filter", - "description": "Remove duplicate/owned tracks from wishlist", "available": True}, - {"type": "update_discovery_pool", "label": "Update Discovery", "icon": "compass", - "description": "Refresh discovery pool with new tracks", "available": True}, - {"type": "start_quality_scan", "label": "Run Quality Scan", "icon": "bar-chart", - "description": "Scan for low-quality audio files", "available": True, - "config_fields": [ - {"key": "scope", "type": "select", "label": "Scope", - "options": [{"value": "watchlist", "label": "Watchlist Artists"}, {"value": "library", "label": "Full Library"}], - "default": "watchlist"} - ]}, - {"type": "backup_database", "label": "Backup Database", "icon": "save", - "description": "Create timestamped database backup", "available": True}, - {"type": "refresh_beatport_cache", "label": "Refresh Beatport Cache", "icon": "music", - "description": "Scrape Beatport homepage and warm the cache", "available": True}, - {"type": "clean_search_history", "label": "Clean Search History", "icon": "trash-2", - "description": "Remove old searches from Soulseek", "available": True}, - {"type": "clean_completed_downloads", "label": "Clean Completed Downloads", "icon": "check-square", - "description": "Clear completed downloads and empty directories", "available": True}, - {"type": "full_cleanup", "label": "Full Cleanup", "icon": "trash", - "description": "Clear quarantine, download queue, import folder, and search history in one sweep", "available": True}, - {"type": "deep_scan_library", "label": "Deep Scan Library", "icon": "search", - "description": "Full library comparison without losing enrichment data", "available": True}, - {"type": "run_script", "label": "Run Script", "icon": "terminal", - "description": "Execute a script from the scripts folder", "available": True}, - {"type": "search_and_download", "label": "Search & Download", "icon": "download", - "description": "Search for a track and download the best match", "available": True, - "config_fields": [ - {"key": "query", "type": "text", "label": "Search Query", - "placeholder": "Artist - Track (leave empty to use trigger's query)"} - ]}, - ], - "notifications": [ - {"type": "discord_webhook", "label": "Discord Webhook", "icon": "message", "description": "Send a Discord notification", "available": True, - "variables": ["time", "name", "run_count", "status"]}, - {"type": "pushbullet", "label": "Pushbullet", "icon": "push", "description": "Push notification to phone/desktop", "available": True, - "variables": ["time", "name", "run_count", "status"]}, - {"type": "telegram", "label": "Telegram", "icon": "message", "description": "Send a Telegram message", "available": True, - "variables": ["time", "name", "run_count", "status"]}, - {"type": "webhook", "label": "Webhook (POST)", "icon": "globe", "description": "Send a POST request to any URL", "available": True, - "variables": ["time", "name", "run_count", "status"]}, - # Signal fire action - {"type": "fire_signal", "label": "Fire Signal", "icon": "zap", - "description": "Fire a signal that other automations can listen for", "available": True, - "config_fields": [ - {"key": "signal_name", "type": "signal_input", "label": "Signal Name"} - ]}, - # Run script then-action - {"type": "run_script", "label": "Run Script", "icon": "terminal", - "description": "Execute a script after the action completes", "available": True, - "config_fields": [ - {"key": "script_name", "type": "script_select", "label": "Script"} - ]}, - ], - "known_signals": _collect_known_signals(), + 'triggers': _auto_blocks.TRIGGERS, + 'actions': _auto_blocks.ACTIONS, + 'notifications': _auto_blocks.NOTIFICATIONS, + 'known_signals': _collect_known_signals(), }) @app.route('/api/mirrored-playlists/list', methods=['GET'])