From bab31218bbd1871f0240d758fafa52affda50e91 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Tue, 9 Dec 2025 10:44:54 -0800 Subject: [PATCH] Improve artist handling and preserve album data in sync Enhanced handling of artist data to support both string and object formats across the database, sync service, and web server. The sync process now preserves full album and artist objects for tracks, enabling wishlist additions with album cover art. The frontend and API were updated to use the full artist objects, and the UI now formats artist names correctly. --- database/music_database.py | 22 +++++++++++++++-- services/sync_service.py | 48 +++++++++++++++++++++++++++----------- web_server.py | 20 +++++++++++++--- webui/static/script.js | 2 +- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index 9cca9a8..4642fb0 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -2303,7 +2303,16 @@ class MusicDatabase: track_name = spotify_track_data.get('name', 'Unknown Track') artists = spotify_track_data.get('artists', []) - artist_name = artists[0].get('name', 'Unknown Artist') if artists else 'Unknown Artist' + if artists: + first_artist = artists[0] + if isinstance(first_artist, str): + artist_name = first_artist + elif isinstance(first_artist, dict): + artist_name = first_artist.get('name', 'Unknown Artist') + else: + artist_name = 'Unknown Artist' + else: + artist_name = 'Unknown Artist' # Check for duplicates by track name + artist (not just Spotify ID) # This prevents adding the same track multiple times with different IDs or edge cases @@ -2319,7 +2328,16 @@ class MusicDatabase: existing_data = json.loads(existing['spotify_data']) existing_name = existing_data.get('name', '') existing_artists = existing_data.get('artists', []) - existing_artist = existing_artists[0].get('name', '') if existing_artists else '' + if existing_artists: + existing_first = existing_artists[0] + if isinstance(existing_first, str): + existing_artist = existing_first + elif isinstance(existing_first, dict): + existing_artist = existing_first.get('name', '') + else: + existing_artist = '' + else: + existing_artist = '' # Case-insensitive comparison of track name and primary artist if (existing_name.lower() == track_name.lower() and diff --git a/services/sync_service.py b/services/sync_service.py index 37f6665..70279fd 100644 --- a/services/sync_service.py +++ b/services/sync_service.py @@ -167,7 +167,13 @@ class PlaylistSyncService: # Update progress for each track progress_percent = 20 + (40 * (i + 1) / total_tracks) # 20-60% for matching - current_track_name = f"{track.artists[0]} - {track.name}" if track.artists else track.name + # Extract artist name from both string and dict formats + if track.artists: + first_artist = track.artists[0] + artist_name = first_artist if isinstance(first_artist, str) else (first_artist.get('name', 'Unknown') if isinstance(first_artist, dict) else str(first_artist)) + current_track_name = f"{artist_name} - {track.name}" + else: + current_track_name = track.name self._update_progress(playlist.name, "Matching tracks", current_track_name, progress_percent, 5, 2, total_tracks=total_tracks, matched_tracks=len([r for r in match_results if r.is_match]), @@ -266,17 +272,25 @@ class PlaylistSyncService: for match_result in unmatched_tracks: spotify_track = match_result.spotify_track - # Convert SpotifyTrack to dict format expected by wishlist service - spotify_track_data = { - 'id': spotify_track.id, - 'name': spotify_track.name, - 'artists': [{'name': a} if isinstance(a, str) else a for a in spotify_track.artists], - 'album': {'name': spotify_track.album}, - 'duration_ms': spotify_track.duration_ms, - 'popularity': getattr(spotify_track, 'popularity', 0), - 'preview_url': getattr(spotify_track, 'preview_url', None), - 'external_urls': getattr(spotify_track, 'external_urls', {}) - } + # Check if we have original track data with full album objects + original_track_data = None + if hasattr(self, '_original_tracks_map') and self._original_tracks_map: + original_track_data = self._original_tracks_map.get(spotify_track.id) + + # Use original data if available (preserves album images), otherwise convert + if original_track_data: + spotify_track_data = original_track_data + else: + spotify_track_data = { + 'id': spotify_track.id, + 'name': spotify_track.name, + 'artists': [{'name': a} if isinstance(a, str) else a for a in spotify_track.artists], + 'album': {'name': spotify_track.album}, + 'duration_ms': spotify_track.duration_ms, + 'popularity': getattr(spotify_track, 'popularity', 0), + 'preview_url': getattr(spotify_track, 'preview_url', None), + 'external_urls': getattr(spotify_track, 'external_urls', {}) + } # Add to wishlist with source context success = wishlist_service.add_spotify_track_to_wishlist( @@ -344,8 +358,14 @@ class PlaylistSyncService: for artist in spotify_track.artists: if self._cancelled: return None, 0.0 - - artist_name = artist if isinstance(artist, str) else artist + + # Extract artist name from both string and dict formats + if isinstance(artist, str): + artist_name = artist + elif isinstance(artist, dict) and 'name' in artist: + artist_name = artist['name'] + else: + artist_name = str(artist) # Use the improved database check_track_exists method with server awareness try: diff --git a/web_server.py b/web_server.py index 1b669d4..c0c88c7 100644 --- a/web_server.py +++ b/web_server.py @@ -12547,7 +12547,7 @@ def get_playlist_tracks(playlist_id): tracks.append({ 'id': track_data['id'], 'name': track_data['name'], - 'artists': [artist['name'] for artist in track_data['artists']], + 'artists': track_data['artists'], # Full artist objects (matches Download Missing Tracks behavior) 'album': track_data['album'], # Full album object 'duration_ms': track_data['duration_ms'], 'popularity': track_data.get('popularity', 0), @@ -12589,7 +12589,7 @@ def get_playlist_tracks(playlist_id): tracks.append({ 'id': track_data['id'], 'name': track_data['name'], - 'artists': [artist['name'] for artist in track_data['artists']], + 'artists': track_data['artists'], # Full artist objects (matches Download Missing Tracks behavior) 'album': track_data['album'], # Full album object with album_type, total_tracks, etc. 'duration_ms': track_data['duration_ms'], 'popularity': track_data.get('popularity', 0), @@ -14502,6 +14502,14 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json): # Recreate a Playlist object from the JSON data sent by the frontend # This avoids needing to re-fetch it from Spotify print(f"🔄 Converting JSON tracks to SpotifyTrack objects...") + + # Store original track data with full album objects (for wishlist with cover art) + original_tracks_map = {} + for t in tracks_json: + track_id = t.get('id', '') + if track_id: + original_tracks_map[track_id] = t + tracks = [] for i, t in enumerate(tracks_json): # Handle album field - extract name if it's a dictionary @@ -14666,7 +14674,10 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json): setup_duration = (sync_start_time - task_start_time) * 1000 print(f"⏱️ [TIMING] Setup completed at {time.strftime('%H:%M:%S')} (took {setup_duration:.1f}ms)") print(f"🚀 Starting actual sync process with asyncio.run()...") - + + # Attach original tracks map to sync_service for wishlist with album images + sync_service._original_tracks_map = original_tracks_map + # Run the sync (this is a blocking call within this thread) result = asyncio.run(sync_service.sync_playlist(playlist, download_missing=False)) @@ -14704,6 +14715,9 @@ def _run_sync_task(playlist_id, playlist_name, tracks_json): # Clean up the callback if sync_service: sync_service.clear_progress_callback(playlist.name) + # Clean up original tracks map + if hasattr(sync_service, '_original_tracks_map'): + del sync_service._original_tracks_map print(f"✅ Cleanup completed for {playlist_id}") diff --git a/webui/static/script.js b/webui/static/script.js index 9e94778..659b870 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -3975,7 +3975,7 @@ function showPlaylistDetailsModal(playlist) { ${index + 1}
${escapeHtml(track.name)}
-
${track.artists.join(', ')}
+
${formatArtists(track.artists)}
${formatDuration(track.duration_ms)}