From 5ffce6bb59559ef1b57737be2952bfe5749e6da5 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Sun, 9 Nov 2025 20:33:25 -0800 Subject: [PATCH] optimize album and cover art data --- web_server.py | 100 ++++++++++++++++++--- webui/static/script.js | 46 +++++++--- webui/static/style.css | 197 +++++++++++++++++++++++------------------ 3 files changed, 232 insertions(+), 111 deletions(-) diff --git a/web_server.py b/web_server.py index 9a3edf8d..e875e76b 100644 --- a/web_server.py +++ b/web_server.py @@ -8707,10 +8707,25 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json): 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: 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] = { - 'status': 'pending', 'track_info': res['track'], + 'status': 'pending', 'track_info': track_info, 'playlist_id': playlist_id, 'batch_id': batch_id, 'track_index': res['track_index'], 'retry_count': 0, 'cached_candidates': [], 'used_sources': set(), @@ -9313,10 +9328,49 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None) try: # Update task status to downloading _update_task_status(task_id, 'downloading') - - # Prepare download (using existing infrastructure) - 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} + + # Prepare download - check if we have explicit album context from artist page + track_info = 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__ 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 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 - 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 - ) + # Determine if this should be treated as album download + # First check if we have explicit album context from artist page + if has_explicit_context: + is_album_context = True + print(f"✅ [Context] Using explicit album context flag from artist page") + 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: # Fallback to original data 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') 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: 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 with tasks_lock: 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_processed': 0, '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 diff --git a/webui/static/script.js b/webui/static/script.js index 85bfb24c..cd862cbb 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -4536,14 +4536,28 @@ async function startMissingTracksProcess(playlistId) { 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`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - tracks: process.tracks, - playlist_name: process.playlist.name, - force_download_all: forceDownloadAll - }) + body: JSON.stringify(requestBody) }); const data = await response.json(); @@ -16616,19 +16630,27 @@ async function createArtistAlbumVirtualPlaylist(album, albumType) { } const data = await response.json(); - + if (!data.success || !data.tracks || data.tracks.length === 0) { 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 - 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 // 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 registerArtistDownload(artist, album, virtualPlaylistId, albumType); diff --git a/webui/static/style.css b/webui/static/style.css index 14d6c506..f4aff17c 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -36,24 +36,26 @@ body { .sidebar { width: 240px; - /* Premium glassmorphic foundation */ + /* Apple-style liquid glassmorphic foundation */ background: linear-gradient(135deg, - rgba(20, 20, 20, 0.95) 0%, - rgba(12, 12, 12, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); + rgba(20, 20, 20, 0.65) 0%, + rgba(12, 12, 12, 0.75) 100%); + backdrop-filter: blur(40px) saturate(1.8); + -webkit-backdrop-filter: blur(40px) saturate(1.8); - /* Enhanced borders */ - border-right: 1px solid rgba(255, 255, 255, 0.12); - border-top: 1px solid rgba(255, 255, 255, 0.18); - border-top-right-radius: 20px; - border-bottom-right-radius: 20px; + /* Soft translucent borders */ + border-right: 1px solid rgba(255, 255, 255, 0.08); + border-top: 1px solid rgba(255, 255, 255, 0.12); + border-top-right-radius: 24px; + border-bottom-right-radius: 24px; - /* Premium shadow effect */ + /* Soft floating shadow with inner glow */ box-shadow: - 0 20px 60px rgba(0, 0, 0, 0.6), - 0 8px 32px rgba(0, 0, 0, 0.4), - 0 0 40px rgba(29, 185, 84, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.15); + 0 8px 32px rgba(0, 0, 0, 0.3), + 0 4px 16px rgba(0, 0, 0, 0.2), + 0 0 60px rgba(29, 185, 84, 0.06), + inset 0 1px 0 rgba(255, 255, 255, 0.1), + inset 0 -1px 0 rgba(0, 0, 0, 0.2); display: flex; flex-direction: column; @@ -62,7 +64,7 @@ body { /* Custom scrollbar styling */ 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 */ @@ -156,45 +158,48 @@ body { } .nav-button:hover { - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.08) 0%, - rgba(255, 255, 255, 0.04) 100%); - border: 1px solid rgba(255, 255, 255, 0.12); + background: linear-gradient(135deg, + rgba(255, 255, 255, 0.06) 0%, + rgba(255, 255, 255, 0.03) 100%); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); transform: translateX(4px); - box-shadow: - 0 4px 16px rgba(0, 0, 0, 0.3), - 0 2px 8px rgba(0, 0, 0, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: + 0 4px 12px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.08); } .nav-button.active { - background: linear-gradient(135deg, - rgba(29, 185, 84, 0.20) 0%, - rgba(29, 185, 84, 0.15) 50%, - rgba(29, 185, 84, 0.10) 100%); - border: 1px solid rgba(29, 185, 84, 0.3); + background: linear-gradient(135deg, + rgba(29, 185, 84, 0.16) 0%, + rgba(29, 185, 84, 0.12) 50%, + rgba(29, 185, 84, 0.08) 100%); + 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); - box-shadow: - 0 6px 20px rgba(29, 185, 84, 0.4), - 0 4px 12px rgba(29, 185, 84, 0.3), - 0 2px 6px rgba(0, 0, 0, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.15), - inset -1px 0 0 rgba(29, 185, 84, 0.8); + box-shadow: + 0 4px 16px rgba(29, 185, 84, 0.25), + 0 2px 8px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.12), + inset -1px 0 0 rgba(29, 185, 84, 0.6); } .nav-button.active:hover { - background: linear-gradient(135deg, - rgba(29, 185, 84, 0.28) 0%, - rgba(29, 185, 84, 0.22) 50%, - rgba(29, 185, 84, 0.15) 100%); - border: 1px solid rgba(29, 185, 84, 0.4); + background: linear-gradient(135deg, + rgba(29, 185, 84, 0.22) 0%, + rgba(29, 185, 84, 0.18) 50%, + rgba(29, 185, 84, 0.12) 100%); + 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); - box-shadow: - 0 8px 24px rgba(29, 185, 84, 0.5), - 0 6px 16px rgba(29, 185, 84, 0.4), - 0 3px 8px rgba(0, 0, 0, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.2), - inset -1px 0 0 rgba(29, 185, 84, 0.9); + box-shadow: + 0 6px 20px rgba(29, 185, 84, 0.3), + 0 3px 10px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.15), + inset -1px 0 0 rgba(29, 185, 84, 0.7); } .nav-icon { @@ -205,29 +210,33 @@ body { justify-content: center; font-size: 16px; font-weight: 600; - border-radius: 14px; - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.10) 0%, - rgba(255, 255, 255, 0.06) 100%); - border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + background: linear-gradient(135deg, + rgba(255, 255, 255, 0.08) 0%, + rgba(255, 255, 255, 0.04) 100%); + 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); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: - 0 2px 6px rgba(0, 0, 0, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: + 0 2px 4px rgba(0, 0, 0, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.08); } .nav-button.active .nav-icon { color: #1ed760; - background: linear-gradient(135deg, - rgba(29, 185, 84, 0.30) 0%, - rgba(30, 215, 96, 0.25) 100%); - border: 1px solid rgba(29, 185, 84, 0.4); + background: linear-gradient(135deg, + rgba(29, 185, 84, 0.25) 0%, + rgba(30, 215, 96, 0.20) 100%); + 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; - box-shadow: - 0 4px 12px rgba(29, 185, 84, 0.4), - 0 2px 6px rgba(29, 185, 84, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.2); + box-shadow: + 0 3px 8px rgba(29, 185, 84, 0.3), + 0 1px 4px rgba(29, 185, 84, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.15); } .nav-text { @@ -882,11 +891,12 @@ body { .main-content { flex: 1; - background: linear-gradient(135deg, - rgba(12, 12, 12, 0.95) 0%, - rgba(16, 16, 16, 0.98) 50%, - rgba(8, 8, 8, 0.95) 100%); - backdrop-filter: blur(20px) saturate(1.2); + background: linear-gradient(135deg, + rgba(12, 12, 12, 0.6) 0%, + rgba(16, 16, 16, 0.7) 50%, + rgba(8, 8, 8, 0.6) 100%); + backdrop-filter: blur(40px) saturate(1.8); + -webkit-backdrop-filter: blur(40px) saturate(1.8); overflow: auto; } @@ -979,11 +989,26 @@ body { } .stat-card { - background: linear-gradient(135deg, rgba(29, 185, 84, 0.1) 0%, rgba(255, 255, 255, 0.02) 100%); - border: 1px solid rgba(29, 185, 84, 0.2); - border-radius: 12px; + background: linear-gradient(135deg, rgba(29, 185, 84, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%); + backdrop-filter: blur(20px) saturate(1.5); + -webkit-backdrop-filter: blur(20px) saturate(1.5); + border: 1px solid rgba(29, 185, 84, 0.15); + border-radius: 16px; padding: 24px; 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 { @@ -3684,24 +3709,24 @@ body { display: flex; flex-direction: column; gap: 25px; /* Spacing between sections */ - padding: 28px 24px 30px 24px; /* Match modal padding */ - - /* Enhanced glassmorphic foundation matching modal */ - background: linear-gradient(135deg, - rgba(20, 20, 20, 0.85) 0%, - rgba(12, 12, 12, 0.92) 100%); - backdrop-filter: blur(20px) saturate(1.2); - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.12); - border-top: 1px solid rgba(255, 255, 255, 0.18); + padding: 28px 24px 30px 24px; + + /* Apple-style liquid glassmorphic foundation */ + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.55) 0%, + rgba(12, 12, 12, 0.65) 100%); + backdrop-filter: blur(40px) saturate(1.8); + -webkit-backdrop-filter: blur(40px) saturate(1.8); + border-radius: 24px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-top: 1px solid rgba(255, 255, 255, 0.12); margin: 20px; - - /* Premium shadow effect matching modal */ - box-shadow: - 0 20px 60px rgba(0, 0, 0, 0.6), - 0 8px 32px rgba(0, 0, 0, 0.4), - 0 0 40px rgba(29, 185, 84, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.15); + + /* Soft floating shadow */ + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.3), + 0 4px 16px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.08); } .dashboard-section {