update discovery modal to collect album info

This commit is contained in:
Broque Thomas 2025-11-24 14:22:32 -08:00
parent 178bb7696d
commit 3be1dc9ace
2 changed files with 142 additions and 43 deletions

View file

@ -12610,12 +12610,30 @@ def _run_tidal_discovery_worker(playlist_id):
'status': 'not_found' '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'] = { result['spotify_data'] = {
'id': spotify_track.id, 'id': spotify_track.id,
'name': spotify_track.name, 'name': spotify_track.name,
'artists': spotify_track.artists, # Already a list of strings 'artists': spotify_track.artists,
'album': spotify_track.album, # Already a string 'album': {'name': spotify_track.album, 'album_type': 'album', 'images': []},
'duration_ms': spotify_track.duration_ms, 'duration_ms': spotify_track.duration_ms,
'external_urls': spotify_track.external_urls 'external_urls': spotify_track.external_urls
} }
@ -12698,19 +12716,28 @@ def _search_spotify_for_tidal_track(tidal_track):
# Find best match using confidence scoring # Find best match using confidence scoring
best_match = None best_match = None
best_match_raw = None # Store raw Spotify API data for full album info
best_confidence = 0.0 best_confidence = 0.0
min_confidence = 0.7 # Higher threshold for Tidal since data is cleaner min_confidence = 0.7 # Higher threshold for Tidal since data is cleaner
for query_idx, search_query in enumerate(search_queries): for query_idx, search_query in enumerate(search_queries):
try: try:
print(f"🔍 Tidal query {query_idx + 1}/{len(search_queries)}: {search_query}") 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) results = spotify_client.search_tracks(search_query, limit=5)
if not results: if not results:
continue continue
# Score each result using matching engine # 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: try:
# Calculate confidence using matching engine's similarity scoring (with fallback) # Calculate confidence using matching engine's similarity scoring (with fallback)
try: try:
@ -12745,6 +12772,7 @@ def _search_spotify_for_tidal_track(tidal_track):
if combined_confidence > best_confidence and combined_confidence >= min_confidence: if combined_confidence > best_confidence and combined_confidence >= min_confidence:
best_confidence = combined_confidence best_confidence = combined_confidence
best_match = result 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})") print(f"✅ New best Tidal match: {result.artists[0]} - {result.name} (confidence: {combined_confidence:.3f})")
except Exception as e: except Exception as e:
@ -12762,10 +12790,10 @@ def _search_spotify_for_tidal_track(tidal_track):
if best_match: if best_match:
print(f"✅ Final Tidal match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})") 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: else:
print(f"❌ No suitable Tidal match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})") print(f"❌ No suitable Tidal match found (best confidence was {best_confidence:.3f}, required {min_confidence:.3f})")
return None
return best_match
except Exception as e: except Exception as e:
print(f"❌ Error searching Spotify for Tidal track: {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 # Fallback to original simple query
search_queries = [f"artist:{cleaned_artist} track:{cleaned_title}"] 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): for query_idx, search_query in enumerate(search_queries):
try: try:
print(f"🔍 YouTube query {query_idx + 1}/{len(search_queries)}: {search_query}") 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) spotify_results = spotify_client.search_tracks(search_query, limit=5)
if not spotify_results: if not spotify_results:
continue continue
# Score each result using matching engine # 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: try:
# Calculate confidence using matching engine's similarity scoring (with fallback) # Calculate confidence using matching engine's similarity scoring (with fallback)
try: try:
@ -13238,6 +13276,7 @@ def _run_youtube_discovery_worker(url_hash):
if combined_confidence > best_confidence and combined_confidence >= min_confidence: if combined_confidence > best_confidence and combined_confidence >= min_confidence:
best_confidence = combined_confidence best_confidence = combined_confidence
spotify_track = spotify_result 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})") print(f"✅ New best YouTube match: {spotify_result.artists[0]} - {spotify_result.name} (confidence: {combined_confidence:.3f})")
except Exception as e: except Exception as e:
@ -13291,11 +13330,17 @@ def _run_youtube_discovery_worker(url_hash):
if spotify_track: if spotify_track:
state['spotify_matches'] += 1 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'] = { result['spotify_data'] = {
'id': spotify_track.id, 'id': spotify_track.id,
'name': spotify_track.name, 'name': spotify_track.name,
'artists': spotify_track.artists, 'artists': spotify_track.artists,
'album': spotify_track.album, 'album': album_data, # Full album object with images
'duration_ms': spotify_track.duration_ms '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 # Try each search query until we find a good match
best_match = None best_match = None
best_raw_track = None # Store raw Spotify data for full album info
best_confidence = 0.0 best_confidence = 0.0
min_confidence = 0.75 # Increased threshold to avoid bad matches like "Dolce" for "Fancy $hit" min_confidence = 0.75 # Increased threshold to avoid bad matches like "Dolce" for "Fancy $hit"
for query_idx, search_query in enumerate(search_queries): for query_idx, search_query in enumerate(search_queries):
try: try:
print(f"🔍 Query {query_idx + 1}/{len(search_queries)}: {search_query}") 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) search_results = spotify_client.search_tracks(search_query, limit=10)
if not search_results: if not search_results:
continue continue
# Use matching engine to find the best match from search results # 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: try:
# Calculate confidence using matching engine's similarity scoring (with fallback) # Calculate confidence using matching engine's similarity scoring (with fallback)
try: try:
@ -17938,6 +17991,7 @@ def _run_beatport_discovery_worker(url_hash):
core_title_confidence >= min_core_title_confidence): core_title_confidence >= min_core_title_confidence):
best_confidence = combined_confidence best_confidence = combined_confidence
best_match = result 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})") print(f"✅ New best match: {result.artists[0]} - {result.name} (confidence: {combined_confidence:.3f})")
except Exception as e: except Exception as e:
@ -17988,10 +18042,16 @@ def _run_beatport_discovery_worker(url_hash):
# Single artist case # Single artist case
formatted_artists = [{'name': str(spotify_track.artists)}] 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'] = { result_entry['spotify_data'] = {
'name': spotify_track.name, 'name': spotify_track.name,
'artists': formatted_artists, # Now formatted as list of objects with 'name' property '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 'id': spotify_track.id
# Remove uri for now since it's causing errors # Remove uri for now since it's causing errors
} }

