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).
pull/253/head
Broque Thomas 1 month ago
parent 7a24431e46
commit a2b9e32d04

@ -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 ──────────────────────────────────────────────

@ -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

@ -6782,6 +6782,7 @@
Server Imports <span class="library-history-tab-count" id="history-import-count">0</span>
</button>
</div>
<div class="history-source-bar" id="history-source-bar" style="display:none"></div>
<div class="library-history-list" id="library-history-list"></div>
<div class="library-history-pagination" id="library-history-pagination"></div>
</div>

@ -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]) =>
`<span class="history-source-chip" style="border-color:${_srcColors[src] || '#888'};color:${_srcColors[src] || '#888'}">${src}: ${cnt}</span>`
).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) {
: `<div class="library-history-thumb-placeholder">${entry.event_type === 'download' ? '📥' : '📚'}</div>`;
let badge = '';
if (entry.event_type === 'download' && entry.quality) {
badge = `<span class="library-history-badge download">${escapeHtml(entry.quality)}</span>`;
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 => `<span class="library-history-badge download">${escapeHtml(p)}</span>`).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 = `<span class="library-history-badge import">${escapeHtml(sourceName)}</span>`;

@ -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);

Loading…
Cancel
Save