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'; }