View file

@ -8807,38 +8807,35 @@ async function addModalTracksToWishlist(playlistId) {
artists: formattedArtists artists: formattedArtists
}; };
// Use track's own album data if available (has correct album_type) // Use track's own album data if available
// If missing, fetch from Spotify using track ID // Convert string album names to objects if needed (no Spotify fetch!)
let trackAlbum = track.album; let trackAlbum = track.album;
let trackAlbumType = track.album?.album_type || 'album'; let trackAlbumType = 'album';
if (!trackAlbum || !trackAlbum.name) { // Handle both object and string album formats
// Try to fetch track data from Spotify to get album info if (typeof trackAlbum === 'string') {
if (track.spotify_track_id || track.id) { // Album is just a string - convert to minimal object
try { trackAlbum = {
const spotifyTrackId = track.spotify_track_id || track.id; name: trackAlbum,
const trackInfoResponse = await fetch(`/api/spotify/track/${spotifyTrackId}`); album_type: 'album',
const trackInfo = await trackInfoResponse.json(); images: []
};
if (trackInfo && trackInfo.album) { trackAlbumType = 'album';
trackAlbum = trackInfo.album; } else if (trackAlbum && typeof trackAlbum === 'object') {
trackAlbumType = trackInfo.album.album_type || 'album'; // Album is already an object - extract album_type
console.log(`✅ Fetched album data for "${track.name}"`); trackAlbumType = trackAlbum.album_type || 'album';
} else { // Ensure it has a name
console.warn(`⚠️ Skipping track "${track.name}" - could not fetch album data`); if (!trackAlbum.name) {
errorCount++; trackAlbum.name = 'Unknown Album';
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;
} }
} 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', { const response = await fetch('/api/add-album-to-wishlist', {
@ -11848,11 +11845,22 @@ async function startTidalDownloadMissing(urlHash) {
spotifyTracks.push(result.spotify_data); spotifyTracks.push(result.spotify_data);
} else if (result.spotify_track && result.status_class === 'found') { } else if (result.spotify_track && result.status_class === 'found') {
// Build from individual fields (automatic discovery format) // 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({ spotifyTracks.push({
id: result.spotify_id || 'unknown', id: result.spotify_id || 'unknown',
name: result.spotify_track || 'Unknown Track', name: result.spotify_track || 'Unknown Track',
artists: result.spotify_artist ? [result.spotify_artist] : ['Unknown Artist'], 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; track = result.spotify_data;
} else { } else {
// Build from individual fields (automatic discovery format) // 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 = { track = {
id: result.spotify_id || 'unknown', id: result.spotify_id || 'unknown',
name: result.spotify_track || 'Unknown Track', name: result.spotify_track || 'Unknown Track',
artists: result.spotify_artist ? [result.spotify_artist] : ['Unknown Artist'], artists: result.spotify_artist ? [result.spotify_artist] : ['Unknown Artist'],
album: result.spotify_album || 'Unknown Album', album: albumObject,
duration_ms: 0 duration_ms: 0
}; };
} }
@ -13908,11 +13926,21 @@ async function startBeatportDownloadMissing(urlHash) {
} else { } else {
track.artists = ['Unknown Artist']; 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 { return {
id: track.id, id: track.id,
name: track.name, name: track.name,
artists: track.artists, artists: track.artists,
album: track.album || 'Unknown Album', album: albumForReturn,
duration_ms: track.duration_ms || 0, duration_ms: track.duration_ms || 0,
external_urls: track.external_urls || {} external_urls: track.external_urls || {}
}; };
@ -16374,11 +16402,22 @@ async function startYouTubeDownloadMissing(urlHash) {
return result.spotify_data; return result.spotify_data;
} else { } else {
// Build from individual fields (automatic discovery format) // 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 { return {
id: result.spotify_id || 'unknown', id: result.spotify_id || 'unknown',
name: result.spotify_track || 'Unknown Track', name: result.spotify_track || 'Unknown Track',
artists: result.spotify_artist ? [result.spotify_artist] : ['Unknown Artist'], artists: result.spotify_artist ? [result.spotify_artist] : ['Unknown Artist'],
album: result.spotify_album || 'Unknown Album' album: albumObject,
duration_ms: 0
}; };
} }
}); });