From 03b7230ac24ebee4439445c3ac684621c38332cb Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:26:36 -0700 Subject: [PATCH] Preserve cover art in discovery fix-modal cache matched_data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion fix to the provider-hardcode bug (6ceedc8). The cache matched_data built by the 5 update_match / fix endpoints was dropping image_url and album.images when album came back as a bare string — common for Deezer and iTunes search results. Cache hits on re-discovery then produced downloads with no artwork. Each save site now carries image info through: - album_obj gets image_url + images:[{url}] populated from spotify_track.image_url - matched_data adds top-level image_url for pipeline consumers that check there - Works for both dict-shaped album (Spotify) and string-shaped album (Deezer/iTunes) Mirrors the handling already present in _build_fix_modal_spotify_data for the in-memory result['spotify_data'] — explains why the UI showed art fine during the fix modal but the cached entry lost it after restart. save_discovery_cache_match uses INSERT OR REPLACE, so existing bad cache entries refresh when the user re-fixes the track. No manual cache clearing needed. Added to 2.38 changelog (same round of discovery-fix work). --- web_server.py | 86 +++++++++++++++++++++++++++++++++++++++--- webui/static/helper.js | 1 + 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/web_server.py b/web_server.py index a6fdbf8d..7b3b6792 100644 --- a/web_server.py +++ b/web_server.py @@ -34562,8 +34562,23 @@ def update_tidal_discovery_match(): artists_list = spotify_track['artists'] if isinstance(artists_list, list): artists_list = [a if isinstance(a, str) else a.get('name', '') for a in artists_list] + # Preserve cover image info so the download pipeline can find + # artwork when this cached match is used later. The fix modal + # sends image_url at the top level; search results often return + # album as a bare string, which previously dropped the artwork. + image_url = spotify_track.get('image_url') or '' album_raw = spotify_track.get('album', '') - album_obj = album_raw if isinstance(album_raw, dict) else {'name': album_raw or ''} + 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}] matched_data = { 'id': spotify_track['id'], @@ -34571,6 +34586,7 @@ def update_tidal_discovery_match(): 'artists': artists_list, 'album': album_obj, 'duration_ms': spotify_track.get('duration_ms', 0), + 'image_url': image_url, 'source': 'spotify', } cache_db = get_database() @@ -36228,8 +36244,23 @@ def update_deezer_discovery_match(): artists_list = spotify_track['artists'] if isinstance(artists_list, list): artists_list = [a if isinstance(a, str) else a.get('name', '') for a in artists_list] + # Preserve cover image info so the download pipeline can find + # artwork when this cached match is used later. The fix modal + # sends image_url at the top level; search results often return + # album as a bare string, which previously dropped the artwork. + image_url = spotify_track.get('image_url') or '' album_raw = spotify_track.get('album', '') - album_obj = album_raw if isinstance(album_raw, dict) else {'name': album_raw or ''} + 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}] matched_data = { 'id': spotify_track['id'], @@ -36237,6 +36268,7 @@ def update_deezer_discovery_match(): 'artists': artists_list, 'album': album_obj, 'duration_ms': spotify_track.get('duration_ms', 0), + 'image_url': image_url, 'source': 'spotify', } cache_db = get_database() @@ -37077,8 +37109,23 @@ def update_spotify_public_discovery_match(): artists_list = spotify_track['artists'] if isinstance(artists_list, list): artists_list = [a if isinstance(a, str) else a.get('name', '') for a in artists_list] + # Preserve cover image info so the download pipeline can find + # artwork when this cached match is used later. The fix modal + # sends image_url at the top level; search results often return + # album as a bare string, which previously dropped the artwork. + image_url = spotify_track.get('image_url') or '' album_raw = spotify_track.get('album', '') - album_obj = album_raw if isinstance(album_raw, dict) else {'name': album_raw or ''} + 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}] matched_data = { 'id': spotify_track['id'], @@ -37086,6 +37133,7 @@ def update_spotify_public_discovery_match(): 'artists': artists_list, 'album': album_obj, 'duration_ms': spotify_track.get('duration_ms', 0), + 'image_url': image_url, 'source': 'spotify', } cache_db = get_database() @@ -38030,8 +38078,23 @@ def update_youtube_discovery_match(): artists_list = spotify_track['artists'] if isinstance(artists_list, list): artists_list = [a if isinstance(a, str) else a.get('name', '') for a in artists_list] + # Preserve cover image info so the download pipeline can find + # artwork when this cached match is used later. The fix modal + # sends image_url at the top level; search results often return + # album as a bare string, which previously dropped the artwork. + image_url = spotify_track.get('image_url') or '' album_raw = spotify_track.get('album', '') - album_obj = album_raw if isinstance(album_raw, dict) else {'name': album_raw or ''} + 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}] matched_data = { 'id': spotify_track['id'], @@ -38039,6 +38102,7 @@ def update_youtube_discovery_match(): 'artists': artists_list, 'album': album_obj, 'duration_ms': spotify_track.get('duration_ms', 0), + 'image_url': image_url, 'source': 'spotify', } cache_db = get_database() @@ -49757,11 +49821,23 @@ def fix_discovery_pool_track(): # Build matched_data in the same format as the discovery flow artists = spotify_track.get('artists', []) album_raw = spotify_track.get('album', '') - album_obj = album_raw if isinstance(album_raw, dict) else {'name': album_raw or ''} image_url = spotify_track.get('image_url', '') if not image_url and isinstance(album_raw, dict): images = album_raw.get('images', []) image_url = images[0].get('url', '') if images else '' + # Ensure album carries the artwork too — download pipeline checks + # album.images / album.image_url when extracting cover art. + 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}] matched_data = { 'id': spotify_track.get('id', ''), diff --git a/webui/static/helper.js b/webui/static/helper.js index cfb5e81f..645cb522 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3602,6 +3602,7 @@ const WHATS_NEW = { '2.38': [ // --- April 21, 2026 (late) --- { date: 'April 21, 2026 (late)' }, + { title: 'Fix Missing Cover Art on Manually Fixed Discovery Tracks', desc: 'The cache matched_data built by the fix modal dropped the image_url and album.images fields when album came back as a bare string (common for Deezer/iTunes search results). Result: re-discovery used the cached match but downloads showed no artwork. Cache writes now carry image_url through to album.images + top-level matched_data, matching what the in-memory state already did. Re-fix the track to refresh its cache entry (INSERT OR REPLACE)', page: 'sync' }, { title: 'Fix Manual Discovery Fixes Lost After Restart (Non-Spotify Users)', desc: 'When you clicked Fix on a discovery track and picked a manual match, the cache save hardcoded the provider as "spotify" regardless of your configured primary metadata source. On re-scan, the worker queried the cache with your actual primary (Deezer, iTunes, Discogs, Hydrabase) and missed the fix entirely. All 5 save sites (Tidal / Deezer / Spotify Public / YouTube / Discovery Pool) now use the active primary source, matching what the automatic workers already do', page: 'sync' }, ], '2.37': [