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).
This commit is contained in:
parent
7a24431e46
commit
a2b9e32d04
5 changed files with 76 additions and 9 deletions
|
|
@ -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…
Reference in a new issue