From 213736e168d55c341e9ede46e966ec605dce320c Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:04:22 -0700 Subject: [PATCH] Fix manual match storing iTunes/Deezer IDs in Spotify ID columns (#213) When Spotify falls back to iTunes/Deezer (auth failure, rate limit, API error), the manual match modals were storing numeric fallback IDs in spotify_*_id columns, breaking Spotify links and metadata fetching. Fix detects the actual provider by inspecting result IDs (alphanumeric = Spotify, numeric = fallback) rather than checking auth status, which can be misleading during rate limit bans. The frontend now passes the real provider to the storage endpoint so IDs land in the correct column. --- web_server.py | 44 ++++++++++++++++++++++++++++++++++-------- webui/static/helper.js | 1 + webui/static/script.js | 6 ++++-- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/web_server.py b/web_server.py index d389a5f2..66022734 100644 --- a/web_server.py +++ b/web_server.py @@ -12304,6 +12304,15 @@ def library_search_service(): return jsonify({"success": False, "error": str(e)}), 500 +def _detect_provider(items, client): + """Detect actual provider from result IDs. Spotify IDs are alphanumeric; + iTunes/Deezer IDs are purely numeric. If the results have numeric IDs, + they came from the fallback source, not Spotify.""" + if items and str(items[0].id).isdigit(): + return client._fallback_source + return 'spotify' + + def _search_service(service, entity_type, query): """Search a service and return normalized results.""" import requests as req_lib @@ -12314,13 +12323,18 @@ def _search_service(service, entity_type, query): client = spotify_enrichment_worker.client if entity_type == 'artist': items = client.search_artists(query, limit=8) - return [{'id': a.id, 'name': a.name, 'image': a.image_url, 'extra': ', '.join(a.genres[:3]) if a.genres else ''} for a in items] + # Detect actual provider from result IDs — Spotify IDs are alphanumeric, + # iTunes/Deezer IDs are purely numeric. Prevents storing wrong IDs. + provider = _detect_provider(items, client) + return [{'id': a.id, 'name': a.name, 'image': a.image_url, 'extra': ', '.join(a.genres[:3]) if a.genres else '', 'provider': provider} for a in items] elif entity_type == 'album': items = client.search_albums(query, limit=8) - return [{'id': a.id, 'name': a.name, 'image': a.image_url, 'extra': f"{', '.join(a.artists)} · {a.release_date or ''}"} for a in items] + provider = _detect_provider(items, client) + return [{'id': a.id, 'name': a.name, 'image': a.image_url, 'extra': f"{', '.join(a.artists)} · {a.release_date or ''}", 'provider': provider} for a in items] elif entity_type == 'track': items = client.search_tracks(query, limit=8) - return [{'id': t.id, 'name': t.name, 'image': t.image_url, 'extra': f"{', '.join(t.artists)} · {t.album or ''}"} for t in items] + provider = _detect_provider(items, client) + return [{'id': t.id, 'name': t.name, 'image': t.image_url, 'extra': f"{', '.join(t.artists)} · {t.album or ''}", 'provider': provider} for t in items] elif service == 'itunes': if not itunes_enrichment_worker or not itunes_enrichment_worker.client: @@ -13186,17 +13200,19 @@ def search_match(): # Search for artists if use_hydrabase: artist_matches = hydrabase_client.search_artists(query, limit=8) + provider = 'hydrabase' else: artist_matches = spotify_client.search_artists(query, limit=8) + provider = _detect_provider(artist_matches, spotify_client) results = [] - + for artist in artist_matches: # Calculate confidence based on search similarity confidence = matching_engine.similarity_score( matching_engine.normalize_string(query), matching_engine.normalize_string(artist.name) ) - + results.append({ "artist": { "id": artist.id, @@ -13208,8 +13224,8 @@ def search_match(): }, "confidence": confidence }) - - return jsonify({"results": results}) + + return jsonify({"results": results, "provider": provider}) elif context == 'album': # Search for albums by specific artist @@ -13218,11 +13234,13 @@ def search_match(): if use_hydrabase: # Hydrabase: search albums by query directly album_matches = hydrabase_client.search_albums(query, limit=20) + provider = 'hydrabase' else: if not artist_id: return jsonify({"error": "Artist ID required for album search"}), 400 # Get artist's albums and filter by query album_matches = spotify_client.get_artist_albums(artist_id) + provider = _detect_provider(album_matches, spotify_client) results = [] for album in album_matches: @@ -13248,7 +13266,7 @@ def search_match(): # Sort by confidence results.sort(key=lambda x: x['confidence'], reverse=True) - return jsonify({"results": results[:8]}) + return jsonify({"results": results[:8], "provider": provider}) else: return jsonify({"error": "Invalid context. Must be 'artist' or 'album'"}), 400 @@ -18981,6 +18999,16 @@ def get_version_info(): "title": "What's New in SoulSync", "subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes", "sections": [ + { + "title": "🔧 Fix Spotify Manual Match Storing Wrong IDs", + "description": "Manual match modals no longer store iTunes/Deezer IDs in Spotify ID columns", + "features": [ + "• Detects actual provider from result IDs — Spotify IDs are alphanumeric, iTunes/Deezer are numeric", + "• Match button now stores IDs in the correct service column (itunes_artist_id vs spotify_artist_id)", + "• Results show provider label when falling back (e.g. 'ID: 312095 (itunes)')", + "• Fixes broken Spotify links on artist pages caused by stored iTunes IDs" + ] + }, { "title": "🛡️ Spotify Enrichment Daily Budget", "description": "The background enrichment worker now caps itself at 3,000 items per day to prevent rate limit bans", diff --git a/webui/static/helper.js b/webui/static/helper.js index 3c310e8f..30f8443c 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3403,6 +3403,7 @@ function closeHelperSearch() { const WHATS_NEW = { '2.1': [ // Newest features first + { title: 'Fix Wrong Spotify IDs', desc: 'Manual match no longer stores iTunes IDs as Spotify IDs — detects actual provider from results' }, { title: 'Spotify Enrichment Budget', desc: 'Background enrichment worker caps at 3,000 items/day to prevent rate limit bans — resets at midnight' }, { title: 'Per-Artist Library Sync', desc: 'Validate files and clean stale entries per artist from the enhanced library view', page: 'library', selector: '.library-controls' }, { title: 'Collaborative Album Handling', desc: 'Smart folder naming for multi-artist albums — uses first listed artist', page: 'settings', selector: '#collab-artist-mode' }, diff --git a/webui/static/script.js b/webui/static/script.js index 2c48911e..2eb96bea 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -18530,6 +18530,7 @@ async function performArtistSearch(query) { const data = await response.json(); console.log('Manual search response:', data); + if (data.provider) currentMatchingData.provider = data.provider; if (data.results) { console.log('Results array:', data.results); renderArtistSearchResults(data.results); @@ -42772,14 +42773,15 @@ async function doManualMatchSearch(service, entityType, query, container, entity } const idLine = document.createElement('div'); idLine.className = 'enhanced-match-result-id'; - idLine.textContent = `ID: ${result.id}`; + const providerLabel = result.provider && result.provider !== service ? ` (${result.provider})` : ''; + idLine.textContent = `ID: ${result.id}${providerLabel}`; info.appendChild(idLine); row.appendChild(info); const matchBtn = document.createElement('button'); matchBtn.className = 'enhanced-meta-save-btn'; matchBtn.textContent = 'Match'; - matchBtn.onclick = () => applyManualMatch(entityType, entityId, service, result.id, artistId); + matchBtn.onclick = () => applyManualMatch(entityType, entityId, result.provider || service, result.id, artistId); row.appendChild(matchBtn); container.appendChild(row);