optimize album and cover art data

This commit is contained in:
Broque Thomas 2025-11-09 20:33:25 -08:00
parent 62ad5a9cd9
commit 5ffce6bb59
3 changed files with 232 additions and 111 deletions

View file

@ -8707,10 +8707,25 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
download_batches[batch_id]['phase'] = 'downloading' download_batches[batch_id]['phase'] = 'downloading'
# Get batch album context (if this is an artist album download)
batch = download_batches[batch_id]
batch_album_context = batch.get('album_context')
batch_artist_context = batch.get('artist_context')
batch_is_album = batch.get('is_album_download', False)
for res in missing_tracks: for res in missing_tracks:
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
track_info = res['track'].copy()
# Add explicit album context to track_info for artist album downloads
if batch_is_album and batch_album_context and batch_artist_context:
track_info['_explicit_album_context'] = batch_album_context
track_info['_explicit_artist_context'] = batch_artist_context
track_info['_is_explicit_album_download'] = True
print(f"🎵 [Task Creation] Added explicit album context for: {track_info.get('name')}")
download_tasks[task_id] = { download_tasks[task_id] = {
'status': 'pending', 'track_info': res['track'], 'status': 'pending', 'track_info': track_info,
'playlist_id': playlist_id, 'batch_id': batch_id, 'playlist_id': playlist_id, 'batch_id': batch_id,
'track_index': res['track_index'], 'retry_count': 0, 'track_index': res['track_index'], 'retry_count': 0,
'cached_candidates': [], 'used_sources': set(), 'cached_candidates': [], 'used_sources': set(),
@ -9313,10 +9328,49 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
try: try:
# Update task status to downloading # Update task status to downloading
_update_task_status(task_id, 'downloading') _update_task_status(task_id, 'downloading')
# Prepare download (using existing infrastructure) # Prepare download - check if we have explicit album context from artist page
spotify_artist_context = {'id': 'from_sync_modal', 'name': track.artists[0] if track.artists else 'Unknown', 'genres': []} track_info = None
spotify_album_context = {'id': 'from_sync_modal', 'name': track.album, 'release_date': '', 'image_url': None} with tasks_lock:
if task_id in download_tasks:
track_info = download_tasks[task_id].get('track_info', {})
# Use explicit album/artist context if available (from artist album downloads)
has_explicit_context = track_info and track_info.get('_is_explicit_album_download', False)
if has_explicit_context:
# Use the real Spotify album/artist data from the UI
explicit_album = track_info.get('_explicit_album_context', {})
explicit_artist = track_info.get('_explicit_artist_context', {})
spotify_artist_context = {
'id': explicit_artist.get('id', 'explicit_artist'),
'name': explicit_artist.get('name', track.artists[0] if track.artists else 'Unknown'),
'genres': explicit_artist.get('genres', [])
}
# Handle both image_url formats (direct string or images array)
album_image_url = None
if explicit_album.get('image_url'):
# Backend API returns image_url as direct string
album_image_url = explicit_album.get('image_url')
elif explicit_album.get('images'):
# Fallback: images array format from Spotify API
album_image_url = explicit_album.get('images', [{}])[0].get('url')
spotify_album_context = {
'id': explicit_album.get('id', 'explicit_album'),
'name': explicit_album.get('name', track.album),
'release_date': explicit_album.get('release_date', ''),
'image_url': album_image_url,
'total_tracks': explicit_album.get('total_tracks', 0),
'album_type': explicit_album.get('album_type', 'album')
}
print(f"🎵 [Explicit Context] Using real album data: '{spotify_album_context['name']}' ({spotify_album_context['album_type']})")
else:
# Fallback to generic context for playlists/wishlists
spotify_artist_context = {'id': 'from_sync_modal', 'name': track.artists[0] if track.artists else 'Unknown', 'genres': []}
spotify_album_context = {'id': 'from_sync_modal', 'name': track.album, 'release_date': '', 'image_url': None}
download_payload = candidate.__dict__ download_payload = candidate.__dict__
username = download_payload.get('username') username = download_payload.get('username')
@ -9377,13 +9431,19 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
enhanced_payload['track_number'] = 1 enhanced_payload['track_number'] = 1
print(f"⚠️ [Context] No track.id available, using fallback track_number: 1") print(f"⚠️ [Context] No track.id available, using fallback track_number: 1")
# Determine if this should be treated as album download based on clean data # Determine if this should be treated as album download
is_album_context = ( # First check if we have explicit album context from artist page
track.album and if has_explicit_context:
track.album.strip() and is_album_context = True
track.album != "Unknown Album" and print(f"✅ [Context] Using explicit album context flag from artist page")
track.album.lower() != track.name.lower() # Album different from track else:
) # Fall back to guessing based on clean data
is_album_context = (
track.album and
track.album.strip() and
track.album != "Unknown Album" and
track.album.lower() != track.name.lower() # Album different from track
)
else: else:
# Fallback to original data # Fallback to original data
enhanced_payload['spotify_clean_title'] = enhanced_payload.get('title', '') enhanced_payload['spotify_clean_title'] = enhanced_payload.get('title', '')
@ -10480,9 +10540,19 @@ def start_missing_tracks_process(playlist_id):
playlist_name = data.get('playlist_name', 'Unknown Playlist') playlist_name = data.get('playlist_name', 'Unknown Playlist')
force_download_all = data.get('force_download_all', False) force_download_all = data.get('force_download_all', False)
# Get album/artist context for artist album downloads
is_album_download = data.get('is_album_download', False)
album_context = data.get('album_context', None)
artist_context = data.get('artist_context', None)
if not tracks: if not tracks:
return jsonify({"success": False, "error": "No tracks provided"}), 400 return jsonify({"success": False, "error": "No tracks provided"}), 400
# Log album context if provided
if is_album_download and album_context and artist_context:
print(f"🎵 [Artist Album] Received album context: '{album_context.get('name')}' by '{artist_context.get('name')}' ({album_context.get('album_type', 'album')})")
print(f" Release: {album_context.get('release_date', 'Unknown')}, Tracks: {album_context.get('total_tracks', len(tracks))}")
# Limit concurrent analysis processes to prevent resource exhaustion # Limit concurrent analysis processes to prevent resource exhaustion
with tasks_lock: with tasks_lock:
active_analysis_count = sum(1 for batch in download_batches.values() active_analysis_count = sum(1 for batch in download_batches.values()
@ -10510,7 +10580,11 @@ def start_missing_tracks_process(playlist_id):
'analysis_total': len(tracks), 'analysis_total': len(tracks),
'analysis_processed': 0, 'analysis_processed': 0,
'analysis_results': [], 'analysis_results': [],
'force_download_all': force_download_all # Pass the force flag to the batch 'force_download_all': force_download_all, # Pass the force flag to the batch
# Album context for artist album downloads (explicit folder structure)
'is_album_download': is_album_download,
'album_context': album_context,
'artist_context': artist_context
} }
# Link YouTube playlist to download process if this is a YouTube playlist # Link YouTube playlist to download process if this is a YouTube playlist

View file

@ -4536,14 +4536,28 @@ async function startMissingTracksProcess(playlistId) {
forceToggleContainer.style.display = 'none'; forceToggleContainer.style.display = 'none';
} }
// Prepare request body - add album/artist context for artist album downloads
const requestBody = {
tracks: process.tracks,
force_download_all: forceDownloadAll
};
// If this is an artist album download, use album name and include full context
if (playlistId.startsWith('artist_album_')) {
requestBody.playlist_name = process.album?.name || process.playlist.name;
requestBody.is_album_download = true;
requestBody.album_context = process.album; // Full Spotify album object
requestBody.artist_context = process.artist; // Full Spotify artist object
console.log(`🎵 [Artist Album] Sending album context: ${process.album?.name} by ${process.artist?.name}`);
} else {
// For playlists/wishlists, use the virtual playlist name
requestBody.playlist_name = process.playlist.name;
}
const response = await fetch(`/api/playlists/${playlistId}/start-missing-process`, { const response = await fetch(`/api/playlists/${playlistId}/start-missing-process`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify(requestBody)
tracks: process.tracks,
playlist_name: process.playlist.name,
force_download_all: forceDownloadAll
})
}); });
const data = await response.json(); const data = await response.json();
@ -16616,19 +16630,27 @@ async function createArtistAlbumVirtualPlaylist(album, albumType) {
} }
const data = await response.json(); const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) { if (!data.success || !data.tracks || data.tracks.length === 0) {
throw new Error('No tracks found for this album'); throw new Error('No tracks found for this album');
} }
console.log(`✅ Loaded ${data.tracks.length} tracks for ${album.name}`); console.log(`✅ Loaded ${data.tracks.length} tracks`);
console.log(`📊 [DEBUG] Backend album data:`, data.album);
console.log(`📊 [DEBUG] Album name from backend:`, data.album?.name);
console.log(`📊 [DEBUG] Original album param:`, album);
// Use album data from API response (has complete data including images array)
const fullAlbumData = data.album;
// Format playlist name with artist and album info // Format playlist name with artist and album info
const playlistName = `[${artist.name}] ${album.name}`; const playlistName = `[${artist.name}] ${fullAlbumData.name}`;
console.log(`📊 [DEBUG] Playlist name created:`, playlistName);
// Open download missing tracks modal with formatted tracks // Open download missing tracks modal with formatted tracks
// Pass false for showLoadingOverlay since we already have one from handleArtistAlbumClick // Pass false for showLoadingOverlay since we already have one from handleArtistAlbumClick
await openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlistName, data.tracks, album, artist, false); // Use fullAlbumData from API response instead of album parameter
await openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlistName, data.tracks, fullAlbumData, artist, false);
// Track this download for artist bubble management // Track this download for artist bubble management
registerArtistDownload(artist, album, virtualPlaylistId, albumType); registerArtistDownload(artist, album, virtualPlaylistId, albumType);

