diff --git a/web_server.py b/web_server.py index d087ed37..7c2b6624 100644 --- a/web_server.py +++ b/web_server.py @@ -33508,7 +33508,8 @@ def search_spotify_tracks(): 'name': t.name, 'artists': t.artists, 'album': t.album, - 'duration_ms': t.duration_ms + 'duration_ms': t.duration_ms, + 'image_url': getattr(t, 'image_url', None), } for t in tracks] return jsonify({'tracks': tracks_dict}) @@ -34369,14 +34370,13 @@ def update_tidal_discovery_match(): else: result['duration'] = '0:00' - # IMPORTANT: Also set spotify_data for sync/download compatibility - result['spotify_data'] = { - 'id': spotify_track['id'], - 'name': spotify_track['name'], - 'artists': spotify_track['artists'], - 'album': spotify_track['album'], - 'duration_ms': spotify_track.get('duration_ms', 0) - } + # IMPORTANT: Also set spotify_data for sync/download compatibility. + # Manual match from the fix modal — build a rich spotify_data (album + # as dict with image info) matching the normal discovery shape, and + # explicitly clear any prior wing-it flag since the user picked a + # real metadata match. + result['spotify_data'] = _build_fix_modal_spotify_data(spotify_track) + result['wing_it_fallback'] = False result['manual_match'] = True # Flag for tracking @@ -36038,14 +36038,13 @@ def update_deezer_discovery_match(): else: result['duration'] = '0:00' - # IMPORTANT: Also set spotify_data for sync/download compatibility - result['spotify_data'] = { - 'id': spotify_track['id'], - 'name': spotify_track['name'], - 'artists': spotify_track['artists'], - 'album': spotify_track['album'], - 'duration_ms': spotify_track.get('duration_ms', 0) - } + # IMPORTANT: Also set spotify_data for sync/download compatibility. + # Manual match from the fix modal — build a rich spotify_data (album + # as dict with image info) matching the normal discovery shape, and + # explicitly clear any prior wing-it flag since the user picked a + # real metadata match. + result['spotify_data'] = _build_fix_modal_spotify_data(spotify_track) + result['wing_it_fallback'] = False result['manual_match'] = True @@ -36888,14 +36887,13 @@ def update_spotify_public_discovery_match(): else: result['duration'] = '0:00' - # IMPORTANT: Also set spotify_data for sync/download compatibility - result['spotify_data'] = { - 'id': spotify_track['id'], - 'name': spotify_track['name'], - 'artists': spotify_track['artists'], - 'album': spotify_track['album'], - 'duration_ms': spotify_track.get('duration_ms', 0) - } + # IMPORTANT: Also set spotify_data for sync/download compatibility. + # Manual match from the fix modal — build a rich spotify_data (album + # as dict with image info) matching the normal discovery shape, and + # explicitly clear any prior wing-it flag since the user picked a + # real metadata match. + result['spotify_data'] = _build_fix_modal_spotify_data(spotify_track) + result['wing_it_fallback'] = False result['manual_match'] = True @@ -37838,14 +37836,13 @@ def update_youtube_discovery_match(): else: result['duration'] = '0:00' - # IMPORTANT: Also set spotify_data for sync/download compatibility - result['spotify_data'] = { - 'id': spotify_track['id'], - 'name': spotify_track['name'], - 'artists': spotify_track['artists'], - 'album': spotify_track['album'], - 'duration_ms': spotify_track.get('duration_ms', 0) - } + # IMPORTANT: Also set spotify_data for sync/download compatibility. + # Manual match from the fix modal — build a rich spotify_data (album + # as dict with image info) matching the normal discovery shape, and + # explicitly clear any prior wing-it flag since the user picked a + # real metadata match. + result['spotify_data'] = _build_fix_modal_spotify_data(spotify_track) + result['wing_it_fallback'] = False result['manual_match'] = True # Flag for tracking @@ -37934,6 +37931,46 @@ def _build_discovery_wing_it_stub(track_name, artist_name, duration_ms=0, image_ } +def _build_fix_modal_spotify_data(spotify_track): + """Build a rich spotify_data dict from the fix-modal POST payload so manual + matches carry the same shape as normal discovery results. + + Key points: + - album is always a dict (normal discovery has it this way; legacy fix-modal + produced a bare string which broke cover art lookup downstream) + - image_url is carried both at top level and inside album.images for parity + with Spotify API responses + - handles both legacy string albums (most search endpoints return this) and + newer object albums + """ + if not isinstance(spotify_track, dict): + spotify_track = {} + + image_url = spotify_track.get('image_url') or '' + album_raw = spotify_track.get('album', '') + + if isinstance(album_raw, dict): + album_obj = dict(album_raw) + if image_url and not album_obj.get('image_url'): + album_obj['image_url'] = image_url + if image_url and not album_obj.get('images'): + album_obj['images'] = [{'url': image_url}] + else: + album_obj = {'name': album_raw or ''} + if image_url: + album_obj['image_url'] = image_url + album_obj['images'] = [{'url': image_url}] + + return { + 'id': spotify_track.get('id', ''), + 'name': spotify_track.get('name', ''), + 'artists': spotify_track.get('artists', []), + 'album': album_obj, + 'duration_ms': spotify_track.get('duration_ms', 0), + 'image_url': image_url, + } + + def _run_youtube_discovery_worker(url_hash): """Background worker for YouTube music discovery process (Spotify preferred, iTunes fallback)""" _ew_state = {} @@ -46543,17 +46580,16 @@ def update_listenbrainz_discovery_match(): result['spotify_id'] = spotify_track.get('id', '') if spotify_track else '' if spotify_track: - # Store spotify_data in the same format as other platforms - result['spotify_data'] = { - 'id': spotify_track.get('id', ''), - 'name': spotify_track.get('name', ''), - 'artists': artists if isinstance(artists, list) else [artists], - 'album': result['spotify_album'], - 'duration_ms': spotify_track.get('duration_ms', 0) - } + # Store spotify_data in the same format as other platforms. + # Manual match from the fix modal — build a rich spotify_data + # (album as dict with image info) matching the normal discovery + # shape, and explicitly clear any prior wing-it flag since the + # user picked a real metadata match. + result['spotify_data'] = _build_fix_modal_spotify_data(spotify_track) else: result['spotify_data'] = None + result['wing_it_fallback'] = False result['manual_match'] = True logger.info(f"Updated ListenBrainz match for track {track_index}: {result['status']}") @@ -48757,14 +48793,13 @@ def update_beatport_discovery_match(): else: result['duration'] = '0:00' - # IMPORTANT: Also set spotify_data for sync/download compatibility - result['spotify_data'] = { - 'id': spotify_track['id'], - 'name': spotify_track['name'], - 'artists': spotify_track['artists'], - 'album': spotify_track['album'], - 'duration_ms': spotify_track.get('duration_ms', 0) - } + # IMPORTANT: Also set spotify_data for sync/download compatibility. + # Manual match from the fix modal — build a rich spotify_data (album + # as dict with image info) matching the normal discovery shape, and + # explicitly clear any prior wing-it flag since the user picked a + # real metadata match. + result['spotify_data'] = _build_fix_modal_spotify_data(spotify_track) + result['wing_it_fallback'] = False result['manual_match'] = True # Flag for tracking diff --git a/webui/static/script.js b/webui/static/script.js index 950b5f31..16954d75 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -19714,7 +19714,8 @@ async function selectDiscoveryFixTrack(track) { name: track.name, artists: track.artists, album: track.album, - duration_ms: track.duration_ms + duration_ms: track.duration_ms, + image_url: track.image_url || null } }; @@ -19782,14 +19783,33 @@ async function selectDiscoveryFixTrack(track) { result.spotify_id = track.id; result.duration = formatDuration(track.duration_ms); result.manual_match = true; + // User picked a real metadata match — no longer a wing-it track + result.wing_it_fallback = false; - // IMPORTANT: Also set spotify_data for download/sync compatibility + // IMPORTANT: Also set spotify_data for download/sync compatibility. + // Build album as a dict (not a bare string) so the download + // pipeline can find cover art via album.image_url / album.images. + // This matches the shape that normal discovery produces. + const _fixImageUrl = track.image_url || ''; + let _fixAlbumObj; + if (track.album && typeof track.album === 'object') { + _fixAlbumObj = { ...track.album }; + if (_fixImageUrl && !_fixAlbumObj.image_url) _fixAlbumObj.image_url = _fixImageUrl; + if (_fixImageUrl && !_fixAlbumObj.images) _fixAlbumObj.images = [{ url: _fixImageUrl }]; + } else { + _fixAlbumObj = { name: track.album || '' }; + if (_fixImageUrl) { + _fixAlbumObj.image_url = _fixImageUrl; + _fixAlbumObj.images = [{ url: _fixImageUrl }]; + } + } result.spotify_data = { id: track.id, name: track.name, artists: track.artists, - album: track.album, - duration_ms: track.duration_ms + album: _fixAlbumObj, + duration_ms: track.duration_ms, + image_url: _fixImageUrl }; // Increment match count if this was previously not_found or error