Make Spotify a selectable metadata source instead of auto-override

Spotify was automatically the primary metadata source whenever
authenticated, ignoring the user's fallback dropdown selection. Now:

- Dropdown renamed "Fallback Source" → "Primary Source" with Spotify
  added as an option alongside iTunes, Deezer, Discogs
- User's selection drives search results, discovery page, status display
- Enhanced search uses selected primary source first, others as tabs
- Discovery page filters by selected source's artist IDs
- Spotify auth still works for playlists, followed artists, enrichment
- Default is 'deezer' — users who want Spotify select it explicitly
pull/289/head
Broque Thomas 1 month ago
parent db5bdb9c59
commit 3c211eaac8

@ -4424,13 +4424,19 @@ def get_status():
if is_rate_limited or cooldown_remaining > 0: if is_rate_limited or cooldown_remaining > 0:
# During rate limit or post-ban cooldown, skip the auth probe entirely. # During rate limit or post-ban cooldown, skip the auth probe entirely.
# Probing Spotify here would reset the rate limit timer. # Probing Spotify here would reset the rate limit timer.
music_source = _get_metadata_fallback_source() # App uses fallback during ban music_source = _get_metadata_fallback_source()
if music_source == 'spotify':
music_source = 'deezer' # Spotify rate limited — can't use it
spotify_response_time = 0 spotify_response_time = 0
else: else:
spotify_start = time.time() spotify_start = time.time()
spotify_connected = spotify_client.is_spotify_authenticated() if spotify_client else False spotify_connected = spotify_client.is_spotify_authenticated() if spotify_client else False
spotify_response_time = (time.time() - spotify_start) * 1000 spotify_response_time = (time.time() - spotify_start) * 1000
music_source = 'spotify' if spotify_connected else _get_metadata_fallback_source() # Use whatever the user configured as primary metadata source
music_source = _get_metadata_fallback_source()
# If user selected spotify but it's not authed, fall back
if music_source == 'spotify' and not spotify_connected:
music_source = 'deezer'
_status_cache['spotify'] = { _status_cache['spotify'] = {
'connected': True, # Always true — iTunes fallback is always available 'connected': True, # Always true — iTunes fallback is always available
@ -5981,7 +5987,7 @@ def test_connection_endpoint():
current_time = time.time() current_time = time.time()
if service == 'spotify': if service == 'spotify':
_status_cache['spotify']['connected'] = True _status_cache['spotify']['connected'] = True
_status_cache['spotify']['source'] = 'spotify' if (spotify_client and spotify_client.is_spotify_authenticated()) else _get_metadata_fallback_source() _status_cache['spotify']['source'] = _get_metadata_fallback_source()
_status_cache_timestamps['spotify'] = current_time _status_cache_timestamps['spotify'] = current_time
print("✅ Updated Spotify status cache after successful test") print("✅ Updated Spotify status cache after successful test")
elif service in ['plex', 'jellyfin', 'navidrome']: elif service in ['plex', 'jellyfin', 'navidrome']:
@ -6031,7 +6037,7 @@ def test_dashboard_connection_endpoint():
current_time = time.time() current_time = time.time()
if service == 'spotify': if service == 'spotify':
_status_cache['spotify']['connected'] = True _status_cache['spotify']['connected'] = True
_status_cache['spotify']['source'] = 'spotify' if (spotify_client and spotify_client.is_spotify_authenticated()) else _get_metadata_fallback_source() _status_cache['spotify']['source'] = _get_metadata_fallback_source()
_status_cache_timestamps['spotify'] = current_time _status_cache_timestamps['spotify'] = current_time
print("✅ Updated Spotify status cache after successful dashboard test") print("✅ Updated Spotify status cache after successful dashboard test")
elif service in ['plex', 'jellyfin', 'navidrome']: elif service in ['plex', 'jellyfin', 'navidrome']:
@ -7698,23 +7704,22 @@ def enhanced_search():
hydrabase_worker.enqueue(query, 'albums') hydrabase_worker.enqueue(query, 'albums')
hydrabase_worker.enqueue(query, 'artists') hydrabase_worker.enqueue(query, 'artists')
# Search primary source synchronously — use is_spotify_authenticated() # Search using the user's configured primary metadata source
# (is_authenticated() always returns True because fallback is always available) fb_source = _get_metadata_fallback_source()
if spotify_client and spotify_client.is_spotify_authenticated(): try:
try: primary_results = _enhanced_search_source(query, _get_metadata_fallback_client())
primary_results = _enhanced_search_source(query, spotify_client) primary_source = fb_source
primary_source = "spotify" except Exception as e:
except Exception as e: logger.debug(f"Primary source ({fb_source}) search failed: {e}")
logger.debug(f"Spotify search failed: {e}")
# If Spotify unavailable, use configured fallback as primary # If primary source failed and it wasn't Spotify, try Spotify as fallback
if primary_results is empty_source: if primary_results is empty_source and fb_source != 'spotify':
fb_source = _get_metadata_fallback_source() if spotify_client and spotify_client.is_spotify_authenticated():
try: try:
primary_results = _enhanced_search_source(query, _get_metadata_fallback_client()) primary_results = _enhanced_search_source(query, spotify_client)
primary_source = fb_source primary_source = "spotify"
except Exception as e: except Exception as e:
logger.debug(f"Fallback ({fb_source}) search failed: {e}") logger.debug(f"Spotify fallback search failed: {e}")
# Determine which alternate sources are available (for frontend to fetch async) # Determine which alternate sources are available (for frontend to fetch async)
spotify_available = bool(spotify_client and spotify_client.is_spotify_authenticated()) spotify_available = bool(spotify_client and spotify_client.is_spotify_authenticated())
@ -32748,16 +32753,22 @@ def _get_deezer_client():
return _deezer_client_instance return _deezer_client_instance
def _get_metadata_fallback_source(): def _get_metadata_fallback_source():
"""Get the configured metadata fallback source ('itunes', 'deezer', 'discogs', or 'hydrabase').""" """Get the configured primary metadata source.
Returns 'spotify', 'itunes', 'deezer', 'discogs', or 'hydrabase'."""
try: try:
return config_manager.get('metadata.fallback_source', 'deezer') or 'deezer' return config_manager.get('metadata.fallback_source', 'deezer') or 'deezer'
except Exception: except Exception:
return 'itunes' return 'deezer'
def _get_metadata_fallback_client(): def _get_metadata_fallback_client():
"""Get the active metadata fallback client based on settings. """Get the active metadata client based on settings.
Returns an iTunesClient, DeezerClient, DiscogsClient, or HydrabaseClient instance with identical interfaces.""" Returns a SpotifyClient, iTunesClient, DeezerClient, DiscogsClient, or HydrabaseClient instance."""
source = _get_metadata_fallback_source() source = _get_metadata_fallback_source()
if source == 'spotify':
if spotify_client and spotify_client.is_spotify_authenticated():
return spotify_client
# Spotify selected but not authed — fall back to deezer
return _get_deezer_client()
if source == 'deezer': if source == 'deezer':
return _get_deezer_client() return _get_deezer_client()
if source == 'discogs': if source == 'discogs':
@ -32765,7 +32776,6 @@ def _get_metadata_fallback_client():
if token: if token:
from core.discogs_client import DiscogsClient from core.discogs_client import DiscogsClient
return DiscogsClient(token=token) return DiscogsClient(token=token)
# No token — fall back to iTunes
from core.itunes_client import iTunesClient from core.itunes_client import iTunesClient
return iTunesClient() return iTunesClient()
if source == 'hydrabase': if source == 'hydrabase':
@ -39499,11 +39509,13 @@ metadata_update_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix=
def _get_active_discovery_source(): def _get_active_discovery_source():
""" """
Determine which music source is active for discovery. Determine which music source is active for discovery.
Returns 'spotify' if Spotify is authenticated, otherwise the configured fallback. Returns the user's configured primary metadata source.
If the selected source requires auth and isn't available, falls back.
""" """
if spotify_client and spotify_client.is_spotify_authenticated(): source = _get_metadata_fallback_source()
return 'spotify' if source == 'spotify' and not (spotify_client and spotify_client.is_spotify_authenticated()):
return _get_metadata_fallback_source() return 'deezer'
return source
@app.route('/api/discover/hero', methods=['GET']) @app.route('/api/discover/hero', methods=['GET'])

@ -4235,15 +4235,16 @@
<div class="api-service-frame"> <div class="api-service-frame">
<h4 class="service-title" style="color: #e8e8e8;">Metadata Source</h4> <h4 class="service-title" style="color: #e8e8e8;">Metadata Source</h4>
<div class="form-group"> <div class="form-group">
<label>Fallback Source:</label> <label>Primary Source:</label>
<select id="metadata-fallback-source"> <select id="metadata-fallback-source">
<option value="spotify">Spotify</option>
<option value="itunes">iTunes / Apple Music</option> <option value="itunes">iTunes / Apple Music</option>
<option value="deezer">Deezer</option> <option value="deezer">Deezer</option>
<option value="discogs">Discogs</option> <option value="discogs">Discogs</option>
</select> </select>
</div> </div>
<div class="callback-info"> <div class="callback-info">
<div class="callback-help">When Spotify is not connected, this source provides artist, album, and track metadata. Discogs requires a personal token configured above.</div> <div class="callback-help">The primary source for artist, album, and track metadata. Spotify requires authentication above. Discogs requires a personal token.</div>
</div> </div>
</div> </div>

Loading…
Cancel
Save