View file

@ -36,24 +36,26 @@ body {
.sidebar { .sidebar {
width: 240px; width: 240px;
/* Premium glassmorphic foundation */ /* Apple-style liquid glassmorphic foundation */
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(20, 20, 20, 0.95) 0%, rgba(20, 20, 20, 0.65) 0%,
rgba(12, 12, 12, 0.98) 100%); rgba(12, 12, 12, 0.75) 100%);
backdrop-filter: blur(20px) saturate(1.2); backdrop-filter: blur(40px) saturate(1.8);
-webkit-backdrop-filter: blur(40px) saturate(1.8);
/* Enhanced borders */ /* Soft translucent borders */
border-right: 1px solid rgba(255, 255, 255, 0.12); border-right: 1px solid rgba(255, 255, 255, 0.08);
border-top: 1px solid rgba(255, 255, 255, 0.18); border-top: 1px solid rgba(255, 255, 255, 0.12);
border-top-right-radius: 20px; border-top-right-radius: 24px;
border-bottom-right-radius: 20px; border-bottom-right-radius: 24px;
/* Premium shadow effect */ /* Soft floating shadow with inner glow */
box-shadow: box-shadow:
0 20px 60px rgba(0, 0, 0, 0.6), 0 8px 32px rgba(0, 0, 0, 0.3),
0 8px 32px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.2),
0 0 40px rgba(29, 185, 84, 0.1), 0 0 60px rgba(29, 185, 84, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.15); inset 0 1px 0 rgba(255, 255, 255, 0.1),
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -62,7 +64,7 @@ body {
/* Custom scrollbar styling */ /* Custom scrollbar styling */
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgba(29, 185, 84, 0.5) rgba(255, 255, 255, 0.05); scrollbar-color: rgba(29, 185, 84, 0.4) rgba(255, 255, 255, 0.03);
} }
/* Sidebar scrollbar webkit styling */ /* Sidebar scrollbar webkit styling */
@ -156,45 +158,48 @@ body {
} }
.nav-button:hover { .nav-button:hover {
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.06) 0%,
rgba(255, 255, 255, 0.04) 100%); rgba(255, 255, 255, 0.03) 100%);
border: 1px solid rgba(255, 255, 255, 0.12); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
transform: translateX(4px); transform: translateX(4px);
box-shadow: box-shadow:
0 4px 16px rgba(0, 0, 0, 0.3), 0 4px 12px rgba(0, 0, 0, 0.2),
0 2px 8px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08);
inset 0 1px 0 rgba(255, 255, 255, 0.1);
} }
.nav-button.active { .nav-button.active {
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(29, 185, 84, 0.20) 0%, rgba(29, 185, 84, 0.16) 0%,
rgba(29, 185, 84, 0.15) 50%, rgba(29, 185, 84, 0.12) 50%,
rgba(29, 185, 84, 0.10) 100%); rgba(29, 185, 84, 0.08) 100%);
border: 1px solid rgba(29, 185, 84, 0.3); backdrop-filter: blur(20px) saturate(1.6);
-webkit-backdrop-filter: blur(20px) saturate(1.6);
border: 1px solid rgba(29, 185, 84, 0.25);
transform: translateX(6px); transform: translateX(6px);
box-shadow: box-shadow:
0 6px 20px rgba(29, 185, 84, 0.4), 0 4px 16px rgba(29, 185, 84, 0.25),
0 4px 12px rgba(29, 185, 84, 0.3), 0 2px 8px rgba(0, 0, 0, 0.2),
0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.15), inset -1px 0 0 rgba(29, 185, 84, 0.6);
inset -1px 0 0 rgba(29, 185, 84, 0.8);
} }
.nav-button.active:hover { .nav-button.active:hover {
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(29, 185, 84, 0.28) 0%, rgba(29, 185, 84, 0.22) 0%,
rgba(29, 185, 84, 0.22) 50%, rgba(29, 185, 84, 0.18) 50%,
rgba(29, 185, 84, 0.15) 100%); rgba(29, 185, 84, 0.12) 100%);
border: 1px solid rgba(29, 185, 84, 0.4); backdrop-filter: blur(20px) saturate(1.8);
-webkit-backdrop-filter: blur(20px) saturate(1.8);
border: 1px solid rgba(29, 185, 84, 0.35);
transform: translateX(8px); transform: translateX(8px);
box-shadow: box-shadow:
0 8px 24px rgba(29, 185, 84, 0.5), 0 6px 20px rgba(29, 185, 84, 0.3),
0 6px 16px rgba(29, 185, 84, 0.4), 0 3px 10px rgba(0, 0, 0, 0.2),
0 3px 8px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.2), inset -1px 0 0 rgba(29, 185, 84, 0.7);
inset -1px 0 0 rgba(29, 185, 84, 0.9);
} }
.nav-icon { .nav-icon {
@ -205,29 +210,33 @@ body {
justify-content: center; justify-content: center;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
border-radius: 14px; border-radius: 10px;
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(255, 255, 255, 0.10) 0%, rgba(255, 255, 255, 0.08) 0%,
rgba(255, 255, 255, 0.06) 100%); rgba(255, 255, 255, 0.04) 100%);
border: 1px solid rgba(255, 255, 255, 0.08); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: box-shadow:
0 2px 6px rgba(0, 0, 0, 0.2), 0 2px 4px rgba(0, 0, 0, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.1); inset 0 1px 0 rgba(255, 255, 255, 0.08);
} }
.nav-button.active .nav-icon { .nav-button.active .nav-icon {
color: #1ed760; color: #1ed760;
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(29, 185, 84, 0.30) 0%, rgba(29, 185, 84, 0.25) 0%,
rgba(30, 215, 96, 0.25) 100%); rgba(30, 215, 96, 0.20) 100%);
border: 1px solid rgba(29, 185, 84, 0.4); backdrop-filter: blur(10px) saturate(1.6);
-webkit-backdrop-filter: blur(10px) saturate(1.6);
border: 1px solid rgba(29, 185, 84, 0.3);
font-weight: 700; font-weight: 700;
box-shadow: box-shadow:
0 4px 12px rgba(29, 185, 84, 0.4), 0 3px 8px rgba(29, 185, 84, 0.3),
0 2px 6px rgba(29, 185, 84, 0.3), 0 1px 4px rgba(29, 185, 84, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2); inset 0 1px 0 rgba(255, 255, 255, 0.15);
} }
.nav-text { .nav-text {
@ -882,11 +891,12 @@ body {
.main-content { .main-content {
flex: 1; flex: 1;
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(12, 12, 12, 0.95) 0%, rgba(12, 12, 12, 0.6) 0%,
rgba(16, 16, 16, 0.98) 50%, rgba(16, 16, 16, 0.7) 50%,
rgba(8, 8, 8, 0.95) 100%); rgba(8, 8, 8, 0.6) 100%);
backdrop-filter: blur(20px) saturate(1.2); backdrop-filter: blur(40px) saturate(1.8);
-webkit-backdrop-filter: blur(40px) saturate(1.8);
overflow: auto; overflow: auto;
} }
@ -979,11 +989,26 @@ body {
} }
.stat-card { .stat-card {
background: linear-gradient(135deg, rgba(29, 185, 84, 0.1) 0%, rgba(255, 255, 255, 0.02) 100%); background: linear-gradient(135deg, rgba(29, 185, 84, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%);
border: 1px solid rgba(29, 185, 84, 0.2); backdrop-filter: blur(20px) saturate(1.5);
border-radius: 12px; -webkit-backdrop-filter: blur(20px) saturate(1.5);
border: 1px solid rgba(29, 185, 84, 0.15);
border-radius: 16px;
padding: 24px; padding: 24px;
text-align: center; text-align: center;
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.3),
0 0 20px rgba(29, 185, 84, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
border-color: rgba(29, 185, 84, 0.25);
} }
.stat-value { .stat-value {
@ -3684,24 +3709,24 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 25px; /* Spacing between sections */ gap: 25px; /* Spacing between sections */
padding: 28px 24px 30px 24px; /* Match modal padding */ padding: 28px 24px 30px 24px;
/* Enhanced glassmorphic foundation matching modal */ /* Apple-style liquid glassmorphic foundation */
background: linear-gradient(135deg, background: linear-gradient(135deg,
rgba(20, 20, 20, 0.85) 0%, rgba(20, 20, 20, 0.55) 0%,
rgba(12, 12, 12, 0.92) 100%); rgba(12, 12, 12, 0.65) 100%);
backdrop-filter: blur(20px) saturate(1.2); backdrop-filter: blur(40px) saturate(1.8);
border-radius: 20px; -webkit-backdrop-filter: blur(40px) saturate(1.8);
border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 24px;
border-top: 1px solid rgba(255, 255, 255, 0.18); border: 1px solid rgba(255, 255, 255, 0.08);
border-top: 1px solid rgba(255, 255, 255, 0.12);
margin: 20px; margin: 20px;
/* Premium shadow effect matching modal */ /* Soft floating shadow */
box-shadow: box-shadow:
0 20px 60px rgba(0, 0, 0, 0.6), 0 8px 32px rgba(0, 0, 0, 0.3),
0 8px 32px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.2),
0 0 40px rgba(29, 185, 84, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.08);
inset 0 1px 0 rgba(255, 255, 255, 0.15);
} }
.dashboard-section { .dashboard-section {