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:
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),

View file

@ -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 = {

View file

@ -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];

View file

@ -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 = '<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') {
phaseText = `${batch.completed}/${total} tracks`;
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 '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';
}