From 3c06e66f03e3dfaf0b9ff928b4142a5a3223ec5a Mon Sep 17 00:00:00 2001
From: JohnBaumb <80135794+JohnBaumb@users.noreply.github.com>
Date: Wed, 22 Apr 2026 11:59:12 -0700
Subject: [PATCH 1/7] fix: downloads page album art, total count, and
clear-completed safety
- Fix missing album art on downloads page across all sources
- Fix downloads page showing wrong total count
- Enrich missing download artwork from metadata cache
- Prevent Clear Completed from removing active download batches
- Keep download limit at 300 per performance design
---
web_server.py | 37 ++++++++++++++++++++++++++++++++++++-
1 file changed, 36 insertions(+), 1 deletion(-)
diff --git a/web_server.py b/web_server.py
index 7adbe3a2..249f7a2e 100644
--- a/web_server.py
+++ b/web_server.py
@@ -31402,6 +31402,34 @@ def get_all_downloads_unified():
# Sort: active first (by priority), then by timestamp desc within each group
items.sort(key=lambda x: (x['priority'], -x['timestamp']))
+ # POST-LOCK: Enrich missing artwork from metadata cache (handles legacy
+ # wishlist tracks that were stored without image data)
+ items_needing_art = [it for it in items if not it.get('artwork') and it.get('title')]
+ if items_needing_art:
+ try:
+ database = get_database()
+ # Batch lookup by track name — metadata cache has deezer/itunes images
+ track_names = list({it['title'] for it in items_needing_art if it['title']})
+ if track_names:
+ placeholders = ','.join('?' for _ in track_names)
+ with database._get_connection() as conn:
+ cursor = conn.cursor()
+ cursor.execute(f"""
+ SELECT name, image_url FROM metadata_cache_entities
+ WHERE entity_type='track' AND name IN ({placeholders})
+ AND image_url IS NOT NULL AND image_url != ''
+ """, track_names)
+ name_to_art = {}
+ for row in cursor.fetchall():
+ if row[0] not in name_to_art:
+ name_to_art[row[0]] = row[1]
+ for it in items_needing_art:
+ cached_url = name_to_art.get(it['title'])
+ if cached_url:
+ it['artwork'] = cached_url
+ except Exception as cache_err:
+ logger.debug(f"Metadata cache artwork lookup failed: {cache_err}")
+
# Build batch summaries for the batch context panel
batch_summaries = []
with tasks_lock:
@@ -31462,9 +31490,16 @@ def clear_completed_downloads():
for tid in task_ids_to_remove:
del download_tasks[tid]
cleared += 1
- # Also clean up empty batches
+ # Also clean up empty batches (but not those still actively processing)
+ active_phases = {'analysis', 'downloading', 'queued'}
empty_batches = []
for bid, batch in download_batches.items():
+ # Never remove batches that are still actively working
+ if batch.get('phase') in active_phases:
+ # Still prune cleared tasks from queue
+ remaining = [t for t in batch.get('queue', []) if t in download_tasks]
+ batch['queue'] = remaining
+ continue
remaining = [t for t in batch.get('queue', []) if t in download_tasks]
if not remaining:
empty_batches.append(bid)
From c3b9f97afcad81ea55ace0a24388e5d99ff3232b Mon Sep 17 00:00:00 2001
From: JohnBaumb <80135794+JohnBaumb@users.noreply.github.com>
Date: Wed, 22 Apr 2026 15:21:11 -0700
Subject: [PATCH 2/7] Add analysis progress to downloads page batch cards
---
web_server.py | 1 +
webui/static/pages-extra.js | 8 ++++++--
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/web_server.py b/web_server.py
index 249f7a2e..00bdce62 100644
--- a/web_server.py
+++ b/web_server.py
@@ -31444,6 +31444,7 @@ def get_all_downloads_unified():
'phase': batch.get('phase', 'unknown'),
'total': len(queue),
'analysis_total': batch.get('analysis_total', len(queue)),
+ 'analysis_processed': batch.get('analysis_processed', 0),
'completed': sum(1 for s in statuses if s in ('completed', 'skipped', 'already_owned')),
'failed': sum(1 for s in statuses if s in ('failed', 'not_found', 'cancelled')),
'active': sum(1 for s in statuses if s in ('downloading', 'searching', 'post_processing')),
diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js
index 78308696..2789f5ea 100644
--- a/webui/static/pages-extra.js
+++ b/webui/static/pages-extra.js
@@ -2488,6 +2488,10 @@ function _adlRenderBatchPanel() {
const hasFailed = batch.failed > 0;
const isTerminal = batch.phase === 'complete' || batch.phase === 'cancelled' || batch.phase === 'error';
const isActive = batch.phase === 'downloading' && batch.active > 0;
+ const isAnalyzing = batch.phase === 'analysis';
+ const analysisTotal = batch.analysis_total || 0;
+ const analysisProcessed = batch.analysis_processed || 0;
+ const analysisPct = analysisTotal > 0 ? Math.round((analysisProcessed / analysisTotal) * 100) : 0;
// Fade progress for completing batches
let fadeStyle = '';
@@ -2511,7 +2515,7 @@ function _adlRenderBatchPanel() {
phaseText = 'Queued';
phaseIcon = '⏳';
} else if (batch.phase === 'analysis') {
- phaseText = 'Analyzing...';
+ phaseText = analysisTotal > 0 ? `Analyzing ${analysisProcessed}/${analysisTotal}...` : 'Analyzing...';
phaseIcon = '';
} else if (batch.phase === 'downloading') {
phaseText = `${batch.completed}/${total} tracks`;
@@ -2610,7 +2614,7 @@ function _adlRenderBatchPanel() {
${tracksHtml}
`;
From 332392f33827adf18c67e11f93e7715c0359f481 Mon Sep 17 00:00:00 2001
From: JohnBaumb <80135794+JohnBaumb@users.noreply.github.com>
Date: Thu, 23 Apr 2026 00:40:33 -0700
Subject: [PATCH 3/7] Improve batch history: thumbnails, source badges,
detailed stats
---
webui/static/pages-extra.js | 36 +++++++++++++++++++++++++++++-------
webui/static/style.css | 8 ++++++++
2 files changed, 37 insertions(+), 7 deletions(-)
diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js
index 2789f5ea..9ea3fc26 100644
--- a/webui/static/pages-extra.js
+++ b/webui/static/pages-extra.js
@@ -2828,10 +2828,19 @@ function _adlRenderBatchHistory() {
list.innerHTML = _adlBatchHistory.map(h => {
const name = _adlEsc(h.playlist_name || 'Unknown');
const downloaded = h.tracks_downloaded || 0;
+ const found = h.tracks_found || 0;
const failed = h.tracks_failed || 0;
const total = h.total_tracks || 0;
- const statsParts = [`${downloaded}/${total}`];
- if (failed > 0) statsParts.push(`${failed} failed`);
+
+ // Build stats: "found/total Xnew Xfailed"
+ const statsParts = [];
+ if (found > 0 || downloaded > 0) {
+ const owned = found - downloaded; // already in library before this sync
+ if (owned > 0) statsParts.push(`${owned}`);
+ if (downloaded > 0) statsParts.push(`${downloaded} new`);
+ }
+ if (failed > 0) statsParts.push(`${failed} failed`);
+ if (statsParts.length === 0) statsParts.push(`0/${total}`);
let dateText = '';
if (h.completed_at) {
@@ -2848,17 +2857,30 @@ function _adlRenderBatchHistory() {
}
}
- const sourceLabel = h.source_page ? `${_adlEsc(h.source_page)}` : '';
+ // Use source (spotify, mirrored, discover, etc.) for badge when available, fall back to source_page
+ const badgeLabel = h.source || h.source_page || '';
+ const sourceLabel = badgeLabel ? `${_adlEsc(badgeLabel)}` : '';
- // Source type color dot
- const sourceColors = { wishlist: '168, 85, 247', sync: '59, 130, 246', album: '16, 185, 129' };
- const dotColor = sourceColors[h.source_page] || '255, 255, 255';
+ // Source type color dot - expanded palette
+ const sourceColors = {
+ wishlist: '168, 85, 247', sync: '59, 130, 246', album: '16, 185, 129',
+ discover: '251, 191, 36', mirrored: '236, 72, 153', spotify: '30, 215, 96',
+ youtube: '255, 0, 0', tidal: '0, 255, 255', deezer: '162, 73, 255',
+ beatport: '148, 252, 19', listenbrainz: '255, 134, 0'
+ };
+ const dotColor = sourceColors[h.source] || sourceColors[h.source_page] || '255, 255, 255';
const histDot = ``;
+ // Optional thumbnail
+ const thumb = h.thumb_url
+ ? `
`
+ : '';
+
return `
${histDot}
+ ${thumb}
${name} ${sourceLabel}
-
${statsParts.join(' ')}
+
${statsParts.join(' · ')}
${dateText}
`;
}).join('');
diff --git a/webui/static/style.css b/webui/static/style.css
index 88ef6c47..89e9a948 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -57119,6 +57119,14 @@ body.reduce-effects *::after {
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
}
+.adl-batch-history-thumb {
+ width: 24px;
+ height: 24px;
+ border-radius: 3px;
+ object-fit: cover;
+ flex-shrink: 0;
+}
+
.adl-batch-history-name {
flex: 1;
font-size: 0.75rem;
From 9b3e65c3d8ca8c0472117ba2a0e854d0112bf4d8 Mon Sep 17 00:00:00 2001
From: JohnBaumb <80135794+JohnBaumb@users.noreply.github.com>
Date: Thu, 23 Apr 2026 00:44:59 -0700
Subject: [PATCH 4/7] Two-line batch history layout with descriptive stats
---
webui/static/pages-extra.js | 23 +++++++++++++++--------
webui/static/style.css | 28 +++++++++++++++++++++-------
2 files changed, 36 insertions(+), 15 deletions(-)
diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js
index 9ea3fc26..73a212b1 100644
--- a/webui/static/pages-extra.js
+++ b/webui/static/pages-extra.js
@@ -2832,15 +2832,17 @@ function _adlRenderBatchHistory() {
const failed = h.tracks_failed || 0;
const total = h.total_tracks || 0;
- // Build stats: "found/total Xnew Xfailed"
+ // Build stats line: "X in library · X downloaded · X failed (of N)"
const statsParts = [];
if (found > 0 || downloaded > 0) {
const owned = found - downloaded; // already in library before this sync
- if (owned > 0) statsParts.push(`${owned}`);
- if (downloaded > 0) statsParts.push(`${downloaded} new`);
+ if (owned > 0) statsParts.push(`${owned} in library`);
+ if (downloaded > 0) statsParts.push(`${downloaded} downloaded`);
}
if (failed > 0) statsParts.push(`${failed} failed`);
- if (statsParts.length === 0) statsParts.push(`0/${total}`);
+ const notFound = total - found - failed;
+ if (notFound > 0) statsParts.push(`${notFound} not found`);
+ if (statsParts.length === 0) statsParts.push(`${total} tracks`);
let dateText = '';
if (h.completed_at) {
@@ -2877,11 +2879,16 @@ function _adlRenderBatchHistory() {
: '';
return `
- ${histDot}
${thumb}
-
${name} ${sourceLabel}
-
${statsParts.join(' · ')}
-
${dateText}
+
+
+ ${histDot}
+ ${name}
+ ${sourceLabel}
+ ${dateText}
+
+
${statsParts.join(' · ')}
+
`;
}).join('');
}
diff --git a/webui/static/style.css b/webui/static/style.css
index 89e9a948..4d7faf14 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -57115,37 +57115,51 @@ body.reduce-effects *::after {
display: flex;
align-items: center;
gap: 8px;
- padding: 6px 0;
+ padding: 7px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
}
.adl-batch-history-thumb {
- width: 24px;
- height: 24px;
+ width: 28px;
+ height: 28px;
border-radius: 3px;
object-fit: cover;
flex-shrink: 0;
}
-.adl-batch-history-name {
+.adl-batch-history-content {
flex: 1;
+ min-width: 0;
+}
+
+.adl-batch-history-row1 {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.adl-batch-history-name {
font-size: 0.75rem;
- color: rgba(255, 255, 255, 0.5);
+ color: rgba(255, 255, 255, 0.55);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ flex: 1;
+ min-width: 0;
}
-.adl-batch-history-stats {
+.adl-batch-history-row2 {
font-size: 0.68rem;
color: rgba(255, 255, 255, 0.3);
- flex-shrink: 0;
+ margin-top: 2px;
+ padding-left: 12px;
}
.adl-batch-history-date {
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.2);
flex-shrink: 0;
+ margin-left: auto;
}
/* ---- Responsive ---- */
From 11b74d7c140d81412a3a5d40e1df0fec900f7bfe Mon Sep 17 00:00:00 2001
From: JohnBaumb <80135794+JohnBaumb@users.noreply.github.com>
Date: Thu, 23 Apr 2026 00:46:54 -0700
Subject: [PATCH 5/7] Widen batch panel to 400px, shorten stat labels
---
webui/static/pages-extra.js | 10 +++++-----
webui/static/style.css | 2 +-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js
index 73a212b1..9e508afb 100644
--- a/webui/static/pages-extra.js
+++ b/webui/static/pages-extra.js
@@ -2832,16 +2832,16 @@ function _adlRenderBatchHistory() {
const failed = h.tracks_failed || 0;
const total = h.total_tracks || 0;
- // Build stats line: "X in library · X downloaded · X failed (of N)"
+ // Build stats line: "X owned · X new · X failed · X missing"
const statsParts = [];
if (found > 0 || downloaded > 0) {
const owned = found - downloaded; // already in library before this sync
- if (owned > 0) statsParts.push(`${owned} in library`);
- if (downloaded > 0) statsParts.push(`${downloaded} downloaded`);
+ if (owned > 0) statsParts.push(`${owned} owned`);
+ if (downloaded > 0) statsParts.push(`${downloaded} new`);
}
- if (failed > 0) statsParts.push(`${failed} failed`);
+ if (failed > 0) statsParts.push(`${failed} failed`);
const notFound = total - found - failed;
- if (notFound > 0) statsParts.push(`${notFound} not found`);
+ if (notFound > 0) statsParts.push(`${notFound} missing`);
if (statsParts.length === 0) statsParts.push(`${total} tracks`);
let dateText = '';
diff --git a/webui/static/style.css b/webui/static/style.css
index 4d7faf14..f8ae0196 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -56736,7 +56736,7 @@ body.reduce-effects *::after {
/* ---- Batch Context Panel ---- */
.adl-batch-panel {
- width: 340px;
+ width: 400px;
flex-shrink: 0;
border-left: 1px solid rgba(255, 255, 255, 0.06);
padding: 0 12px 0 24px;
From d2bd66c0cee0c87c98ed5acb3331c98a94ae3fc3 Mon Sep 17 00:00:00 2001
From: JohnBaumb <80135794+JohnBaumb@users.noreply.github.com>
Date: Thu, 23 Apr 2026 00:55:46 -0700
Subject: [PATCH 6/7] Show server push status in batch history
---
webui/static/pages-extra.js | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js
index 9e508afb..97d60281 100644
--- a/webui/static/pages-extra.js
+++ b/webui/static/pages-extra.js
@@ -2878,6 +2878,20 @@ function _adlRenderBatchHistory() {
? `
`
: '';
+ // Server push status indicator
+ let pushBadge = '';
+ if (h.server_push_status) {
+ const pushIcons = {
+ pending: ['⏳', 'rgba(251,191,36,0.7)', 'Waiting to push to server'],
+ pushing: ['⬆', 'rgba(96,165,250,0.7)', 'Pushing to server...'],
+ success: ['✓', 'rgba(74,222,128,0.7)', 'Pushed to server'],
+ failed: ['✗', 'rgba(239,68,68,0.7)', 'Server push failed'],
+ skipped: ['—', 'rgba(255,255,255,0.2)', 'Server push skipped'],
+ };
+ const [icon, color, tip] = pushIcons[h.server_push_status] || ['?', 'rgba(255,255,255,0.3)', h.server_push_status];
+ pushBadge = ` · ${icon} server`;
+ }
+
return `
${thumb}
@@ -2887,7 +2901,7 @@ function _adlRenderBatchHistory() {
${sourceLabel}
${dateText}
-
${statsParts.join(' · ')}
+
${statsParts.join(' · ')}${pushBadge}
`;
}).join('');
From 735a0ebe58d4f01081b61024f3b7ba0ce34f8844 Mon Sep 17 00:00:00 2001
From: JohnBaumb <80135794+JohnBaumb@users.noreply.github.com>
Date: Thu, 23 Apr 2026 01:48:40 -0700
Subject: [PATCH 7/7] Fix missing album art on downloads page with multi-layer
fallback
---
web_server.py | 87 ++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 76 insertions(+), 11 deletions(-)
diff --git a/web_server.py b/web_server.py
index 00bdce62..f20486bb 100644
--- a/web_server.py
+++ b/web_server.py
@@ -31352,13 +31352,26 @@ def get_all_downloads_unified():
album = str(raw_album) if raw_album else ''
artwork = track_info.get('artwork_url') or track_info.get('image_url') or track_info.get('album_art') or ''
- # Try album images
+ # Try album images (both image_url and images[] formats)
if not artwork:
raw_alb = track_info.get('album')
if isinstance(raw_alb, dict):
- images = raw_alb.get('images') or []
- if images and isinstance(images, list) and len(images) > 0:
- artwork = images[0].get('url', '') if isinstance(images[0], dict) else str(images[0])
+ artwork = raw_alb.get('image_url') or ''
+ if not artwork:
+ images = raw_alb.get('images') or []
+ if images and isinstance(images, list) and len(images) > 0:
+ artwork = images[0].get('url', '') if isinstance(images[0], dict) else str(images[0])
+ # Try spotify_data nested object (wishlist tracks store metadata here)
+ if not artwork:
+ sp_data = track_info.get('spotify_data') or {}
+ if isinstance(sp_data, dict):
+ sp_album = sp_data.get('album') or {}
+ if isinstance(sp_album, dict):
+ artwork = sp_album.get('image_url') or ''
+ if not artwork:
+ sp_imgs = sp_album.get('images') or []
+ if sp_imgs and isinstance(sp_imgs, list) and isinstance(sp_imgs[0], dict):
+ artwork = sp_imgs[0].get('url', '')
status = task.get('status', 'queued')
# Determine download progress percentage
@@ -31408,18 +31421,18 @@ def get_all_downloads_unified():
if items_needing_art:
try:
database = get_database()
- # Batch lookup by track name — metadata cache has deezer/itunes images
- track_names = list({it['title'] for it in items_needing_art if it['title']})
- if track_names:
- placeholders = ','.join('?' for _ in track_names)
- with database._get_connection() as conn:
- cursor = conn.cursor()
+ with database._get_connection() as conn:
+ cursor = conn.cursor()
+ # 1. Batch lookup by track name
+ track_names = list({it['title'] for it in items_needing_art if it['title']})
+ name_to_art = {}
+ if track_names:
+ placeholders = ','.join('?' for _ in track_names)
cursor.execute(f"""
SELECT name, image_url FROM metadata_cache_entities
WHERE entity_type='track' AND name IN ({placeholders})
AND image_url IS NOT NULL AND image_url != ''
""", track_names)
- name_to_art = {}
for row in cursor.fetchall():
if row[0] not in name_to_art:
name_to_art[row[0]] = row[1]
@@ -31427,6 +31440,58 @@ def get_all_downloads_unified():
cached_url = name_to_art.get(it['title'])
if cached_url:
it['artwork'] = cached_url
+
+ # 2. Remaining items: look up by album name (album art is better than nothing)
+ still_needing = [it for it in items_needing_art if not it.get('artwork') and it.get('album')]
+ if still_needing:
+ album_names = list({it['album'] for it in still_needing if it['album']})
+ if album_names:
+ placeholders = ','.join('?' for _ in album_names)
+ cursor.execute(f"""
+ SELECT name, image_url FROM metadata_cache_entities
+ WHERE entity_type='album' AND name IN ({placeholders})
+ AND image_url IS NOT NULL AND image_url != ''
+ """, album_names)
+ album_to_art = {}
+ for row in cursor.fetchall():
+ if row[0] not in album_to_art:
+ album_to_art[row[0]] = row[1]
+ for it in still_needing:
+ cached_url = album_to_art.get(it['album'])
+ if cached_url:
+ it['artwork'] = cached_url
+
+ # 3. Last resort: check matched_downloads_context for items with active downloads
+ final_needing = [it for it in items_needing_art if not it.get('artwork')]
+ if final_needing:
+ with matched_context_lock:
+ for it in final_needing:
+ # Find task in download_tasks to get username/filename for context lookup
+ task_id = it.get('task_id')
+ if not task_id:
+ continue
+ with tasks_lock:
+ task = download_tasks.get(task_id)
+ if not task:
+ continue
+ t_username = task.get('username')
+ t_filename = task.get('filename')
+ if t_username and t_filename:
+ ctx_key = _make_context_key(t_username, t_filename)
+ ctx = matched_downloads_context.get(ctx_key)
+ if ctx:
+ _sp_album = ctx.get('spotify_album') or {}
+ _sp_track = ctx.get('track_info') or {}
+ _sp_images = _sp_album.get('images') or []
+ _art = ''
+ if _sp_images and isinstance(_sp_images[0], dict):
+ _art = _sp_images[0].get('url', '') or ''
+ if not _art:
+ _art = _sp_album.get('image_url') or ''
+ if not _art:
+ _art = _sp_track.get('image_url') or ''
+ if _art:
+ it['artwork'] = _art
except Exception as cache_err:
logger.debug(f"Metadata cache artwork lookup failed: {cache_err}")