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)
This commit is contained in:
JohnBaumb 2026-04-27 14:01:10 -07:00
parent 4317235a24
commit 0ec8b25a17
5 changed files with 75 additions and 13 deletions

View file

@ -339,11 +339,15 @@ class PlaylistSyncService:
if original_track_data: if original_track_data:
spotify_track_data = original_track_data spotify_track_data = original_track_data
else: else:
_img_url = getattr(spotify_track, 'image_url', None) or ''
spotify_track_data = { spotify_track_data = {
'id': spotify_track.id, 'id': spotify_track.id,
'name': spotify_track.name, 'name': spotify_track.name,
'artists': [{'name': a} if isinstance(a, str) else a for a in spotify_track.artists], '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, 'duration_ms': spotify_track.duration_ms,
'popularity': getattr(spotify_track, 'popularity', 0), 'popularity': getattr(spotify_track, 'popularity', 0),
'preview_url': getattr(spotify_track, 'preview_url', None), 'preview_url': getattr(spotify_track, 'preview_url', None),

View file

@ -5950,7 +5950,7 @@ def get_debug_info():
# Active downloads & syncs (use list() snapshots to avoid RuntimeError from concurrent mutation) # Active downloads & syncs (use list() snapshots to avoid RuntimeError from concurrent mutation)
try: 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: except Exception:
active_downloads = 0 active_downloads = 0
active_syncs = 0 active_syncs = 0
@ -28395,6 +28395,13 @@ def _ensure_spotify_track_format(track_info):
album.setdefault('album_type', 'album') album.setdefault('album_type', 'album')
album.setdefault('total_tracks', 0) 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 # Build proper Spotify track structure
spotify_track = { spotify_track = {
'id': track_info.get('id', f"webui_{hash(str(track_info))}"), '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'), 'phase': batch.get('phase', 'unknown'),
'total': len(queue), 'total': len(queue),
'analysis_total': batch.get('analysis_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')), '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')), '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')), '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 = dict(album_raw) # Copy all fields including artists
album_data.setdefault('name', 'Unknown Album') album_data.setdefault('name', 'Unknown Album')
album_data.setdefault('album_type', track_info.get('album_type', '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: else:
_img = track_info.get('image_url') or track_info.get('album_image_url') or ''
album_data = { album_data = {
'name': str(album_raw) if album_raw else 'Unknown Album', '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 = { spotify_track_data = {

View file

@ -7901,13 +7901,17 @@ async function startDiscoverPlaylistSync(playlistType, playlistName) {
} }
return t; return t;
} }
const _coverUrl = track.album_cover_url || track.image_url || '';
return { return {
id: track.spotify_track_id || track.track_id || '', id: track.spotify_track_id || track.track_id || '',
name: track.track_name || track.name || '', name: track.track_name || track.name || '',
artists: [track.artist_name || 'Unknown Artist'], 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, 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; return t;
} }
const _coverUrl = track.album_cover_url || track.image_url || '';
return { return {
id: track.spotify_track_id || track.track_id || '', id: track.spotify_track_id || track.track_id || '',
name: track.track_name || track.name || '', name: track.track_name || track.name || '',
artists: [track.artist_name || 'Unknown Artist'], 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, 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(); const result = await batchResponse.json();
if (result.success) { 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}`); const card = document.getElementById(`discover-sync-card-${playlistType}`);
if (card) { if (card) {
const statusEl = card.querySelector('.discover-sync-status'); const statusEl = card.querySelector('.discover-sync-status');
if (statusEl) { if (statusEl) {
statusEl.className = 'discover-sync-status syncing'; statusEl.className = 'discover-sync-status syncing';
statusEl.textContent = 'Downloading...'; statusEl.textContent = 'Analyzing...';
} }
} }
// Poll the download batch status // Poll the download batch status
@ -9378,6 +9386,28 @@ function pollDiscoverBatchFromTab(playlistType, batchId, playlistName) {
const data = await resp.json(); const data = await resp.json();
const phase = data.phase || data.status; 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') { if (phase === 'complete' || phase === 'error' || phase === 'cancelled') {
clearInterval(pollInterval); clearInterval(pollInterval);
delete discoverSyncPollers[playlistType]; delete discoverSyncPollers[playlistType];

View file

@ -2484,10 +2484,16 @@ function _adlRenderBatchPanel() {
const isFiltered = _adlFilterBatchId === batch.batch_id; const isFiltered = _adlFilterBatchId === batch.batch_id;
const total = batch.total || 1; const total = batch.total || 1;
const done = batch.completed + batch.failed; 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 hasFailed = batch.failed > 0;
const isTerminal = batch.phase === 'complete' || batch.phase === 'cancelled' || batch.phase === 'error'; 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 // Fade progress for completing batches
let fadeStyle = ''; let fadeStyle = '';
@ -2508,8 +2514,13 @@ function _adlRenderBatchPanel() {
let phaseText = ''; let phaseText = '';
let phaseIcon = ''; let phaseIcon = '';
if (batch.phase === 'analysis') { 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 = '<span class="adl-spinner" style="margin-right:4px"></span>'; phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';
} else if (batch.phase === 'queued') {
phaseText = 'Queued';
phaseIcon = '<span style="margin-right:4px">⏳</span>';
} else if (batch.phase === 'downloading') { } else if (batch.phase === 'downloading') {
phaseText = `${batch.completed}/${total} tracks`; phaseText = `${batch.completed}/${total} tracks`;
if (batch.active > 0) phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>'; if (batch.active > 0) phaseIcon = '<span class="adl-spinner" style="margin-right:4px"></span>';

View file

@ -1413,6 +1413,8 @@ function getPhaseText(phase) {
case 'discovered': return 'Discovery Complete'; case 'discovered': return 'Discovery Complete';
case 'syncing': return 'Syncing...'; case 'syncing': return 'Syncing...';
case 'sync_complete': return 'Sync Complete'; case 'sync_complete': return 'Sync Complete';
case 'analysis': return 'Analyzing...';
case 'queued': return 'Queued';
case 'downloading': return 'Downloading...'; case 'downloading': return 'Downloading...';
case 'download_complete': return 'Download Complete'; case 'download_complete': return 'Download Complete';
default: return phase; default: return phase;
@ -1422,7 +1424,8 @@ function getPhaseText(phase) {
function getPhaseColor(phase) { function getPhaseColor(phase) {
switch (phase) { switch (phase) {
case 'fresh': return '#999'; 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))'; case 'discovered': case 'sync_complete': case 'download_complete': return 'rgb(var(--accent-rgb))';
default: return '#999'; default: return '#999';
} }