diff --git a/web_server.py b/web_server.py index de84c8dc..8d426861 100644 --- a/web_server.py +++ b/web_server.py @@ -12610,12 +12610,30 @@ def _run_tidal_discovery_worker(playlist_id): 'status': 'not_found' } - if spotify_track: + if isinstance(spotify_track, tuple): + # Function now returns (Track, raw_data) + track_obj, raw_track_data = spotify_track + # Use full album object from raw API response + album_obj = raw_track_data.get('album', {}) if raw_track_data else {} + + result['spotify_data'] = { + 'id': track_obj.id, + 'name': track_obj.name, + 'artists': track_obj.artists, # Already a list of strings + 'album': album_obj, # Full album object with images + 'duration_ms': track_obj.duration_ms, + 'external_urls': track_obj.external_urls + } + result['status'] = 'found' + successful_discoveries += 1 + state['spotify_matches'] = successful_discoveries + elif spotify_track: + # Fallback for old format (shouldn't happen after update) result['spotify_data'] = { 'id': spotify_track.id, 'name': spotify_track.name, - 'artists': spotify_track.artists, # Already a list of strings - 'album': spotify_track.album, # Already a string + 'artists': spotify_track.artists, + 'album': {'name': spotify_track.album, 'album_type': 'album', 'images': []}, 'duration_ms': spotify_track.duration_ms, 'external_urls': spotify_track.external_urls } @@ -12698,19 +12716,28 @@ def _search_spotify_for_tidal_track(tidal_track): # Find best match using confidence scoring best_match = None + best_match_raw = None # Store raw Spotify API data for full album info best_confidence = 0.0 min_confidence = 0.7 # Higher threshold for Tidal since data is cleaner for query_idx, search_query in enumerate(search_queries): try: print(f"🔍 Tidal query {query_idx + 1}/{len(search_queries)}: {search_query}") + + # Get raw Spotify API response to access full album object with images + raw_results = spotify_client.sp.search(q=search_query, type='track', limit=5) + if not raw_results or 'tracks' not in raw_results or not raw_results['tracks']['items']: + continue + + # Also get Track objects for matching logic results = spotify_client.search_tracks(search_query, limit=5) if not results: continue # Score each result using matching engine - for result in results: + for idx, result in enumerate(results): + raw_track = raw_results['tracks']['items'][idx] if idx < len(raw_results['tracks']['items']) else None try: # Calculate confidence using matching engine's similarity scoring (with fallback) try: @@ -12745,6 +12772,7 @@ def _search_spotify_for_tidal_track(tidal_track): if combined_confidence > best_confidence and combined_confidence >= min_confidence: best_confidence = combined_confidence best_match = result + best_match_raw = raw_track # Store raw data with full album object print(f"✅ New best Tidal match: {result.artists[0]} - {result.name} (confidence: {combined_confidence:.3f})") except Exception as e: @@ -12762,10 +12790,10 @@ def _search_spotify_for_tidal_track(tidal_track): if best_match: print(f"✅ Final Tidal match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})") + return (best_match, best_match_raw) # Return both Track object and raw data else: print(f"❌ No suitable Tidal match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})") - - return best_match + return None except Exception as e: print(f"❌ Error searching Spotify for Tidal track: {e}") @@ -13179,16 +13207,26 @@ def _run_youtube_discovery_worker(url_hash): # Fallback to original simple query search_queries = [f"artist:{cleaned_artist} track:{cleaned_title}"] + # Store raw Spotify data for best match + best_raw_track = None + for query_idx, search_query in enumerate(search_queries): try: print(f"🔍 YouTube query {query_idx + 1}/{len(search_queries)}: {search_query}") + + # Get raw Spotify API response to access full album object with images + raw_results = spotify_client.sp.search(q=search_query, type='track', limit=5) + if not raw_results or 'tracks' not in raw_results or not raw_results['tracks']['items']: + continue + spotify_results = spotify_client.search_tracks(search_query, limit=5) if not spotify_results: continue # Score each result using matching engine - for spotify_result in spotify_results: + for result_idx, spotify_result in enumerate(spotify_results): + raw_track = raw_results['tracks']['items'][result_idx] if result_idx < len(raw_results['tracks']['items']) else None try: # Calculate confidence using matching engine's similarity scoring (with fallback) try: @@ -13238,6 +13276,7 @@ def _run_youtube_discovery_worker(url_hash): if combined_confidence > best_confidence and combined_confidence >= min_confidence: best_confidence = combined_confidence spotify_track = spotify_result + best_raw_track = raw_track # Store raw data with full album object print(f"✅ New best YouTube match: {spotify_result.artists[0]} - {spotify_result.name} (confidence: {combined_confidence:.3f})") except Exception as e: @@ -13291,11 +13330,17 @@ def _run_youtube_discovery_worker(url_hash): if spotify_track: state['spotify_matches'] += 1 + # Use full album object from raw Spotify data if available + album_data = best_raw_track.get('album', {}) if best_raw_track else {} + if not album_data: + # Fallback to string album name + album_data = {'name': spotify_track.album, 'album_type': 'album', 'images': []} + result['spotify_data'] = { 'id': spotify_track.id, 'name': spotify_track.name, 'artists': spotify_track.artists, - 'album': spotify_track.album, + 'album': album_data, # Full album object with images 'duration_ms': spotify_track.duration_ms } @@ -17868,19 +17913,27 @@ def _run_beatport_discovery_worker(url_hash): # Try each search query until we find a good match best_match = None + best_raw_track = None # Store raw Spotify data for full album info best_confidence = 0.0 min_confidence = 0.75 # Increased threshold to avoid bad matches like "Dolce" for "Fancy $hit" for query_idx, search_query in enumerate(search_queries): try: print(f"🔍 Query {query_idx + 1}/{len(search_queries)}: {search_query}") + + # Get raw Spotify API response to access full album object with images + raw_results = spotify_client.sp.search(q=search_query, type='track', limit=10) + if not raw_results or 'tracks' not in raw_results or not raw_results['tracks']['items']: + continue + search_results = spotify_client.search_tracks(search_query, limit=10) if not search_results: continue # Use matching engine to find the best match from search results - for result in search_results: + for result_idx, result in enumerate(search_results): + raw_track = raw_results['tracks']['items'][result_idx] if result_idx < len(raw_results['tracks']['items']) else None try: # Calculate confidence using matching engine's similarity scoring (with fallback) try: @@ -17938,6 +17991,7 @@ def _run_beatport_discovery_worker(url_hash): core_title_confidence >= min_core_title_confidence): best_confidence = combined_confidence best_match = result + best_raw_track = raw_track # Store raw data with full album object print(f"✅ New best match: {result.artists[0]} - {result.name} (confidence: {combined_confidence:.3f})") except Exception as e: @@ -17988,10 +18042,16 @@ def _run_beatport_discovery_worker(url_hash): # Single artist case formatted_artists = [{'name': str(spotify_track.artists)}] + # Use full album object from raw Spotify data if available + album_data = best_raw_track.get('album', {}) if best_raw_track else {} + if not album_data: + # Fallback to string album name + album_data = {'name': spotify_track.album, 'album_type': 'album', 'images': []} + result_entry['spotify_data'] = { 'name': spotify_track.name, 'artists': formatted_artists, # Now formatted as list of objects with 'name' property - 'album': spotify_track.album, # Already a string + 'album': album_data, # Full album object with images 'id': spotify_track.id # Remove uri for now since it's causing errors } diff --git a/webui/static/script.js b/webui/static/script.js index fe3561fb..b4502c06 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -8807,38 +8807,35 @@ async function addModalTracksToWishlist(playlistId) { artists: formattedArtists }; - // Use track's own album data if available (has correct album_type) - // If missing, fetch from Spotify using track ID + // Use track's own album data if available + // Convert string album names to objects if needed (no Spotify fetch!) let trackAlbum = track.album; - let trackAlbumType = track.album?.album_type || 'album'; + let trackAlbumType = 'album'; - if (!trackAlbum || !trackAlbum.name) { - // Try to fetch track data from Spotify to get album info - if (track.spotify_track_id || track.id) { - try { - const spotifyTrackId = track.spotify_track_id || track.id; - const trackInfoResponse = await fetch(`/api/spotify/track/${spotifyTrackId}`); - const trackInfo = await trackInfoResponse.json(); - - if (trackInfo && trackInfo.album) { - trackAlbum = trackInfo.album; - trackAlbumType = trackInfo.album.album_type || 'album'; - console.log(`✅ Fetched album data for "${track.name}"`); - } else { - console.warn(`⚠️ Skipping track "${track.name}" - could not fetch album data`); - errorCount++; - continue; - } - } catch (fetchError) { - console.error(`❌ Error fetching track data for "${track.name}":`, fetchError); - errorCount++; - continue; - } - } else { - console.warn(`⚠️ Skipping track "${track.name}" - missing album data and track ID`); - errorCount++; - continue; + // Handle both object and string album formats + if (typeof trackAlbum === 'string') { + // Album is just a string - convert to minimal object + trackAlbum = { + name: trackAlbum, + album_type: 'album', + images: [] + }; + trackAlbumType = 'album'; + } else if (trackAlbum && typeof trackAlbum === 'object') { + // Album is already an object - extract album_type + trackAlbumType = trackAlbum.album_type || 'album'; + // Ensure it has a name + if (!trackAlbum.name) { + trackAlbum.name = 'Unknown Album'; } + } else { + // No album data at all - create minimal object + trackAlbum = { + name: 'Unknown Album', + album_type: 'album', + images: [] + }; + trackAlbumType = 'album'; } const response = await fetch('/api/add-album-to-wishlist', { @@ -11848,11 +11845,22 @@ async function startTidalDownloadMissing(urlHash) { spotifyTracks.push(result.spotify_data); } else if (result.spotify_track && result.status_class === 'found') { // Build from individual fields (automatic discovery format) + // Convert album to proper object format for wishlist compatibility + const albumData = result.spotify_album || 'Unknown Album'; + const albumObject = typeof albumData === 'object' && albumData !== null + ? albumData + : { + name: typeof albumData === 'string' ? albumData : 'Unknown Album', + album_type: 'album', + images: [] + }; + spotifyTracks.push({ id: result.spotify_id || 'unknown', name: result.spotify_track || 'Unknown Track', artists: result.spotify_artist ? [result.spotify_artist] : ['Unknown Artist'], - album: result.spotify_album || 'Unknown Album' + album: albumObject, + duration_ms: 0 }); } } @@ -13889,11 +13897,21 @@ async function startBeatportDownloadMissing(urlHash) { track = result.spotify_data; } else { // Build from individual fields (automatic discovery format) + // Convert album to proper object format for wishlist compatibility + const albumData = result.spotify_album || 'Unknown Album'; + const albumObject = typeof albumData === 'object' && albumData !== null + ? albumData + : { + name: typeof albumData === 'string' ? albumData : 'Unknown Album', + album_type: 'album', + images: [] + }; + track = { id: result.spotify_id || 'unknown', name: result.spotify_track || 'Unknown Track', artists: result.spotify_artist ? [result.spotify_artist] : ['Unknown Artist'], - album: result.spotify_album || 'Unknown Album', + album: albumObject, duration_ms: 0 }; } @@ -13908,11 +13926,21 @@ async function startBeatportDownloadMissing(urlHash) { } else { track.artists = ['Unknown Artist']; } + + // Ensure album is an object (in case it was converted back to string somehow) + const albumForReturn = typeof track.album === 'object' && track.album !== null + ? track.album + : { + name: typeof track.album === 'string' ? track.album : 'Unknown Album', + album_type: 'album', + images: [] + }; + return { id: track.id, name: track.name, artists: track.artists, - album: track.album || 'Unknown Album', + album: albumForReturn, duration_ms: track.duration_ms || 0, external_urls: track.external_urls || {} }; @@ -16374,11 +16402,22 @@ async function startYouTubeDownloadMissing(urlHash) { return result.spotify_data; } else { // Build from individual fields (automatic discovery format) + // Convert album to proper object format for wishlist compatibility + const albumData = result.spotify_album || 'Unknown Album'; + const albumObject = typeof albumData === 'object' && albumData !== null + ? albumData + : { + name: typeof albumData === 'string' ? albumData : 'Unknown Album', + album_type: 'album', + images: [] + }; + return { id: result.spotify_id || 'unknown', name: result.spotify_track || 'Unknown Track', artists: result.spotify_artist ? [result.spotify_artist] : ['Unknown Artist'], - album: result.spotify_album || 'Unknown Album' + album: albumObject, + duration_ms: 0 }; } });