From 0ec8b25a17c7d4464b8fc48a8a8e4991d853e264 Mon Sep 17 00:00:00 2001
From: JohnBaumb <80135794+JohnBaumb@users.noreply.github.com>
Date: Mon, 27 Apr 2026 14:01:10 -0700
Subject: [PATCH] fix: cover art loss, sidebar badge, analysis UI, and phase
text/color
- Recover image_url into album.images across all sync paths (ensure_spotify_track_format, cancel V1, sync_service fallback, discover.js wishlist add)
- Include analysis, downloading, and queued phases in sidebar badge count
- Add analysis_processed to batch summary API so downloads page shows analysis progress
- Show Analyzing X/Y with correct progress bar in batch cards (pages-extra.js)
- Add analysis and queued cases to getPhaseText/getPhaseColor (sync-spotify.js)
---
services/sync_service.py | 6 +++++-
web_server.py | 18 ++++++++++++++--
webui/static/discover.js | 42 ++++++++++++++++++++++++++++++------
webui/static/pages-extra.js | 17 ++++++++++++---
webui/static/sync-spotify.js | 5 ++++-
5 files changed, 75 insertions(+), 13 deletions(-)
diff --git a/services/sync_service.py b/services/sync_service.py
index 4582a5cf..7ffbc62c 100644
--- a/services/sync_service.py
+++ b/services/sync_service.py
@@ -339,11 +339,15 @@ class PlaylistSyncService:
if original_track_data:
spotify_track_data = original_track_data
else:
+ _img_url = getattr(spotify_track, 'image_url', None) or ''
spotify_track_data = {
'id': spotify_track.id,
'name': spotify_track.name,
'artists': [{'name': a} if isinstance(a, str) else a for a in spotify_track.artists],
- 'album': {'name': spotify_track.album},
+ 'album': {
+ 'name': spotify_track.album,
+ 'images': [{'url': _img_url}] if _img_url else []
+ },
'duration_ms': spotify_track.duration_ms,
'popularity': getattr(spotify_track, 'popularity', 0),
'preview_url': getattr(spotify_track, 'preview_url', None),
diff --git a/web_server.py b/web_server.py
index 08a65f9c..473240b7 100644
--- a/web_server.py
+++ b/web_server.py
@@ -5950,7 +5950,7 @@ def get_debug_info():
# Active downloads & syncs (use list() snapshots to avoid RuntimeError from concurrent mutation)
try:
- active_downloads = len([bid for bid, bd in list(download_batches.items()) if bd.get('phase') == 'downloading'])
+ active_downloads = len([bid for bid, bd in list(download_batches.items()) if bd.get('phase') in ('analysis', 'downloading', 'queued')])
except Exception:
active_downloads = 0
active_syncs = 0
@@ -28395,6 +28395,13 @@ def _ensure_spotify_track_format(track_info):
album.setdefault('album_type', 'album')
album.setdefault('total_tracks', 0)
+ # Recover cover art from top-level image_url / album_cover_url when album
+ # was a plain string (discover fallback path) and images ended up empty
+ if not album.get('images'):
+ _img_url = track_info.get('image_url') or track_info.get('album_cover_url') or ''
+ if _img_url:
+ album['images'] = [{'url': _img_url}]
+
# Build proper Spotify track structure
spotify_track = {
'id': track_info.get('id', f"webui_{hash(str(track_info))}"),
@@ -31637,6 +31644,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')),
@@ -31822,10 +31830,16 @@ def cancel_download_task():
album_data = dict(album_raw) # Copy all fields including artists
album_data.setdefault('name', 'Unknown Album')
album_data.setdefault('album_type', track_info.get('album_type', 'album'))
+ # Ensure images are present (recover from top-level image_url if needed)
+ if 'images' not in album_data:
+ _img = track_info.get('image_url') or track_info.get('album_image_url') or ''
+ album_data['images'] = [{'url': _img}] if _img else []
else:
+ _img = track_info.get('image_url') or track_info.get('album_image_url') or ''
album_data = {
'name': str(album_raw) if album_raw else 'Unknown Album',
- 'album_type': track_info.get('album_type', 'album')
+ 'album_type': track_info.get('album_type', 'album'),
+ 'images': [{'url': _img}] if _img else []
}
spotify_track_data = {
diff --git a/webui/static/discover.js b/webui/static/discover.js
index 8ecbf6de..828f1881 100644
--- a/webui/static/discover.js
+++ b/webui/static/discover.js
@@ -7901,13 +7901,17 @@ async function startDiscoverPlaylistSync(playlistType, playlistName) {
}
return t;
}
+ const _coverUrl = track.album_cover_url || track.image_url || '';
return {
id: track.spotify_track_id || track.track_id || '',
name: track.track_name || track.name || '',
artists: [track.artist_name || 'Unknown Artist'],
- album: track.album_name || '',
+ album: {
+ name: track.album_name || '',
+ images: _coverUrl ? [{ url: _coverUrl }] : []
+ },
duration_ms: track.duration_ms || 0,
- image_url: track.album_cover_url || track.image_url || ''
+ image_url: _coverUrl
};
});
@@ -9260,13 +9264,17 @@ async function _doSyncDiscoverPlaylist(playlistType, playlistName) {
}
return t;
}
+ const _coverUrl = track.album_cover_url || track.image_url || '';
return {
id: track.spotify_track_id || track.track_id || '',
name: track.track_name || track.name || '',
artists: [track.artist_name || 'Unknown Artist'],
- album: track.album_name || '',
+ album: {
+ name: track.album_name || '',
+ images: _coverUrl ? [{ url: _coverUrl }] : []
+ },
duration_ms: track.duration_ms || 0,
- image_url: track.album_cover_url || track.image_url || ''
+ image_url: _coverUrl
};
});
@@ -9287,13 +9295,13 @@ async function _doSyncDiscoverPlaylist(playlistType, playlistName) {
const result = await batchResponse.json();
if (result.success) {
- showToast(`Downloading ${playlistName} (${syncTracks.length} tracks)...`, 'info');
+ showToast(`${playlistName}: analyzing ${syncTracks.length} tracks...`, 'info');
const card = document.getElementById(`discover-sync-card-${playlistType}`);
if (card) {
const statusEl = card.querySelector('.discover-sync-status');
if (statusEl) {
statusEl.className = 'discover-sync-status syncing';
- statusEl.textContent = 'Downloading...';
+ statusEl.textContent = 'Analyzing...';
}
}
// Poll the download batch status
@@ -9378,6 +9386,28 @@ function pollDiscoverBatchFromTab(playlistType, batchId, playlistName) {
const data = await resp.json();
const phase = data.phase || data.status;
+ // Update card status text during intermediate phases
+ const card = document.getElementById(`discover-sync-card-${playlistType}`);
+ if (card && phase !== 'complete' && phase !== 'error' && phase !== 'cancelled') {
+ const statusEl = card.querySelector('.discover-sync-status');
+ if (statusEl) {
+ statusEl.className = 'discover-sync-status syncing';
+ if (phase === 'analyzing' || phase === 'analysis') {
+ const analysisResults = data.analysis_results || [];
+ const total = data.total || data.track_count || 0;
+ const analyzed = analysisResults.length;
+ statusEl.textContent = total > 0 ? `Analyzing ${analyzed}/${total}` : 'Analyzing...';
+ } else if (phase === 'downloading') {
+ const tasks = data.tasks || [];
+ const completed = tasks.filter(t => t.status === 'completed').length;
+ const total = tasks.length;
+ statusEl.textContent = total > 0 ? `Downloading ${completed}/${total}` : 'Downloading...';
+ } else {
+ statusEl.textContent = phase === 'idle' ? 'Starting...' : `${phase.charAt(0).toUpperCase() + phase.slice(1)}...`;
+ }
+ }
+ }
+
if (phase === 'complete' || phase === 'error' || phase === 'cancelled') {
clearInterval(pollInterval);
delete discoverSyncPollers[playlistType];
diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js
index a8722975..1c52ec83 100644
--- a/webui/static/pages-extra.js
+++ b/webui/static/pages-extra.js
@@ -2484,10 +2484,16 @@ function _adlRenderBatchPanel() {
const isFiltered = _adlFilterBatchId === batch.batch_id;
const total = batch.total || 1;
const done = batch.completed + batch.failed;
- const pct = Math.round((done / total) * 100);
+ let pct;
+ if (batch.phase === 'analysis') {
+ const at = batch.analysis_total || 1;
+ pct = Math.round(((batch.analysis_processed || 0) / at) * 100);
+ } else {
+ pct = Math.round((done / total) * 100);
+ }
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 isActive = (batch.phase === 'downloading' && batch.active > 0) || batch.phase === 'analysis';
// Fade progress for completing batches
let fadeStyle = '';
@@ -2508,8 +2514,13 @@ function _adlRenderBatchPanel() {
let phaseText = '';
let phaseIcon = '';
if (batch.phase === 'analysis') {
- phaseText = 'Analyzing...';
+ const ap = batch.analysis_processed || 0;
+ const at = batch.analysis_total || 0;
+ phaseText = at > 0 ? `Analyzing ${ap}/${at}` : 'Analyzing...';
phaseIcon = '';
+ } else if (batch.phase === 'queued') {
+ phaseText = 'Queued';
+ phaseIcon = '⏳';
} else if (batch.phase === 'downloading') {
phaseText = `${batch.completed}/${total} tracks`;
if (batch.active > 0) phaseIcon = '';
diff --git a/webui/static/sync-spotify.js b/webui/static/sync-spotify.js
index 95e592c2..78e0d59e 100644
--- a/webui/static/sync-spotify.js
+++ b/webui/static/sync-spotify.js
@@ -1413,6 +1413,8 @@ function getPhaseText(phase) {
case 'discovered': return 'Discovery Complete';
case 'syncing': return 'Syncing...';
case 'sync_complete': return 'Sync Complete';
+ case 'analysis': return 'Analyzing...';
+ case 'queued': return 'Queued';
case 'downloading': return 'Downloading...';
case 'download_complete': return 'Download Complete';
default: return phase;
@@ -1422,7 +1424,8 @@ function getPhaseText(phase) {
function getPhaseColor(phase) {
switch (phase) {
case 'fresh': return '#999';
- case 'discovering': case 'syncing': case 'downloading': return '#ffa500';
+ case 'discovering': case 'syncing': case 'downloading': case 'analysis': return '#ffa500';
+ case 'queued': return '#6b9fff';
case 'discovered': case 'sync_complete': case 'download_complete': return 'rgb(var(--accent-rgb))';
default: return '#999';
}