From ad262822a4285c00233ab7ef4220581240c1f50f Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:21:37 -0700 Subject: [PATCH] Cache per-track results in sync history + fix config_manager import Sync operations now store per-track data (name, artist, match status, confidence, download status) in a new track_results column on sync_history. Also fixed missing config_manager import in add_to_wishlist that crashed the duplicate tracks toggle. --- database/music_database.py | 28 +++++++++++++++- web_server.py | 68 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index 26cc4f74..026f6797 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -518,12 +518,23 @@ class MusicDatabase: is_album_download INTEGER DEFAULT 0, playlist_folder_mode INTEGER DEFAULT 0, started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - completed_at TIMESTAMP + completed_at TIMESTAMP, + track_results TEXT ) """) cursor.execute("CREATE INDEX IF NOT EXISTS idx_sh_started_at ON sync_history (started_at DESC)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_sh_source ON sync_history (source)") + # Migration: add track_results column to existing sync_history tables + try: + cursor.execute("SELECT track_results FROM sync_history LIMIT 1") + except Exception: + try: + cursor.execute("ALTER TABLE sync_history ADD COLUMN track_results TEXT") + logger.info("Added track_results column to sync_history table") + except Exception: + pass + conn.commit() logger.info("Database initialized successfully") @@ -6059,6 +6070,7 @@ class MusicDatabase: # Check for duplicates by track name + artist (not just Spotify ID) # When allow_duplicates is True (default), same song from different albums can coexist + from config.settings import config_manager allow_duplicates = config_manager.get('wishlist.allow_duplicate_tracks', True) if not allow_duplicates: @@ -9012,6 +9024,20 @@ class MusicDatabase: logger.debug(f"Error updating sync history completion: {e}") return False + def update_sync_history_track_results(self, batch_id, track_results_json): + """Store per-track match/download results on a sync_history entry.""" + try: + conn = self._get_connection() + cursor = conn.cursor() + cursor.execute(""" + UPDATE sync_history SET track_results = ? WHERE batch_id = ? + """, (track_results_json, batch_id)) + conn.commit() + return cursor.rowcount > 0 + except Exception as e: + logger.debug(f"Error updating sync history track results: {e}") + return False + def refresh_sync_history_entry(self, entry_id, tracks_found=0, tracks_downloaded=0, tracks_failed=0): """Update an existing sync_history entry with new stats and reset timestamps to move it to the top.""" try: diff --git a/web_server.py b/web_server.py index 444746ef..01721efb 100644 --- a/web_server.py +++ b/web_server.py @@ -24320,6 +24320,27 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): try: db_sh = MusicDatabase() db_sh.update_sync_history_completion(batch_id, tracks_found=tracks_found, tracks_downloaded=0, tracks_failed=0) + # Save per-track results (all found, no downloads) + track_results = [] + for res in analysis_results: + td = res.get('track', {}) + artists = td.get('artists', []) + first_artist = (artists[0].get('name', artists[0]) if isinstance(artists[0], dict) else str(artists[0])) if artists else '' + alb = td.get('album', '') + track_results.append({ + 'index': res.get('track_index', 0), + 'name': td.get('name', ''), + 'artist': first_artist, + 'album': alb.get('name', '') if isinstance(alb, dict) else str(alb or ''), + 'duration_ms': td.get('duration_ms', 0), + 'source_track_id': td.get('id', ''), + 'status': 'found' if res.get('found') else 'not_found', + 'confidence': round(res.get('confidence', 0.0), 3), + 'matched_track': None, + 'download_status': None, + }) + if track_results: + db_sh.update_sync_history_track_results(batch_id, json.dumps(track_results)) except Exception: pass @@ -27198,6 +27219,7 @@ def get_sync_history_entry(entry_id): entry['tracks'] = json.loads(entry['tracks_json']) if entry.get('tracks_json') else [] entry['artist_context'] = json.loads(entry['artist_context']) if entry.get('artist_context') else None entry['album_context'] = json.loads(entry['album_context']) if entry.get('album_context') else None + entry['track_results'] = json.loads(entry['track_results']) if entry.get('track_results') else None entry.pop('tracks_json', None) return jsonify({"success": True, "entry": entry}) @@ -27317,7 +27339,7 @@ def _record_sync_history_start(batch_id, playlist_id, playlist_name, tracks, logger.warning(f"Failed to record sync history start: {e}") def _record_sync_history_completion(batch_id, batch): - """Update sync history with completion stats. + """Update sync history with completion stats and per-track results. NOTE: Called from within tasks_lock context — do NOT acquire tasks_lock here.""" try: analysis_results = batch.get('analysis_results', []) @@ -27325,13 +27347,53 @@ def _record_sync_history_completion(batch_id, batch): queue = batch.get('queue', []) completed_count = 0 failed_count = len(batch.get('permanently_failed_tracks', [])) - # Already inside tasks_lock — safe to read download_tasks directly + + # Build download status map: track_index → status + download_status_map = {} for task_id in queue: - if task_id in download_tasks and download_tasks[task_id].get('status') == 'completed': + task = download_tasks.get(task_id, {}) + ti = task.get('track_index') + if ti is not None: + download_status_map[ti] = task.get('status', 'unknown') + if task.get('status') == 'completed': completed_count += 1 + # Build per-track results from analysis + track_results = [] + for res in analysis_results: + track_data = res.get('track', {}) + artists = track_data.get('artists', []) + if artists: + first = artists[0] + artist_name = first.get('name', first) if isinstance(first, dict) else str(first) + else: + artist_name = '' + + album = track_data.get('album', '') + album_name = album.get('name', '') if isinstance(album, dict) else str(album or '') + + idx = res.get('track_index', 0) + entry = { + 'index': idx, + 'name': track_data.get('name', ''), + 'artist': artist_name, + 'album': album_name, + 'duration_ms': track_data.get('duration_ms', 0), + 'source_track_id': track_data.get('id', ''), + 'status': 'found' if res.get('found') else 'not_found', + 'confidence': round(res.get('confidence', 0.0), 3), + 'matched_track': None, + 'download_status': download_status_map.get(idx), + } + track_results.append(entry) + db = MusicDatabase() db.update_sync_history_completion(batch_id, tracks_found, completed_count, failed_count) + + # Save per-track results + if track_results: + db.update_sync_history_track_results(batch_id, json.dumps(track_results)) + except Exception as e: logger.warning(f"Failed to record sync history completion: {e}")