Rename status cache to metadata_source

Expose the primary metadata provider status under a generic cache key and update the websocket fixture plus frontend readers to match.
This commit is contained in:
Antti Kettunen 2026-05-02 10:42:33 +03:00
parent 1d9d399a2f
commit 36267618a3
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
8 changed files with 73 additions and 73 deletions

View file

@ -17,7 +17,7 @@ from flask_socketio import SocketIO, join_room, leave_room
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_DEFAULT_STATUS_CACHE = { _DEFAULT_STATUS_CACHE = {
'spotify': {'connected': True, 'authenticated': True, 'response_time': 12.5, 'source': 'spotify'}, 'metadata_source': {'connected': True, 'authenticated': True, 'response_time': 12.5, 'source': 'spotify'},
'media_server': {'connected': True, 'response_time': 8.1, 'type': 'plex'}, 'media_server': {'connected': True, 'response_time': 8.1, 'type': 'plex'},
'soulseek': {'connected': True, 'response_time': 5.3, 'source': 'soulseek'}, 'soulseek': {'connected': True, 'response_time': 5.3, 'source': 'soulseek'},
} }
@ -255,7 +255,7 @@ wishlist_stats_state = copy.deepcopy(_DEFAULT_WISHLIST_STATS)
def _build_status_payload(): def _build_status_payload():
return { return {
'spotify': dict(_status_cache['spotify']), 'metadata_source': dict(_status_cache['metadata_source']),
'media_server': dict(_status_cache['media_server']), 'media_server': dict(_status_cache['media_server']),
'soulseek': dict(_status_cache['soulseek']), 'soulseek': dict(_status_cache['soulseek']),
'active_media_server': _status_cache['media_server'].get('type', 'plex'), 'active_media_server': _status_cache['media_server'].get('type', 'plex'),

View file

@ -52,11 +52,11 @@ class TestServiceStatus:
status_events = [e for e in received if e['name'] == 'status:update'] status_events = [e for e in received if e['name'] == 'status:update']
assert len(status_events) >= 1 assert len(status_events) >= 1
data = status_events[0]['args'][0] data = status_events[0]['args'][0]
assert 'spotify' in data assert 'metadata_source' in data
assert 'media_server' in data assert 'media_server' in data
assert 'soulseek' in data assert 'soulseek' in data
assert 'active_media_server' in data assert 'active_media_server' in data
assert 'authenticated' in data['spotify'] assert 'authenticated' in data['metadata_source']
def test_status_matches_http(self, test_app, shared_state): def test_status_matches_http(self, test_app, shared_state):
"""Socket event data matches HTTP endpoint response exactly.""" """Socket event data matches HTTP endpoint response exactly."""
@ -73,7 +73,7 @@ class TestServiceStatus:
assert len(status_events) >= 1 assert len(status_events) >= 1
ws_data = status_events[0]['args'][0] ws_data = status_events[0]['args'][0]
assert ws_data['spotify'] == http_data['spotify'] assert ws_data['metadata_source'] == http_data['metadata_source']
assert ws_data['media_server'] == http_data['media_server'] assert ws_data['media_server'] == http_data['media_server']
assert ws_data['soulseek'] == http_data['soulseek'] assert ws_data['soulseek'] == http_data['soulseek']
assert ws_data['active_media_server'] == http_data['active_media_server'] assert ws_data['active_media_server'] == http_data['active_media_server']
@ -86,13 +86,13 @@ class TestServiceStatus:
build = shared_state['build_status_payload'] build = shared_state['build_status_payload']
# Mutate cache # Mutate cache
status_cache['spotify']['source'] = 'itunes' status_cache['metadata_source']['source'] = 'itunes'
socketio.emit('status:update', build()) socketio.emit('status:update', build())
received = client.get_received() received = client.get_received()
status_events = [e for e in received if e['name'] == 'status:update'] status_events = [e for e in received if e['name'] == 'status:update']
data = status_events[-1]['args'][0] data = status_events[-1]['args'][0]
assert data['spotify']['source'] == 'itunes' assert data['metadata_source']['source'] == 'itunes'
# ========================================================================= # =========================================================================

View file

@ -811,12 +811,12 @@ _idle_since = {}
_IDLE_GRACE_SECONDS = 5 _IDLE_GRACE_SECONDS = 5
_status_cache = { _status_cache = {
'spotify': {'connected': False, 'authenticated': False, 'response_time': 0, 'source': 'itunes'}, 'metadata_source': {'connected': False, 'authenticated': False, 'response_time': 0, 'source': 'itunes'},
'media_server': {'connected': False, 'response_time': 0, 'type': None}, 'media_server': {'connected': False, 'response_time': 0, 'type': None},
'soulseek': {'connected': False, 'response_time': 0}, 'soulseek': {'connected': False, 'response_time': 0},
} }
_status_cache_timestamps: dict[str, float] = { _status_cache_timestamps: dict[str, float] = {
'spotify': 0, 'metadata_source': 0,
'media_server': 0, 'media_server': 0,
'soulseek': 0, 'soulseek': 0,
} }
@ -3447,12 +3447,12 @@ def get_status():
active_server = config_manager.get_active_media_server() active_server = config_manager.get_active_media_server()
# Test Spotify - with caching to avoid excessive API calls # Test Spotify - with caching to avoid excessive API calls
if current_time - _status_cache_timestamps['spotify'] > STATUS_CACHE_TTL: if current_time - _status_cache_timestamps['metadata_source'] > STATUS_CACHE_TTL:
# Guard against spotify_client being None (partial init) # Guard against spotify_client being None (partial init)
is_rate_limited = spotify_client.is_rate_limited() if spotify_client else False is_rate_limited = spotify_client.is_rate_limited() if spotify_client else False
rate_limit_info = spotify_client.get_rate_limit_info() if (spotify_client and is_rate_limited) else None rate_limit_info = spotify_client.get_rate_limit_info() if (spotify_client and is_rate_limited) else None
cooldown_remaining = spotify_client.get_post_ban_cooldown_remaining() if spotify_client else 0 cooldown_remaining = spotify_client.get_post_ban_cooldown_remaining() if spotify_client else 0
spotify_session_active = spotify_client.is_spotify_authenticated() if spotify_client else False metadata_source_authenticated = spotify_client.is_spotify_authenticated() if spotify_client else False
# Read configured source once — no auth validation here, we do that explicitly below # Read configured source once — no auth validation here, we do that explicitly below
configured_source = config_manager.get('metadata.fallback_source', 'deezer') or 'deezer' configured_source = config_manager.get('metadata.fallback_source', 'deezer') or 'deezer'
@ -3461,27 +3461,27 @@ def get_status():
# 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 = 'deezer' if configured_source == 'spotify' else configured_source music_source = 'deezer' if configured_source == 'spotify' else configured_source
spotify_response_time = 0 metadata_source_response_time = 0
else: else:
spotify_start = time.time() metadata_source_start = time.time()
spotify_connected = spotify_client.is_spotify_authenticated() if spotify_client else False metadata_source_connected = spotify_client.is_spotify_authenticated() if spotify_client else False
spotify_response_time = (time.time() - spotify_start) * 1000 metadata_source_response_time = (time.time() - metadata_source_start) * 1000
# Use configured source; fall back to deezer only if Spotify isn't authenticated # Use configured source; fall back to deezer only if Spotify isn't authenticated
if configured_source == 'spotify': if configured_source == 'spotify':
music_source = 'spotify' if spotify_connected else 'deezer' music_source = 'spotify' if metadata_source_connected else 'deezer'
else: else:
music_source = configured_source music_source = configured_source
_status_cache['spotify'] = { _status_cache['metadata_source'] = {
'connected': spotify_session_active, 'connected': metadata_source_authenticated,
'authenticated': spotify_session_active, 'authenticated': metadata_source_authenticated,
'response_time': round(spotify_response_time, 1), 'response_time': round(metadata_source_response_time, 1),
'source': music_source, 'source': music_source,
'rate_limited': is_rate_limited, 'rate_limited': is_rate_limited,
'rate_limit': rate_limit_info, 'rate_limit': rate_limit_info,
'post_ban_cooldown': cooldown_remaining if cooldown_remaining > 0 else None 'post_ban_cooldown': cooldown_remaining if cooldown_remaining > 0 else None
} }
_status_cache_timestamps['spotify'] = current_time _status_cache_timestamps['metadata_source'] = current_time
# else: use cached value # else: use cached value
# Test media server - use EXISTING instances (they have internal caching) # Test media server - use EXISTING instances (they have internal caching)
@ -3556,7 +3556,7 @@ def get_status():
active_dl_count += 1 active_dl_count += 1
status_data = { status_data = {
'spotify': _status_cache['spotify'], 'metadata_source': _status_cache['metadata_source'],
'media_server': _status_cache['media_server'], 'media_server': _status_cache['media_server'],
'soulseek': _status_cache['soulseek'], 'soulseek': _status_cache['soulseek'],
'active_media_server': active_server, 'active_media_server': active_server,
@ -4186,7 +4186,7 @@ def handle_settings():
if tidal_enrichment_worker: if tidal_enrichment_worker:
tidal_enrichment_worker.client = tidal_client tidal_enrichment_worker.client = tidal_client
# Invalidate status cache so next poll reflects new settings (e.g. fallback source change) # Invalidate status cache so next poll reflects new settings (e.g. fallback source change)
_status_cache_timestamps['spotify'] = 0 _status_cache_timestamps['metadata_source'] = 0
logger.info("Service clients re-initialized with new settings.") logger.info("Service clients re-initialized with new settings.")
return jsonify({"success": True, "message": "Settings saved successfully."}) return jsonify({"success": True, "message": "Settings saved successfully."})
except Exception as e: except Exception as e:
@ -4804,10 +4804,10 @@ def test_connection_endpoint():
current_time = time.time() current_time = time.time()
if service == 'spotify': if service == 'spotify':
spotify_session_active = spotify_client.is_spotify_authenticated() if spotify_client else False spotify_session_active = spotify_client.is_spotify_authenticated() if spotify_client else False
_status_cache['spotify']['connected'] = True _status_cache['metadata_source']['connected'] = True
_status_cache['spotify']['authenticated'] = spotify_session_active _status_cache['metadata_source']['authenticated'] = spotify_session_active
_status_cache['spotify']['source'] = _get_metadata_fallback_source() _status_cache['metadata_source']['source'] = _get_metadata_fallback_source()
_status_cache_timestamps['spotify'] = current_time _status_cache_timestamps['metadata_source'] = current_time
logger.info("Updated Spotify status cache after successful test") logger.info("Updated Spotify status cache after successful test")
elif service in ['plex', 'jellyfin', 'navidrome', 'soulsync']: elif service in ['plex', 'jellyfin', 'navidrome', 'soulsync']:
_status_cache['media_server']['connected'] = True _status_cache['media_server']['connected'] = True
@ -4972,10 +4972,10 @@ def test_dashboard_connection_endpoint():
current_time = time.time() current_time = time.time()
if service == 'spotify': if service == 'spotify':
spotify_session_active = spotify_client.is_spotify_authenticated() if spotify_client else False spotify_session_active = spotify_client.is_spotify_authenticated() if spotify_client else False
_status_cache['spotify']['connected'] = True _status_cache['metadata_source']['connected'] = True
_status_cache['spotify']['authenticated'] = spotify_session_active _status_cache['metadata_source']['authenticated'] = spotify_session_active
_status_cache['spotify']['source'] = _get_metadata_fallback_source() _status_cache['metadata_source']['source'] = _get_metadata_fallback_source()
_status_cache_timestamps['spotify'] = current_time _status_cache_timestamps['metadata_source'] = current_time
logger.info("Updated Spotify status cache after successful dashboard test") logger.info("Updated Spotify status cache after successful dashboard test")
elif service in ['plex', 'jellyfin', 'navidrome', 'soulsync']: elif service in ['plex', 'jellyfin', 'navidrome', 'soulsync']:
_status_cache['media_server']['connected'] = True _status_cache['media_server']['connected'] = True
@ -5854,7 +5854,7 @@ def spotify_callback():
return _spotify_auth_result_page("Your personal Spotify account is now connected. You can close this window.", authenticated=True) return _spotify_auth_result_page("Your personal Spotify account is now connected. You can close this window.", authenticated=True)
if profile_client: if profile_client:
profile_client._invalidate_auth_cache() profile_client._invalidate_auth_cache()
_status_cache_timestamps['spotify'] = 0 _status_cache_timestamps['metadata_source'] = 0
add_activity_item("", "Spotify Auth Warning", f"Profile {profile_id_from_state} completed OAuth but Spotify did not confirm an authenticated session", "Now") add_activity_item("", "Spotify Auth Warning", f"Profile {profile_id_from_state} completed OAuth but Spotify did not confirm an authenticated session", "Now")
return _spotify_auth_result_page( return _spotify_auth_result_page(
"Spotify authorization completed, but SoulSync could not confirm an authenticated Spotify session for this profile. You can close this window and try Authenticate again.", "Spotify authorization completed, but SoulSync could not confirm an authenticated Spotify session for this profile. You can close this window and try Authenticate again.",
@ -5890,7 +5890,7 @@ def spotify_callback():
_clear_rate_limit() _clear_rate_limit()
spotify_client._invalidate_auth_cache() spotify_client._invalidate_auth_cache()
# Invalidate status cache so next poll picks up the new connection # Invalidate status cache so next poll picks up the new connection
_status_cache_timestamps['spotify'] = 0 _status_cache_timestamps['metadata_source'] = 0
# Refresh enrichment worker's client so it picks up new auth # Refresh enrichment worker's client so it picks up new auth
if spotify_enrichment_worker and hasattr(spotify_enrichment_worker, 'client'): if spotify_enrichment_worker and hasattr(spotify_enrichment_worker, 'client'):
spotify_enrichment_worker.client.reload_config() spotify_enrichment_worker.client.reload_config()
@ -5900,7 +5900,7 @@ def spotify_callback():
else: else:
logger.warning("Spotify OAuth token exchange succeeded but authentication validation failed") logger.warning("Spotify OAuth token exchange succeeded but authentication validation failed")
spotify_client._invalidate_auth_cache() spotify_client._invalidate_auth_cache()
_status_cache_timestamps['spotify'] = 0 _status_cache_timestamps['metadata_source'] = 0
add_activity_item("", "Spotify Auth Warning", "OAuth completed, but Spotify did not confirm an authenticated session", "Now") add_activity_item("", "Spotify Auth Warning", "OAuth completed, but Spotify did not confirm an authenticated session", "Now")
return _spotify_auth_result_page( return _spotify_auth_result_page(
"Spotify authorization completed, but SoulSync could not confirm an authenticated Spotify session. You can close this window and try Authenticate again.", "Spotify authorization completed, but SoulSync could not confirm an authenticated Spotify session. You can close this window and try Authenticate again.",
@ -5929,7 +5929,7 @@ def spotify_disconnect():
source_label = get_metadata_source_label(active_source) source_label = get_metadata_source_label(active_source)
if configured_source == 'spotify': if configured_source == 'spotify':
config_manager.set('metadata.fallback_source', active_source) config_manager.set('metadata.fallback_source', active_source)
_status_cache['spotify'] = { _status_cache['metadata_source'] = {
'connected': False, 'connected': False,
'authenticated': False, 'authenticated': False,
'response_time': 0, 'response_time': 0,
@ -5938,7 +5938,7 @@ def spotify_disconnect():
'rate_limit': None, 'rate_limit': None,
'post_ban_cooldown': None 'post_ban_cooldown': None
} }
_status_cache_timestamps['spotify'] = time.time() _status_cache_timestamps['metadata_source'] = time.time()
add_activity_item("", "Spotify Disconnected", f"Using {source_label} for metadata", "Now") add_activity_item("", "Spotify Disconnected", f"Using {source_label} for metadata", "Now")
return jsonify({ return jsonify({
'success': True, 'success': True,
@ -32073,7 +32073,7 @@ def start_oauth_callback_servers():
_clear_rate_limit() _clear_rate_limit()
spotify_client._invalidate_auth_cache() spotify_client._invalidate_auth_cache()
# Invalidate status cache so next poll picks up the new connection # Invalidate status cache so next poll picks up the new connection
_status_cache_timestamps['spotify'] = 0 _status_cache_timestamps['metadata_source'] = 0
# Refresh enrichment worker's client so it picks up new auth # Refresh enrichment worker's client so it picks up new auth
if spotify_enrichment_worker and hasattr(spotify_enrichment_worker, 'client'): if spotify_enrichment_worker and hasattr(spotify_enrichment_worker, 'client'):
spotify_enrichment_worker.client.reload_config() spotify_enrichment_worker.client.reload_config()
@ -32086,7 +32086,7 @@ def start_oauth_callback_servers():
else: else:
_oauth_logger.warning("Spotify token exchange succeeded but authentication validation failed") _oauth_logger.warning("Spotify token exchange succeeded but authentication validation failed")
spotify_client._invalidate_auth_cache() spotify_client._invalidate_auth_cache()
_status_cache_timestamps['spotify'] = 0 _status_cache_timestamps['metadata_source'] = 0
add_activity_item("", "Spotify Auth Warning", "OAuth completed, but Spotify did not confirm an authenticated session", "Now") add_activity_item("", "Spotify Auth Warning", "OAuth completed, but Spotify did not confirm an authenticated session", "Now")
self.send_response(200) self.send_response(200)
self.send_header('Content-type', 'text/html') self.send_header('Content-type', 'text/html')
@ -34053,14 +34053,14 @@ def _build_status_payload():
# Always include fresh rate limit info (it changes over time as ban expires) # Always include fresh rate limit info (it changes over time as ban expires)
# Call is_rate_limited() first to ensure ban-end timestamp is recorded for cooldown # Call is_rate_limited() first to ensure ban-end timestamp is recorded for cooldown
spotify_data = dict(_status_cache.get('spotify', {})) metadata_source_data = dict(_status_cache.get('metadata_source', {}))
is_rl = spotify_client.is_rate_limited() if spotify_client else False is_rl = spotify_client.is_rate_limited() if spotify_client else False
rate_limit_info = spotify_client.get_rate_limit_info() if is_rl else None rate_limit_info = spotify_client.get_rate_limit_info() if is_rl else None
cooldown_remaining = spotify_client.get_post_ban_cooldown_remaining() if spotify_client else 0 cooldown_remaining = spotify_client.get_post_ban_cooldown_remaining() if spotify_client else 0
spotify_data['rate_limited'] = is_rl metadata_source_data['rate_limited'] = is_rl
spotify_data['rate_limit'] = rate_limit_info metadata_source_data['rate_limit'] = rate_limit_info
if cooldown_remaining > 0: if cooldown_remaining > 0:
spotify_data['post_ban_cooldown'] = cooldown_remaining metadata_source_data['post_ban_cooldown'] = cooldown_remaining
# Count active downloads for nav badge # Count active downloads for nav badge
active_dl_count = 0 active_dl_count = 0
@ -34073,7 +34073,7 @@ def _build_status_payload():
pass pass
return { return {
'spotify': spotify_data, 'metadata_source': metadata_source_data,
'media_server': _status_cache.get('media_server', {}), 'media_server': _status_cache.get('media_server', {}),
'soulseek': soulseek_data, 'soulseek': soulseek_data,
'active_media_server': config_manager.get_active_media_server(), 'active_media_server': config_manager.get_active_media_server(),

View file

@ -471,24 +471,24 @@ function initializeWebSocket() {
function handleServiceStatusUpdate(data) { function handleServiceStatusUpdate(data) {
// Cache for library status card // Cache for library status card
_lastServiceStatus = data; _lastStatusPayload = data;
if (typeof syncSpotifySettingsAuthState === 'function') { if (typeof syncSpotifySettingsAuthState === 'function') {
syncSpotifySettingsAuthState(data?.spotify || null); syncSpotifySettingsAuthState(data?.metadata_source || null);
} }
if (typeof syncPrimaryMetadataSourceAvailability === 'function') { if (typeof syncPrimaryMetadataSourceAvailability === 'function') {
syncPrimaryMetadataSourceAvailability(data?.spotify || null); syncPrimaryMetadataSourceAvailability(data?.metadata_source || null);
} }
if (typeof sanitizeMetadataSourceSelection === 'function') { if (typeof sanitizeMetadataSourceSelection === 'function') {
sanitizeMetadataSourceSelection({ quiet: true }); sanitizeMetadataSourceSelection({ quiet: true });
} }
// Same logic as fetchAndUpdateServiceStatus response handler // Same logic as fetchAndUpdateServiceStatus response handler
updateServiceStatus('spotify', data.spotify); updateServiceStatus('spotify', data.metadata_source);
updateServiceStatus('media-server', data.media_server); updateServiceStatus('media-server', data.media_server);
updateServiceStatus('soulseek', data.soulseek); updateServiceStatus('soulseek', data.soulseek);
updateSidebarServiceStatus('spotify', data.spotify); updateSidebarServiceStatus('spotify', data.metadata_source);
updateSidebarServiceStatus('media-server', data.media_server); updateSidebarServiceStatus('media-server', data.media_server);
updateSidebarServiceStatus('soulseek', data.soulseek); updateSidebarServiceStatus('soulseek', data.soulseek);
@ -514,10 +514,10 @@ function handleServiceStatusUpdate(data) {
if (data.enrichment) renderEnrichmentCards(data.enrichment); if (data.enrichment) renderEnrichmentCards(data.enrichment);
// Spotify rate limit / cooldown / recovery // Spotify rate limit / cooldown / recovery
if (data.spotify?.rate_limited && data.spotify.rate_limit) { if (data.metadata_source?.rate_limited && data.metadata_source.rate_limit) {
handleSpotifyRateLimit(data.spotify.rate_limit); handleSpotifyRateLimit(data.metadata_source.rate_limit);
_spotifyInCooldown = false; _spotifyInCooldown = false;
} else if (data.spotify?.post_ban_cooldown > 0) { } else if (data.metadata_source?.post_ban_cooldown > 0) {
if (_spotifyRateLimitShown && !_spotifyInCooldown) { if (_spotifyRateLimitShown && !_spotifyInCooldown) {
_spotifyRateLimitShown = false; _spotifyRateLimitShown = false;
_spotifyInCooldown = true; _spotifyInCooldown = true;
@ -881,12 +881,12 @@ const API = {
} }
}; };
// Track last service status for library card (used by websocket handler in core + artists) // Track the last `/status` payload (shared service snapshot used across the UI)
let _lastServiceStatus = null; let _lastStatusPayload = null;
let _isSoulsyncStandalone = false; // Global flag: true when no media server (sync buttons hidden) let _isSoulsyncStandalone = false; // Global flag: true when no media server (sync buttons hidden)
function getActiveMetadataSource() { function getActiveMetadataSource() {
return _lastServiceStatus?.spotify?.source || 'spotify'; return _lastStatusPayload?.metadata_source?.source || 'spotify';
} }
// =============================== // ===============================

View file

@ -2974,13 +2974,13 @@ async function _checkSetupStatus() {
const completion = _getSetupCompletion(); const completion = _getSetupCompletion();
const results = { ...completion }; const results = { ...completion };
// ── /status — checks services (spotify, media_server, soulseek) ───── // ── /status — checks metadata_source, media_server, soulseek ────────
try { try {
const resp = await fetch('/status'); const resp = await fetch('/status');
if (resp.ok) { if (resp.ok) {
const data = await resp.json(); const data = await resp.json();
// Metadata source is available when status reports a source. // Metadata source is available when status reports a source.
if (data.spotify?.source) { if (data.metadata_source?.source) {
results['metadata-source'] = results['metadata-source'] || Date.now(); results['metadata-source'] = results['metadata-source'] || Date.now();
_markSetupComplete('metadata-source'); _markSetupComplete('metadata-source');
} }

View file

@ -60,7 +60,7 @@ function syncMetadataSourceSelection(source) {
function _isMetadataSourceSelectable(source) { function _isMetadataSourceSelectable(source) {
if (source === 'spotify') { if (source === 'spotify') {
return _lastServiceStatus?.spotify?.authenticated === true; return _lastStatusPayload?.metadata_source?.authenticated === true;
} }
if (source === 'discogs') { if (source === 'discogs') {
const token = document.getElementById('discogs-token'); const token = document.getElementById('discogs-token');
@ -173,7 +173,7 @@ function initializeSettings() {
if (discogsTokenInput) { if (discogsTokenInput) {
discogsTokenInput.addEventListener('input', () => { discogsTokenInput.addEventListener('input', () => {
if (typeof syncPrimaryMetadataSourceAvailability === 'function') { if (typeof syncPrimaryMetadataSourceAvailability === 'function') {
syncPrimaryMetadataSourceAvailability(_lastServiceStatus?.spotify || null); syncPrimaryMetadataSourceAvailability(_lastStatusPayload?.metadata_source || null);
} }
sanitizeMetadataSourceSelection({ quiet: true }); sanitizeMetadataSourceSelection({ quiet: true });
}); });
@ -207,10 +207,10 @@ function initializeSettings() {
// Test button event listeners removed - they use onclick attributes in HTML to avoid double firing // Test button event listeners removed - they use onclick attributes in HTML to avoid double firing
if (typeof syncPrimaryMetadataSourceAvailability === 'function') { if (typeof syncPrimaryMetadataSourceAvailability === 'function') {
syncPrimaryMetadataSourceAvailability(_lastServiceStatus?.spotify || null); syncPrimaryMetadataSourceAvailability(_lastStatusPayload?.metadata_source || null);
} }
syncSpotifySettingsAuthState(_lastServiceStatus?.spotify || null); syncSpotifySettingsAuthState(_lastStatusPayload?.metadata_source || null);
syncMetadataSourceSelection(_lastServiceStatus?.spotify?.source); syncMetadataSourceSelection(_lastStatusPayload?.metadata_source?.source);
sanitizeMetadataSourceSelection({ quiet: true }); sanitizeMetadataSourceSelection({ quiet: true });
if (metadataSourceSelect) { if (metadataSourceSelect) {
metadataSourceSelect.dataset.lastValidSource = metadataSourceSelect.value; metadataSourceSelect.dataset.lastValidSource = metadataSourceSelect.value;
@ -408,7 +408,7 @@ async function applyServiceStatusGradients() {
else header.appendChild(spinner); else header.appendChild(spinner);
} }
}); });
syncSpotifySettingsAuthState(_lastServiceStatus?.spotify || null); syncSpotifySettingsAuthState(_lastStatusPayload?.metadata_source || null);
} catch (e) { } catch (e) {
console.warn('[Settings Status] Failed to apply gradients:', e); console.warn('[Settings Status] Failed to apply gradients:', e);
} }
@ -2544,7 +2544,7 @@ async function saveSettings(quiet = false) {
const discogsTokenInput = document.getElementById('discogs-token'); const discogsTokenInput = document.getElementById('discogs-token');
const discogsTokenPresent = !!discogsTokenInput?.value?.trim(); const discogsTokenPresent = !!discogsTokenInput?.value?.trim();
let metadataSource = metadataSourceSelect?.value || 'itunes'; let metadataSource = metadataSourceSelect?.value || 'itunes';
const spotifySessionActive = _lastServiceStatus?.spotify?.authenticated === true; const spotifySessionActive = _lastStatusPayload?.metadata_source?.authenticated === true;
if (metadataSource === 'spotify' && !spotifySessionActive) { if (metadataSource === 'spotify' && !spotifySessionActive) {
metadataSource = 'deezer'; metadataSource = 'deezer';
if (metadataSourceSelect) metadataSourceSelect.value = metadataSource; if (metadataSourceSelect) metadataSourceSelect.value = metadataSource;

View file

@ -3143,25 +3143,25 @@ async function fetchAndUpdateServiceStatus() {
const data = await response.json(); const data = await response.json();
// Cache for library status card // Cache for library status card
_lastServiceStatus = data; _lastStatusPayload = data;
if (typeof syncSpotifySettingsAuthState === 'function') { if (typeof syncSpotifySettingsAuthState === 'function') {
syncSpotifySettingsAuthState(data?.spotify || null); syncSpotifySettingsAuthState(data?.metadata_source || null);
} }
if (typeof syncPrimaryMetadataSourceAvailability === 'function') { if (typeof syncPrimaryMetadataSourceAvailability === 'function') {
syncPrimaryMetadataSourceAvailability(data?.spotify || null); syncPrimaryMetadataSourceAvailability(data?.metadata_source || null);
} }
if (typeof sanitizeMetadataSourceSelection === 'function') { if (typeof sanitizeMetadataSourceSelection === 'function') {
sanitizeMetadataSourceSelection({ quiet: true }); sanitizeMetadataSourceSelection({ quiet: true });
} }
// Update service status indicators and text (dashboard) // Update service status indicators and text (dashboard)
updateServiceStatus('spotify', data.spotify); updateServiceStatus('spotify', data.metadata_source);
updateServiceStatus('media-server', data.media_server); updateServiceStatus('media-server', data.media_server);
updateServiceStatus('soulseek', data.soulseek); updateServiceStatus('soulseek', data.soulseek);
// Update sidebar service status indicators // Update sidebar service status indicators
updateSidebarServiceStatus('spotify', data.spotify); updateSidebarServiceStatus('spotify', data.metadata_source);
updateSidebarServiceStatus('media-server', data.media_server); updateSidebarServiceStatus('media-server', data.media_server);
updateSidebarServiceStatus('soulseek', data.soulseek); updateSidebarServiceStatus('soulseek', data.soulseek);
@ -3185,8 +3185,8 @@ async function fetchAndUpdateServiceStatus() {
if (data.enrichment) renderEnrichmentCards(data.enrichment); if (data.enrichment) renderEnrichmentCards(data.enrichment);
// Check for Spotify rate limit // Check for Spotify rate limit
if (data.spotify && data.spotify.rate_limited && data.spotify.rate_limit) { if (data.metadata_source && data.metadata_source.rate_limited && data.metadata_source.rate_limit) {
handleSpotifyRateLimit(data.spotify.rate_limit); handleSpotifyRateLimit(data.metadata_source.rate_limit);
} else if (_spotifyRateLimitShown) { } else if (_spotifyRateLimitShown) {
handleSpotifyRateLimit(null); handleSpotifyRateLimit(null);
} }

View file

@ -6558,8 +6558,8 @@ function updateLibraryStatusCard(dbStats) {
const isScanning = window._libraryStatusScanning || false; const isScanning = window._libraryStatusScanning || false;
// Determine state // Determine state
const serverConnected = _lastServiceStatus && _lastServiceStatus.media_server && _lastServiceStatus.media_server.connected; const serverConnected = _lastStatusPayload && _lastStatusPayload.media_server && _lastStatusPayload.media_server.connected;
const serverType = _lastServiceStatus && _lastServiceStatus.active_media_server; const serverType = _lastStatusPayload && _lastStatusPayload.active_media_server;
const hasData = tracks > 0; const hasData = tracks > 0;
const hasServer = !!serverType && serverType !== 'none'; const hasServer = !!serverType && serverType !== 'none';
@ -6668,7 +6668,7 @@ function updateLibraryStatusCard(dbStats) {
} }
} }
// _lastServiceStatus and _isSoulsyncStandalone are declared in core.js // _lastStatusPayload and _isSoulsyncStandalone are declared in core.js
const _origFetchServiceStatus = typeof fetchAndUpdateServiceStatus === 'function' ? fetchAndUpdateServiceStatus : null; const _origFetchServiceStatus = typeof fetchAndUpdateServiceStatus === 'function' ? fetchAndUpdateServiceStatus : null;
function _capitalize(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; } function _capitalize(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }