Replace hardcoded post-download chain with system automations
This commit is contained in:
parent
8b6a2c0adc
commit
156c37d907
3 changed files with 99 additions and 146 deletions
|
|
@ -37,6 +37,19 @@ SYSTEM_AUTOMATIONS = [
|
|||
'action_type': 'scan_watchlist',
|
||||
'initial_delay': 300, # 5 minutes after startup
|
||||
},
|
||||
# Event-based system automations (no initial_delay/next_run needed)
|
||||
{
|
||||
'name': 'Auto-Scan After Downloads',
|
||||
'trigger_type': 'batch_complete',
|
||||
'trigger_config': {},
|
||||
'action_type': 'scan_library',
|
||||
},
|
||||
{
|
||||
'name': 'Auto-Update Database After Scan',
|
||||
'trigger_type': 'library_scan_completed',
|
||||
'trigger_config': {},
|
||||
'action_type': 'start_database_update',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -120,10 +133,13 @@ class AutomationEngine:
|
|||
existing = self.db.get_system_automation_by_action(spec['action_type'])
|
||||
|
||||
if existing:
|
||||
# Reset next_run to initial delay on every startup
|
||||
next_run = (datetime.now() + timedelta(seconds=spec['initial_delay'])).strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.db.update_automation(existing['id'], next_run=next_run)
|
||||
logger.info(f"System automation '{spec['name']}' next_run reset to {spec['initial_delay']}s from now")
|
||||
# Only reset next_run for timer-based triggers that have an initial delay
|
||||
if spec.get('initial_delay') is not None:
|
||||
next_run = (datetime.now() + timedelta(seconds=spec['initial_delay'])).strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.db.update_automation(existing['id'], next_run=next_run)
|
||||
logger.info(f"System automation '{spec['name']}' next_run reset to {spec['initial_delay']}s from now")
|
||||
else:
|
||||
logger.info(f"System automation '{spec['name']}' ready (event-based)")
|
||||
|
||||
def get_system_automation_next_run_seconds(self, action_type):
|
||||
"""Get seconds until next run for a system automation. Returns 0 if not found or disabled."""
|
||||
|
|
|
|||
|
|
@ -652,6 +652,17 @@ def _register_automation_handlers():
|
|||
|
||||
automation_engine.register_progress_callbacks(_progress_init, _progress_finish)
|
||||
|
||||
# Register permanent callback: when any scan completes, emit library_scan_completed event
|
||||
# This replaces the hardcoded scan_completion_callback → trigger_automatic_database_update chain
|
||||
if web_scan_manager:
|
||||
def _on_library_scan_completed():
|
||||
if automation_engine:
|
||||
server_type = getattr(web_scan_manager, '_current_server_type', None) or 'unknown'
|
||||
automation_engine.emit('library_scan_completed', {
|
||||
'server_type': server_type,
|
||||
})
|
||||
web_scan_manager.add_scan_completion_callback(_on_library_scan_completed)
|
||||
|
||||
print("✅ Automation action handlers registered")
|
||||
|
||||
|
||||
|
|
@ -3908,6 +3919,9 @@ def get_automation_blocks():
|
|||
{"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"],
|
||||
|
|
@ -6490,7 +6504,8 @@ def clear_quarantine():
|
|||
@app.route('/api/scan/request', methods=['POST'])
|
||||
def request_media_scan():
|
||||
"""
|
||||
Request a media library scan with automatic completion callback support.
|
||||
Request a media library scan.
|
||||
Database update is handled automatically by the 'Auto-Update Database After Scan' system automation.
|
||||
"""
|
||||
try:
|
||||
if not web_scan_manager:
|
||||
|
|
@ -6498,33 +6513,13 @@ def request_media_scan():
|
|||
|
||||
data = request.get_json() or {}
|
||||
reason = data.get('reason', 'Web UI download completed')
|
||||
auto_database_update = data.get('auto_database_update', True)
|
||||
|
||||
def scan_completion_callback():
|
||||
"""Callback to trigger automatic database update after scan completes"""
|
||||
if auto_database_update:
|
||||
try:
|
||||
logger.info("🔄 Starting automatic incremental database update after scan completion")
|
||||
# Start database update in a separate thread to avoid blocking
|
||||
threading.Thread(
|
||||
target=trigger_automatic_database_update,
|
||||
args=("Post-scan automatic update",),
|
||||
daemon=True
|
||||
).start()
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting automatic database update: {e}")
|
||||
|
||||
# Request scan with callback
|
||||
result = web_scan_manager.request_scan(
|
||||
reason=reason,
|
||||
callback=scan_completion_callback if auto_database_update else None
|
||||
)
|
||||
result = web_scan_manager.request_scan(reason=reason)
|
||||
|
||||
add_activity_item("📡", "Media Scan", f"Scan requested: {reason}", "Now")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"scan_info": result,
|
||||
"auto_database_update": auto_database_update
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -6581,7 +6576,16 @@ def request_incremental_database_update():
|
|||
}), 400
|
||||
|
||||
# Start incremental update
|
||||
result = trigger_automatic_database_update(reason)
|
||||
if db_update_state.get('status') == 'running':
|
||||
return jsonify({"success": False, "error": "Database update already running"}), 409
|
||||
|
||||
active_server = config_manager.get_active_media_server()
|
||||
with db_update_lock:
|
||||
db_update_state.update({
|
||||
"status": "running", "phase": "Initializing...",
|
||||
"progress": 0, "current_item": "", "processed": 0, "total": 0, "error_message": ""
|
||||
})
|
||||
db_update_executor.submit(_run_db_update_task, False, active_server)
|
||||
|
||||
add_activity_item("🔄", "Database Update", f"Incremental update started: {reason}", "Now")
|
||||
return jsonify({
|
||||
|
|
@ -6596,52 +6600,6 @@ def request_incremental_database_update():
|
|||
logger.error(f"Error requesting incremental database update: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
def trigger_automatic_database_update(reason="Automatic update"):
|
||||
"""
|
||||
Helper function to trigger automatic incremental database update.
|
||||
"""
|
||||
try:
|
||||
from config.settings import config_manager
|
||||
active_server = config_manager.get_active_media_server()
|
||||
|
||||
# Get the appropriate media client
|
||||
media_client = None
|
||||
if active_server == "jellyfin" and jellyfin_client:
|
||||
media_client = jellyfin_client
|
||||
elif active_server == "navidrome" and navidrome_client:
|
||||
media_client = navidrome_client
|
||||
else:
|
||||
media_client = plex_client # Default fallback
|
||||
|
||||
if not media_client or not media_client.is_connected():
|
||||
logger.error(f"No connected {active_server} client for automatic database update")
|
||||
return False
|
||||
|
||||
# Create and start database update worker
|
||||
worker = DatabaseUpdateWorker(
|
||||
media_client=media_client,
|
||||
server_type=active_server,
|
||||
full_refresh=False # Always incremental for automatic updates
|
||||
)
|
||||
|
||||
def update_completion_callback():
|
||||
logger.info(f"✅ Automatic incremental database update completed for {active_server}")
|
||||
add_activity_item("✅", "Database Update", f"Automatic update completed ({active_server})", "Now")
|
||||
|
||||
# Start update in background thread
|
||||
update_thread = threading.Thread(
|
||||
target=lambda: worker.run_with_callback(update_completion_callback),
|
||||
daemon=True
|
||||
)
|
||||
update_thread.start()
|
||||
|
||||
logger.info(f"🔄 Started automatic incremental database update for {active_server}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in automatic database update: {e}")
|
||||
return False
|
||||
|
||||
@app.route('/api/test/automation', methods=['POST'])
|
||||
def test_automation_workflow():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -12935,86 +12935,35 @@ window.matchedDownloadAlbum = matchedDownloadAlbum;
|
|||
window.matchedDownloadAlbumTrack = matchedDownloadAlbumTrack;
|
||||
|
||||
/**
|
||||
* Handle automatic post-download operations: cleanup → scan → database update
|
||||
* This replicates the GUI's automatic functionality after download modal completion
|
||||
* Handle post-download cleanup: clear finished downloads from slskd.
|
||||
* Scan and database update are now handled by system automations
|
||||
* (batch_complete → scan_library → library_scan_completed → start_database_update).
|
||||
*/
|
||||
async function handlePostDownloadAutomation(playlistId, process) {
|
||||
try {
|
||||
// Check if we have successful downloads that warrant automation
|
||||
const successfulDownloads = getSuccessfulDownloadCount(process);
|
||||
|
||||
if (successfulDownloads === 0) {
|
||||
console.log(`🔄 [AUTO] No successful downloads for ${playlistId} - skipping automation`);
|
||||
console.log(`🔄 [AUTO] No successful downloads for ${playlistId} - skipping cleanup`);
|
||||
return;
|
||||
}
|
||||
console.log(`🔄 [AUTO] Post-download cleanup for ${playlistId} (${successfulDownloads} successful downloads)`);
|
||||
|
||||
console.log(`🔄 [AUTO] Starting automatic post-download operations for ${playlistId} (${successfulDownloads} successful downloads)`);
|
||||
|
||||
// Step 1: Clear completed downloads from slskd
|
||||
console.log(`🗑️ [AUTO] Step 1: Clearing completed downloads...`);
|
||||
showToast('🗑️ Clearing completed downloads...', 'info', 3000);
|
||||
|
||||
// Clear completed downloads from slskd
|
||||
try {
|
||||
const clearResponse = await fetch('/api/downloads/clear-finished', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (clearResponse.ok) {
|
||||
console.log(`✅ [AUTO] Step 1 complete: Downloads cleared`);
|
||||
console.log(`✅ [AUTO] Completed downloads cleared`);
|
||||
} else {
|
||||
console.warn(`⚠️ [AUTO] Step 1 warning: Clear downloads failed, continuing anyway`);
|
||||
console.warn(`⚠️ [AUTO] Clear downloads failed, continuing anyway`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ [AUTO] Step 1 error: ${error.message}, continuing anyway`);
|
||||
console.warn(`⚠️ [AUTO] Clear error: ${error.message}`);
|
||||
}
|
||||
|
||||
// Step 2: Request media server scan
|
||||
console.log(`📡 [AUTO] Step 2: Requesting media server scan...`);
|
||||
showToast('📡 Scanning media server library...', 'info', 5000);
|
||||
|
||||
try {
|
||||
const scanResponse = await fetch('/api/scan/request', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
reason: `Download modal completed for ${playlistId} (${successfulDownloads} tracks)`,
|
||||
auto_database_update: true // This will trigger step 3 automatically after scan completes
|
||||
})
|
||||
});
|
||||
|
||||
const scanResult = await scanResponse.json();
|
||||
|
||||
if (scanResponse.ok && scanResult.success) {
|
||||
console.log(`✅ [AUTO] Step 2 complete: Media scan requested`);
|
||||
console.log(`🔄 [AUTO] Scan info:`, scanResult.scan_info);
|
||||
|
||||
// Show success toast with scan details
|
||||
if (scanResult.scan_info.status === 'scheduled') {
|
||||
showToast(`📡 Media scan scheduled (${scanResult.scan_info.delay_seconds}s delay)`, 'success', 5000);
|
||||
} else {
|
||||
showToast('📡 Media scan requested successfully', 'success', 3000);
|
||||
}
|
||||
|
||||
// Database update will be triggered automatically by the scan completion callback
|
||||
if (scanResult.auto_database_update) {
|
||||
console.log(`🔄 [AUTO] Step 3 will run automatically after scan completes`);
|
||||
showToast('🔄 Database update will follow automatically', 'info', 3000);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ [AUTO] Step 2 failed: ${scanResult.error || 'Unknown scan error'}`);
|
||||
showToast('❌ Media scan failed', 'error', 5000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ [AUTO] Step 2 error: ${error.message}`);
|
||||
showToast('❌ Media scan request failed', 'error', 5000);
|
||||
}
|
||||
|
||||
console.log(`🏁 [AUTO] Automatic post-download operations initiated for ${playlistId}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ [AUTO] Error in post-download automation: ${error.message}`);
|
||||
showToast('❌ Automatic operations failed', 'error', 5000);
|
||||
console.error(`❌ [AUTO] Error in post-download cleanup: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -15998,6 +15947,31 @@ const TOOL_HELP_CONTENT = {
|
|||
<p><code>{files_scanned}</code>, <code>{duplicates_found}</code>, <code>{space_freed}</code></p>
|
||||
`
|
||||
},
|
||||
'auto-library_scan_completed': {
|
||||
title: 'Library Scan Done',
|
||||
content: `
|
||||
<h4>What is this trigger?</h4>
|
||||
<p>Fires when a media server library scan is considered complete. This only happens after a <strong>Scan Library</strong> action was triggered — it cannot fire on its own.</p>
|
||||
|
||||
<h4>How does it know the scan is done?</h4>
|
||||
<p>Your media server (Plex, Jellyfin, Navidrome) doesn't send a "scan finished" signal back to SoulSync. So after telling the server to scan, SoulSync waits <strong>approximately 5 minutes</strong> and then assumes the scan has finished. This is a generous estimate that works for most libraries.</p>
|
||||
|
||||
<h4>Timing</h4>
|
||||
<p>From the moment a download finishes to when this trigger fires, expect roughly <strong>6-7 minutes</strong>:</p>
|
||||
<ol>
|
||||
<li>60 second debounce wait (groups multiple downloads together)</li>
|
||||
<li>Media server scan triggered</li>
|
||||
<li>~5 minute wait (assumed scan completion)</li>
|
||||
<li>This event fires</li>
|
||||
</ol>
|
||||
|
||||
<h4>Default use</h4>
|
||||
<p>The system automation <strong>Auto-Update Database After Scan</strong> listens for this trigger to start an incremental database update, keeping your SoulSync library in sync with your media server.</p>
|
||||
|
||||
<h4>Available variables</h4>
|
||||
<p><code>{server_type}</code> — which media server was scanned (plex, jellyfin, navidrome)</p>
|
||||
`
|
||||
},
|
||||
|
||||
// ==================== Automation Action Help ====================
|
||||
|
||||
|
|
@ -16040,10 +16014,21 @@ const TOOL_HELP_CONTENT = {
|
|||
title: 'Scan Library',
|
||||
content: `
|
||||
<h4>What does this action do?</h4>
|
||||
<p>Triggers your media server (Plex) to scan its music library folder for new or changed files. This makes newly downloaded music appear in your media server.</p>
|
||||
<p>Tells your media server (Plex, Jellyfin, or Navidrome) to scan its music library folder for new or changed files. This makes newly downloaded music appear in your media server.</p>
|
||||
|
||||
<h4>How it works</h4>
|
||||
<ol>
|
||||
<li>A <strong>60 second debounce</strong> groups rapid requests — if multiple downloads finish close together, only one scan is triggered</li>
|
||||
<li>After the debounce, your media server is told to scan</li>
|
||||
<li>SoulSync waits <strong>~5 minutes</strong> (your media server doesn't report when it's finished, so this is an assumed completion time)</li>
|
||||
<li>The <strong>Library Scan Done</strong> event fires, which can trigger follow-up actions like a database update</li>
|
||||
</ol>
|
||||
|
||||
<h4>Default use</h4>
|
||||
<p>The system automation <strong>Auto-Scan After Downloads</strong> uses this action to automatically scan your library when a batch download completes. You can disable that automation if you prefer to scan manually.</p>
|
||||
|
||||
<h4>Note</h4>
|
||||
<p>Jellyfin and Navidrome detect new files automatically in real-time, so this action is primarily useful for Plex.</p>
|
||||
<p>Jellyfin and Navidrome often detect new files automatically, but the scan ensures nothing is missed.</p>
|
||||
`
|
||||
},
|
||||
'auto-refresh_mirrored': {
|
||||
|
|
@ -29674,13 +29659,12 @@ async function handleMediaScanButtonClick() {
|
|||
statusValue.textContent = 'Scanning...';
|
||||
statusValue.style.color = 'rgb(var(--accent-rgb))';
|
||||
|
||||
// Request scan
|
||||
// Request scan (database update handled by system automation)
|
||||
const response = await fetch('/api/scan/request', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
reason: 'Manual scan triggered from dashboard',
|
||||
auto_database_update: true
|
||||
reason: 'Manual scan triggered from dashboard'
|
||||
})
|
||||
});
|
||||
|
||||
|
|
@ -29693,11 +29677,6 @@ async function handleMediaScanButtonClick() {
|
|||
let countdownInterval = null;
|
||||
let pollInterval = null;
|
||||
|
||||
// Show auto database update message
|
||||
if (result.auto_database_update) {
|
||||
showToast('🔄 Database will update automatically after scan', 'info', 3000);
|
||||
}
|
||||
|
||||
// Update last scan time
|
||||
const lastTimeEl = document.getElementById('media-scan-last-time');
|
||||
if (lastTimeEl) {
|
||||
|
|
@ -43233,7 +43212,7 @@ const _autoIcons = {
|
|||
download_quarantined: '\u26A0\uFE0F', wishlist_item_added: '\u2795',
|
||||
watchlist_artist_added: '\uD83D\uDC64', watchlist_artist_removed: '\uD83D\uDC64',
|
||||
import_completed: '\uD83D\uDCE5', mirrored_playlist_created: '\uD83D\uDCC2',
|
||||
quality_scan_completed: '\uD83D\uDCCA', duplicate_scan_completed: '\uD83D\uDDC2\uFE0F',
|
||||
quality_scan_completed: '\uD83D\uDCCA', duplicate_scan_completed: '\uD83D\uDDC2\uFE0F', library_scan_completed: '\uD83D\uDCE1',
|
||||
start_database_update: '\uD83D\uDDC4\uFE0F', run_duplicate_cleaner: '\uD83D\uDDC2\uFE0F',
|
||||
clear_quarantine: '\uD83D\uDDD1\uFE0F', cleanup_wishlist: '\uD83E\uDDF9',
|
||||
update_discovery_pool: '\uD83E\uDDED', start_quality_scan: '\uD83D\uDCCA',
|
||||
|
|
@ -43352,7 +43331,7 @@ function _autoFormatTrigger(type, config) {
|
|||
watchlist_artist_added: 'Artist Watched', watchlist_artist_removed: 'Artist Unwatched',
|
||||
import_completed: 'Import Complete', mirrored_playlist_created: 'Playlist Mirrored',
|
||||
quality_scan_completed: 'Quality Scan Done', duplicate_scan_completed: 'Duplicate Scan Done',
|
||||
signal_received: 'Signal Received' };
|
||||
library_scan_completed: 'Library Scan Done', signal_received: 'Signal Received' };
|
||||
let label = labels[type] || type || 'Unknown';
|
||||
if (config && config.conditions && config.conditions.length) {
|
||||
const first = config.conditions[0];
|
||||
|
|
|
|||
Loading…
Reference in a new issue