From 5daa8c0596abc06e3be82d3775607d42f17c8067 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 8 Mar 2026 12:20:50 -0700 Subject: [PATCH] Add rich stats to automation run history --- web_server.py | 59 ++++++++++- webui/static/script.js | 223 ++++++++++++++++++++++++++++++++++++++++- webui/static/style.css | 16 +++ 3 files changed, 290 insertions(+), 8 deletions(-) diff --git a/web_server.py b/web_server.py index bd55d343..6cacde00 100644 --- a/web_server.py +++ b/web_server.py @@ -274,10 +274,22 @@ def _register_automation_handlers(): return {'status': 'completed'} except Exception as e: return {'status': 'error', 'error': str(e)} + # Note: wishlist processing is async (batch submitted to executor), stats come via batch completion def _auto_scan_watchlist(config): try: + pre_state_id = id(watchlist_scan_state) _process_watchlist_scan_automatically(automation_id=config.get('_automation_id')) + # Only report stats if a fresh scan actually ran (state dict was reassigned) + if id(watchlist_scan_state) != pre_state_id: + summary = watchlist_scan_state.get('summary', {}) + return { + 'status': 'completed', + 'artists_scanned': summary.get('total_artists', 0), + 'successful_scans': summary.get('successful_scans', 0), + 'new_tracks_found': summary.get('new_tracks_found', 0), + 'tracks_added_to_wishlist': summary.get('tracks_added_to_wishlist', 0), + } return {'status': 'completed'} except Exception as e: return {'status': 'error', 'error': str(e)} @@ -347,11 +359,12 @@ def _register_automation_handlers(): phase='Timed out', log_line='Library scan timed out after 30 minutes', log_type='error') return {'status': 'error', 'reason': 'Timed out', '_manages_own_progress': True} + elapsed = round(time.time() - poll_start, 1) _update_automation_progress(automation_id, status='finished', progress=100, phase='Complete', log_line='Library scan completed', log_type='success') - return {'status': 'completed', '_manages_own_progress': True} + return {'status': 'completed', '_manages_own_progress': True, 'scan_duration_seconds': elapsed} except Exception as e: _update_automation_progress(automation_id, status='error', @@ -659,7 +672,17 @@ def _register_automation_handlers(): final_status = db_update_state.get('status', 'unknown') if final_status == 'error': return {'status': 'error', 'reason': db_update_state.get('error_message', 'Unknown error'), '_manages_own_progress': True} - return {'status': 'completed', 'full_refresh': str(full), '_manages_own_progress': True} + with db_update_lock: + stats = { + 'status': 'completed', 'full_refresh': str(full), '_manages_own_progress': True, + 'artists': db_update_state.get('total', 0), + 'albums': db_update_state.get('total_albums', 0), + 'tracks': db_update_state.get('total_tracks', 0), + 'removed_artists': db_update_state.get('removed_artists', 0), + 'removed_albums': db_update_state.get('removed_albums', 0), + 'removed_tracks': db_update_state.get('removed_tracks', 0), + } + return stats def _auto_deep_scan_library(config): global _db_update_automation_id @@ -703,7 +726,17 @@ def _register_automation_handlers(): final_status = db_update_state.get('status', 'unknown') if final_status == 'error': return {'status': 'error', 'reason': db_update_state.get('error_message', 'Unknown error'), '_manages_own_progress': True} - return {'status': 'completed', '_manages_own_progress': True} + with db_update_lock: + stats = { + 'status': 'completed', '_manages_own_progress': True, + 'artists': db_update_state.get('total', 0), + 'albums': db_update_state.get('total_albums', 0), + 'tracks': db_update_state.get('total_tracks', 0), + 'removed_artists': db_update_state.get('removed_artists', 0), + 'removed_albums': db_update_state.get('removed_albums', 0), + 'removed_tracks': db_update_state.get('removed_tracks', 0), + } + return stats def _auto_run_duplicate_cleaner(config): automation_id = config.get('_automation_id') @@ -748,10 +781,18 @@ def _register_automation_handlers(): dupes = duplicate_cleaner_state.get('duplicates_found', 0) removed = duplicate_cleaner_state.get('deleted', 0) + space_freed = duplicate_cleaner_state.get('space_freed', 0) + scanned = duplicate_cleaner_state.get('files_scanned', 0) _update_automation_progress(automation_id, status='finished', progress=100, phase='Complete', log_line=f'Found {dupes} duplicates, removed {removed} files', log_type='success') - return {'status': 'completed', '_manages_own_progress': True} + return { + 'status': 'completed', '_manages_own_progress': True, + 'files_scanned': scanned, + 'duplicates_found': dupes, + 'files_deleted': removed, + 'space_freed_mb': round(space_freed / (1024 * 1024), 1), + } def _auto_clear_quarantine(config): import shutil as _shutil @@ -848,7 +889,13 @@ def _register_automation_handlers(): _update_automation_progress(automation_id, status='finished', progress=100, phase='Complete', log_line=f'Quality scan complete — {issues} issues found', log_type='success') - return {'status': 'completed', 'scope': scope, '_manages_own_progress': True} + return { + 'status': 'completed', 'scope': scope, '_manages_own_progress': True, + 'tracks_scanned': quality_scanner_state.get('processed', 0), + 'quality_met': quality_scanner_state.get('quality_met', 0), + 'low_quality': issues, + 'matched': quality_scanner_state.get('matched', 0), + } def _auto_backup_database(config): import sqlite3, glob as _glob @@ -14175,6 +14222,8 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ with db_update_lock: db_update_state["status"] = "finished" db_update_state["phase"] = f"Completed: {successful} successful, {failed} failed{removal_msg}." + db_update_state["total_albums"] = total_albums + db_update_state["total_tracks"] = total_tracks db_update_state["removed_artists"] = removed_artists db_update_state["removed_albums"] = removed_albums db_update_state["removed_tracks"] = removed_tracks diff --git a/webui/static/script.js b/webui/static/script.js index bb9ae142..d271ec07 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -16385,7 +16385,7 @@ const TOOL_HELP_CONTENT = {
Creates a timestamped backup of SoulSync's SQLite database. Uses the SQLite backup API for a safe hot-copy while the app is running.
Keeps the last 3 backups automatically. Older backups are cleaned up to save disk space.
+Keeps the last 5 backups automatically. Older backups are cleaned up to save disk space.
Scrapes the Beatport homepage for top charts and caches the results locally. Keeps the Beatport charts page loading instantly without needing to scrape on every visit.
+ +Cache lasts 24 hours. This action refreshes it early so it's always warm when you visit the charts page.
+ +Removes old search queries from Soulseek. This keeps your search history clean and prevents buildup over time.
+ +Clears completed downloads from the transfer list and removes any empty directories left behind in the staging folder.
+ +Walks your entire media server library and compares it against SoulSync's database. Adds any new tracks found and removes stale entries that no longer exist on the server.
+ +Sends a notification to a Discord channel via webhook when the automation's action completes.
+ +Use these in your message template:
+{time} — When the automation ran{name} — Automation name{run_count} — How many times this automation has run{status} — Result status of the actionSends a push notification to your phone or desktop via Pushbullet when the automation's action completes.
+ +Use these in your message template:
+{time} — When the automation ran{name} — Automation name{run_count} — How many times this automation has run{status} — Result status of the actionSends a message to a Telegram chat via bot when the automation's action completes.
+ +Use these in your message template:
+{time} — When the automation ran{name} — Automation name{run_count} — How many times this automation has run{status} — Result status of the action