Fix discovery fix search fallback and add Deezer search endpoint

Discovery fix modal now tries the user's active metadata source
first, then falls back through all available sources (Spotify,
Deezer, iTunes) in sequence. Previously hardcoded Spotify with no
fallback, leaving users without Spotify stuck on "Searching...".

New /api/deezer/search_tracks endpoint exposes the existing
DeezerClient.search_tracks() method for the fix modal. Same
request/response format as Spotify and iTunes endpoints.
This commit is contained in:
Broque Thomas 2026-04-08 11:04:22 -07:00
parent 3d96752087
commit e1c5ae0eb5
2 changed files with 86 additions and 34 deletions

View file

@ -30984,6 +30984,48 @@ def search_itunes_tracks():
return jsonify({"error": str(e)}), 500
@app.route('/api/deezer/search_tracks', methods=['GET'])
def search_deezer_tracks():
"""Search for tracks on Deezer - used by discovery fix modal when Deezer is the source"""
try:
track_q = request.args.get('track', '').strip()
artist_q = request.args.get('artist', '').strip()
legacy_query = request.args.get('query', '').strip()
limit = int(request.args.get('limit', 20))
if track_q or artist_q:
parts = []
if track_q:
parts.append(track_q)
if artist_q:
parts.append(artist_q)
query = ' '.join(parts)
elif legacy_query:
query = legacy_query
else:
return jsonify({"error": "Query parameter is required"}), 400
from core.deezer_client import DeezerClient
client = _get_deezer_client()
tracks = client.search_tracks(query, limit=limit)
tracks_dict = [{
'id': t.id,
'name': t.name,
'artists': t.artists,
'album': t.album,
'duration_ms': t.duration_ms,
'image_url': getattr(t, 'image_url', None),
'source': 'deezer'
} for t in tracks]
return jsonify({'tracks': tracks_dict})
except Exception as e:
print(f"❌ Error searching Deezer tracks: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/itunes/album/<album_id>', methods=['GET'])
def get_itunes_album_tracks(album_id):
"""Fetches full track details for a specific iTunes album."""

View file

@ -18414,44 +18414,54 @@ async function searchDiscoveryFix() {
return;
}
// Determine discovery source from state
const identifier = currentDiscoveryFix.identifier;
const state = listenbrainzPlaylistStates[identifier] || youtubePlaylistStates[identifier];
const discoverySource = state?.discovery_source || state?.discoverySource || 'spotify';
const useFallback = discoverySource !== 'spotify';
const resultsContainer = fixModalOverlay.querySelector('#fix-modal-results');
const sourceLabel = discoverySource === 'spotify' ? 'Spotify' : discoverySource === 'deezer' ? 'Deezer' : 'iTunes';
resultsContainer.innerHTML = `<div class="loading">🔍 Searching ${sourceLabel}...</div>`;
// Build search params
const params = new URLSearchParams();
if (trackInput) params.set('track', trackInput);
if (artistInput) params.set('artist', artistInput);
if (!trackInput && !artistInput) {
resultsContainer.innerHTML = '<div class="no-results">Enter a track name or artist.</div>';
return;
}
params.set('limit', '50');
// Use the user's active metadata source first, then fall back to others
const activeSource = (currentMusicSourceName || 'Spotify').toLowerCase();
const allSources = [
{ key: 'spotify', endpoint: '/api/spotify/search_tracks', label: 'Spotify' },
{ key: 'deezer', endpoint: '/api/deezer/search_tracks', label: 'Deezer' },
{ key: 'itunes', endpoint: '/api/itunes/search_tracks', label: 'iTunes' },
];
// Put the active source first, keep others as fallbacks
const activeIdx = allSources.findIndex(s => activeSource.includes(s.key));
const searchSources = activeIdx > 0
? [allSources[activeIdx], ...allSources.filter((_, i) => i !== activeIdx)]
: allSources;
resultsContainer.innerHTML = `<div class="loading">🔍 Searching ${searchSources[0].label}...</div>`;
try {
// Build search params — send track and artist separately for field-specific filtering
const params = new URLSearchParams();
if (trackInput) params.set('track', trackInput);
if (artistInput) params.set('artist', artistInput);
if (!trackInput && !artistInput) {
resultsContainer.innerHTML = '<div class="no-results">Enter a track name or artist.</div>';
return;
for (let i = 0; i < searchSources.length; i++) {
const source = searchSources[i];
try {
const response = await fetch(`${source.endpoint}?${params.toString()}`);
const data = await response.json();
if (data.tracks && data.tracks.length > 0) {
renderDiscoveryFixResults(data.tracks, fixModalOverlay);
return;
}
// No results from this source — show next source status if there is one
if (i < searchSources.length - 1) {
resultsContainer.innerHTML = `<div class="loading">🔍 Trying ${searchSources[i + 1].label}...</div>`;
}
} catch (e) {
console.warn(`Discovery fix search failed on ${source.label}: ${e.message}`);
}
}
params.set('limit', '50');
// Call appropriate search API based on discovery source
const searchEndpoint = useFallback ? '/api/itunes/search_tracks' : '/api/spotify/search_tracks';
const response = await fetch(`${searchEndpoint}?${params.toString()}`);
const data = await response.json();
if (data.error) {
resultsContainer.innerHTML = `<div class="error-message">❌ ${data.error}</div>`;
return;
}
if (!data.tracks || data.tracks.length === 0) {
resultsContainer.innerHTML = '<div class="no-results">No matches found. Try different search terms.</div>';
return;
}
// Render results (same format for both Spotify and iTunes)
renderDiscoveryFixResults(data.tracks, fixModalOverlay);
// All sources exhausted
resultsContainer.innerHTML = '<div class="no-results">No matches found on any source. Try different search terms.</div>';
} catch (error) {
console.error('Search error:', error);