From 7a24431e46770942943aeda27cbc88559e28e8e0 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:20:35 -0700 Subject: [PATCH] Redesign watchlist linked artist section with per-source match controls Replaced single "Change" button with per-source rows showing match status for each provider (Spotify, Apple Music, Deezer, Discogs). Each row has Fix/Match button that searches that specific source API, plus clear button to remove individual matches. - Per-source search uses _search_service (same as enrichment modal) - Backend: added Discogs to valid providers, empty ID clears match - Fixed provider validation to accept 'discogs' alongside others - Clear sets DB column to NULL instead of rejecting empty string --- web_server.py | 33 +++--- webui/static/script.js | 240 ++++++++++++++++++----------------------- webui/static/style.css | 164 ++++++++++++---------------- 3 files changed, 196 insertions(+), 241 deletions(-) diff --git a/web_server.py b/web_server.py index 33458305..95ca611c 100644 --- a/web_server.py +++ b/web_server.py @@ -38629,10 +38629,14 @@ def watchlist_artist_link_provider(artist_id): return jsonify({"success": False, "error": "No data provided"}), 400 new_provider_id = data.get('provider_id', '').strip() - provider = data.get('provider', '').strip() # 'spotify', 'itunes', or 'deezer' + provider = data.get('provider', '').strip() - if not new_provider_id or provider not in ('spotify', 'itunes', 'deezer'): - return jsonify({"success": False, "error": "Missing provider or provider_id"}), 400 + valid_providers = ('spotify', 'itunes', 'deezer', 'discogs') + if provider not in valid_providers: + return jsonify({"success": False, "error": f"Invalid provider. Must be one of: {', '.join(valid_providers)}"}), 400 + + # Empty provider_id = clear the match for this source + is_clear = not new_provider_id conn = sqlite3.connect(str(database.database_path)) cursor = conn.cursor() @@ -38653,22 +38657,27 @@ def watchlist_artist_link_provider(artist_id): artist_name = row[1] # Check for duplicate โ€” another watchlist artist already has this provider ID - col_map = {'spotify': 'spotify_artist_id', 'itunes': 'itunes_artist_id', 'deezer': 'deezer_artist_id'} + col_map = {'spotify': 'spotify_artist_id', 'itunes': 'itunes_artist_id', 'deezer': 'deezer_artist_id', 'discogs': 'discogs_artist_id'} col = col_map[provider] - cursor.execute(f"SELECT id, artist_name FROM watchlist_artists WHERE {col} = ? AND id != ?", - (new_provider_id, watchlist_row_id)) - duplicate = cursor.fetchone() - if duplicate: - conn.close() - return jsonify({"success": False, "error": f"Another watchlist artist ('{duplicate[1]}') already has this {provider} ID"}), 409 + if not is_clear: + cursor.execute(f"SELECT id, artist_name FROM watchlist_artists WHERE {col} = ? AND id != ?", + (new_provider_id, watchlist_row_id)) + duplicate = cursor.fetchone() + if duplicate: + conn.close() + return jsonify({"success": False, "error": f"Another watchlist artist ('{duplicate[1]}') already has this {provider} ID"}), 409 + + # Set to new ID or NULL (clear) + update_val = new_provider_id if not is_clear else None cursor.execute(f"UPDATE watchlist_artists SET {col} = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", - (new_provider_id, watchlist_row_id)) + (update_val, watchlist_row_id)) conn.commit() conn.close() - print(f"โœ… Manually linked watchlist artist '{artist_name}' to {provider} ID: {new_provider_id}") + action = 'Cleared' if is_clear else 'Linked' + print(f"โœ… {action} watchlist artist '{artist_name}' {provider} ID: {new_provider_id or 'NULL'}") return jsonify({ "success": True, diff --git a/webui/static/script.js b/webui/static/script.js index 56c5d5f5..6722ff24 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -39105,197 +39105,165 @@ function _populateLinkedProviderSection(artistId, artistName, spotifyId, itunesI const content = document.getElementById('watchlist-linked-provider-content'); if (!section || !content) return; - // Determine which providers are linked - const hasSpotify = !!spotifyId; - const hasItunes = !!itunesId; - const hasDeezer = !!deezerId; - const hasDiscogs = !!discogsId; - - if (!hasSpotify && !hasItunes && !hasDeezer && !hasDiscogs) { - section.style.display = 'none'; - return; - } - section.style.display = ''; - // Build linked artist display โ€” show a card for each linked provider - // The artist info from the API is for the currently active provider - const linkedName = artistInfo?.name || artistName; - const linkedImage = artistInfo?.image_url || ''; - const nameMatches = linkedName.toLowerCase().trim() === artistName.toLowerCase().trim(); + const sources = [ + { key: 'spotify', label: 'Spotify', icon: '๐ŸŸข', id: spotifyId || '', color: '#1db954' }, + { key: 'itunes', label: 'Apple Music', icon: '๐Ÿ”ด', id: itunesId || '', color: '#fc3c44' }, + { key: 'deezer', label: 'Deezer', icon: '๐ŸŸฃ', id: deezerId || '', color: '#a238ff' }, + { key: 'discogs', label: 'Discogs', icon: '๐ŸŸค', id: discogsId || '', color: '#b08968' }, + ]; - let html = `
`; - - if (linkedImage) { - html += ``; - } else { - html += `
๐ŸŽต
`; + let html = '
'; + for (const src of sources) { + const matched = !!src.id; + const shortId = src.id ? (src.id.length > 16 ? src.id.substring(0, 14) + '...' : src.id) : ''; + html += ` +
+ ${src.icon} + ${src.label} + ${matched + ? `${shortId}` + : 'Not matched' + } + + ${matched ? `` : ''} +
`; } + html += '
'; - html += `
`; - html += `
${escapeHtml(linkedName)}
`; - - // Show provider badges - html += `
`; - if (hasSpotify) { - html += `Spotify`; - } - if (hasItunes) { - html += `iTunes`; - } - if (hasDeezer) { - html += `Deezer`; - } - if (hasDiscogs) { - html += `Discogs`; - } - html += `
`; - - // Show mismatch warning if linked name differs from watchlist name - if (!nameMatches) { - html += `
- โš ๏ธ Name differs from watchlist entry "${escapeHtml(artistName)}" -
`; - } - - html += `
`; // close info - - html += ``; - html += `
`; // close card - - // Search UI (hidden by default) - html += `