From f8fbcb507c9a5348be9a0966fa099261923a2bb5 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:18:58 -0700 Subject: [PATCH] Add source provenance and AcoustID result to download history Track original source filename, track ID, and AcoustID verification result for every download. Helps debug wrong-file downloads from streaming sources like Tidal. Each column migrated independently for crash safety. Frontend shows source detail line and color-coded AcoustID badge per entry. Button renamed to "Download History". --- database/music_database.py | 15 +++++++++++---- web_server.py | 34 +++++++++++++++++++++++++++++++++- webui/index.html | 2 +- webui/static/script.js | 23 ++++++++++++++++++++++- webui/static/style.css | 10 ++++++++++ 5 files changed, 77 insertions(+), 7 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index ac622c68..930ea223 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -509,6 +509,10 @@ class MusicDatabase: if 'download_source' not in lh_cols: cursor.execute("ALTER TABLE library_history ADD COLUMN download_source TEXT") logger.info("Added download_source column to library_history") + for _col in ['source_track_id', 'source_track_title', 'source_filename', 'acoustid_result']: + if _col not in lh_cols: + cursor.execute(f"ALTER TABLE library_history ADD COLUMN {_col} TEXT") + logger.info(f"Added {_col} column to library_history") # Sync history table β€” tracks the last 100 sync operations with cached context for re-trigger cursor.execute(""" @@ -9617,16 +9621,19 @@ class MusicDatabase: def add_library_history_entry(self, event_type, title, artist_name=None, album_name=None, quality=None, server_source=None, file_path=None, thumb_url=None, - download_source=None): + download_source=None, source_track_id=None, source_track_title=None, + source_filename=None, acoustid_result=None): """Record a download or import event to the library history table.""" try: conn = self._get_connection() cursor = conn.cursor() cursor.execute(""" INSERT INTO library_history (event_type, title, artist_name, album_name, - quality, server_source, file_path, thumb_url, download_source) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - """, (event_type, title, artist_name, album_name, quality, server_source, file_path, thumb_url, download_source)) + quality, server_source, file_path, thumb_url, download_source, + source_track_id, source_track_title, source_filename, acoustid_result) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, (event_type, title, artist_name, album_name, quality, server_source, file_path, thumb_url, + download_source, source_track_id, source_track_title, source_filename, acoustid_result)) conn.commit() return True except Exception as e: diff --git a/web_server.py b/web_server.py index 5f9e8adf..f780e6d5 100644 --- a/web_server.py +++ b/web_server.py @@ -1910,6 +1910,19 @@ def _record_library_history_download(context): if isinstance(album_info, dict): thumb_url = album_info.get('album_image_url', '') + # Source provenance β€” what file/track was actually downloaded + source_filename = search_result.get('filename', '') + # Track ID: try search result first, then track_info (Spotify ID used for streaming lookups) + source_track_id = (search_result.get('track_id', '') + or search_result.get('id', '') + or ti.get('id', '')) + # Source track title: only save if it differs from expected title (otherwise it's just noise) + _src_title = search_result.get('title', '') or search_result.get('name', '') + source_track_title = _src_title if _src_title and _src_title != title else '' + + # AcoustID verification result + acoustid_result = context.get('_acoustid_result', '') + db = get_database() db.add_library_history_entry( event_type='download', @@ -1919,7 +1932,11 @@ def _record_library_history_download(context): quality=quality, file_path=file_path, thumb_url=thumb_url, - download_source=download_source + download_source=download_source, + source_track_id=source_track_id, + source_track_title=source_track_title, + source_filename=source_filename, + acoustid_result=acoustid_result ) except Exception: pass # Non-critical, never block download flow @@ -19588,6 +19605,7 @@ def _post_process_matched_download(context_key, context, file_path): context ) print(f"πŸ” AcoustID verification result: {verification_result.value} - {verification_msg}") + context['_acoustid_result'] = verification_result.value if verification_result == VerificationResult.FAIL: # Move to quarantine instead of Transfer @@ -19629,11 +19647,14 @@ def _post_process_matched_download(context_key, context, file_path): return # NEVER continue processing a known-wrong file else: print(f"⚠️ AcoustID verification skipped: missing track/artist info") + context['_acoustid_result'] = 'skip' else: print(f"ℹ️ AcoustID verification not available: {available_reason}") + context['_acoustid_result'] = 'disabled' except Exception as verify_error: # Any verification error should NOT block the download - fail open print(f"⚠️ AcoustID verification error (continuing normally): {verify_error}") + context['_acoustid_result'] = 'error' # --- END ACOUSTID VERIFICATION --- # --- SIMPLE DOWNLOAD HANDLING --- @@ -20887,6 +20908,17 @@ def get_version_info(): "title": "What's New in SoulSync", "subtitle": f"Version {SOULSYNC_VERSION} β€” Latest Changes", "sections": [ + { + "title": "πŸ“‹ Download History β€” Source Provenance", + "description": "Download history now tracks the original source file info for every download", + "features": [ + "β€’ Source filename, track ID, and original track title saved with each download", + "β€’ AcoustID verification result (Verified/Failed/Skipped/Off) shown as a badge per entry", + "β€’ Source details displayed in monospace under each history entry for easy debugging", + "β€’ Settings Connections tab redesigned with collapsible accordion services and brand-colored dots" + ], + "usage_note": "Click 'Download History' on the Dashboard to see source provenance for new downloads." + }, { "title": "πŸ—ΊοΈ Artist Map β€” Visualize Your Music Universe", "description": "Three interactive canvas-based visualization modes on the Discover page", diff --git a/webui/index.html b/webui/index.html index a41a5752..f7bea449 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1094,7 +1094,7 @@

