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) { : `
${entry.event_type === 'download' ? '📥' : '📚'}
`; let badge = ''; - if (entry.event_type === 'download' && entry.quality) { - badge = `${escapeHtml(entry.quality)}`; + if (entry.event_type === 'download') { + const parts = []; + if (entry.download_source) parts.push(entry.download_source); + if (entry.quality) parts.push(entry.quality); + badge = parts.map(p => `${escapeHtml(p)}`).join(''); } else if (entry.event_type === 'import' && entry.server_source) { const sourceName = { plex: 'Plex', jellyfin: 'Jellyfin', navidrome: 'Navidrome' }[entry.server_source] || entry.server_source; badge = `${escapeHtml(sourceName)}`; diff --git a/webui/static/style.css b/webui/static/style.css index 30beacb4..daa12daa 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -9446,6 +9446,22 @@ body.helper-mode-active #dashboard-activity-feed:hover { border: 1px solid rgba(120, 160, 230, 0.2); } +.history-source-bar { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 8px 16px; +} + +.history-source-chip { + font-size: 11px; + font-weight: 600; + padding: 3px 10px; + border-radius: 12px; + border: 1px solid; + background: rgba(255, 255, 255, 0.03); +} + .library-history-entry-time { font-size: 11px; color: rgba(255, 255, 255, 0.25);