optimize album and cover art data

pull/64/head
Broque Thomas 6 months ago
parent 62ad5a9cd9
commit 5ffce6bb59

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

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

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

Loading…
Cancel
Save