Recent Activity

- +
diff --git a/webui/static/script.js b/webui/static/script.js index 0f6119c1..784d03c1 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -21081,15 +21081,36 @@ function renderHistoryEntry(entry) { badge = `${escapeHtml(sourceName)}`; } + // AcoustID badge + let acoustidBadge = ''; + if (entry.acoustid_result) { + const _aidColors = { pass: '#4caf50', fail: '#ef5350', skip: '#ff9800', disabled: '#666', error: '#ef5350' }; + const _aidLabels = { pass: 'Verified', fail: 'Failed', skip: 'Skipped', disabled: 'Off', error: 'Error' }; + const color = _aidColors[entry.acoustid_result] || '#666'; + const label = _aidLabels[entry.acoustid_result] || entry.acoustid_result; + acoustidBadge = `AcoustID: ${label}`; + } + const meta = [entry.artist_name, entry.album_name].filter(Boolean).join(' β€” '); + // Source provenance detail line + let sourceDetail = ''; + if (entry.event_type === 'download' && (entry.source_filename || entry.source_track_title)) { + const parts = []; + if (entry.source_filename) parts.push(`File: ${escapeHtml(entry.source_filename)}`); + else if (entry.source_track_title) parts.push(`Source track: ${escapeHtml(entry.source_track_title)}`); + if (entry.source_track_id) parts.push(`ID: ${escapeHtml(entry.source_track_id)}`); + sourceDetail = `
${parts.join(' Β· ')}
`; + } + return `
${thumb}
${escapeHtml(entry.title || 'Unknown')}
+ ${sourceDetail}
- ${badge} + ${badge}${acoustidBadge}
${formatHistoryTime(entry.created_at)}
`; } diff --git a/webui/static/style.css b/webui/static/style.css index 1c512f17..69228b96 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -9423,6 +9423,16 @@ body.helper-mode-active #dashboard-activity-feed:hover { margin-top: 2px; } +.library-history-entry-source { + font-size: 10px; + color: rgba(255, 255, 255, 0.25); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 2px; + font-family: monospace; +} + .library-history-badge { font-size: 10px; font-weight: 600;