From a2b9e32d04884ffa15c3fd8ba048281ed9a4d299 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:03:46 -0700 Subject: [PATCH] Add download source tracking to library history modal New download_source column on library_history table records which source (Soulseek, Tidal, Qobuz, HiFi, YouTube, Deezer) each track was downloaded from. Extracted from context username during post-processing. Frontend shows source badge alongside quality badge on each download entry. Source breakdown bar below tabs shows per-source totals with color-coded chips (e.g., "Soulseek: 847 | Tidal: 203"). Includes DB migration for existing installs. Existing entries show quality only (source is NULL). --- database/music_database.py | 36 ++++++++++++++++++++++++++++++------ web_server.py | 9 ++++++++- webui/index.html | 1 + webui/static/script.js | 23 +++++++++++++++++++++-- webui/static/style.css | 16 ++++++++++++++++ 5 files changed, 76 insertions(+), 9 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index 9a68eaee..73f39bfb 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -496,12 +496,20 @@ class MusicDatabase: server_source TEXT, file_path TEXT, thumb_url TEXT, + download_source TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) cursor.execute("CREATE INDEX IF NOT EXISTS idx_lh_event_type ON library_history (event_type)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_lh_created_at ON library_history (created_at DESC)") + # Migration: add download_source column + cursor.execute("PRAGMA table_info(library_history)") + lh_cols = {c[1] for c in cursor.fetchall()} + 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") + # Sync history table — tracks the last 100 sync operations with cached context for re-trigger cursor.execute(""" CREATE TABLE IF NOT EXISTS sync_history ( @@ -9600,16 +9608,17 @@ class MusicDatabase: # ── Library History ───────────────────────────────────────────────── 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): + quality=None, server_source=None, file_path=None, thumb_url=None, + download_source=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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, (event_type, title, artist_name, album_name, quality, server_source, file_path, thumb_url)) + 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)) conn.commit() return True except Exception as e: @@ -9645,7 +9654,7 @@ class MusicDatabase: return [], 0 def get_library_history_stats(self): - """Return counts per event_type: {downloads: int, imports: int}.""" + """Return counts per event_type and per download_source.""" try: conn = self._get_connection() cursor = conn.cursor() @@ -9656,10 +9665,25 @@ class MusicDatabase: stats['downloads'] = row['cnt'] elif row['event_type'] == 'import': stats['imports'] = row['cnt'] + + # Per-source breakdown for downloads + source_counts = {} + try: + cursor.execute(""" + SELECT download_source, COUNT(*) as cnt FROM library_history + WHERE event_type = 'download' AND download_source IS NOT NULL AND download_source != '' + GROUP BY download_source ORDER BY cnt DESC + """) + for row in cursor.fetchall(): + source_counts[row['download_source']] = row['cnt'] + except Exception: + pass + stats['source_counts'] = source_counts + return stats except Exception as e: logger.debug(f"Error getting library history stats: {e}") - return {'downloads': 0, 'imports': 0} + return {'downloads': 0, 'imports': 0, 'source_counts': {}} # ── Sync History ────────────────────────────────────────────── diff --git a/web_server.py b/web_server.py index 95ca611c..7f6daf78 100644 --- a/web_server.py +++ b/web_server.py @@ -1874,6 +1874,12 @@ def _emit_track_downloaded(context): def _record_library_history_download(context): """Record a completed download to the library_history table. Non-blocking.""" try: + # Determine download source + search_result = context.get('original_search_result') or context.get('search_result') or {} + username = search_result.get('username', context.get('_download_username', '')) + _svc_map = {'youtube': 'YouTube', 'tidal': 'Tidal', 'qobuz': 'Qobuz', 'hifi': 'HiFi', 'deezer_dl': 'Deezer'} + download_source = _svc_map.get(username, 'Soulseek') + ti = context.get('track_info') or context.get('search_result') or {} artist_name = '' artists = ti.get('artists', []) @@ -1912,7 +1918,8 @@ def _record_library_history_download(context): album_name=album_name, quality=quality, file_path=file_path, - thumb_url=thumb_url + thumb_url=thumb_url, + download_source=download_source ) except Exception: pass # Non-critical, never block download flow diff --git a/webui/index.html b/webui/index.html index b3c05bcf..1bd1e518 100644 --- a/webui/index.html +++ b/webui/index.html @@ -6782,6 +6782,7 @@ Server Imports 0 +
diff --git a/webui/static/script.js b/webui/static/script.js index 6722ff24..2dde28a7 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -20994,6 +20994,22 @@ async function loadLibraryHistory() { if (dlCount) dlCount.textContent = data.stats?.downloads || 0; if (imCount) imCount.textContent = data.stats?.imports || 0; + // Source breakdown bar (downloads tab only) + const sourceBar = document.getElementById('history-source-bar'); + if (sourceBar) { + const sc = data.stats?.source_counts || {}; + const srcEntries = Object.entries(sc).sort((a, b) => b[1] - a[1]); + if (srcEntries.length > 0 && tab === 'download') { + const _srcColors = { Soulseek: '#4caf50', Tidal: '#000', YouTube: '#ff0000', Qobuz: '#4285f4', HiFi: '#00bcd4', Deezer: '#a238ff' }; + sourceBar.innerHTML = srcEntries.map(([src, cnt]) => + `${src}: ${cnt}` + ).join(''); + sourceBar.style.display = ''; + } else { + sourceBar.style.display = 'none'; + } + } + if (!data.entries || data.entries.length === 0) { const emptyIcon = tab === 'download' ? '📥' : '📚'; const emptyText = tab === 'download' @@ -21019,8 +21035,11 @@ function renderHistoryEntry(entry) { : `