From 36267618a381f26b109f248f7d29acd53a3d6441 Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Sat, 2 May 2026 10:42:33 +0300 Subject: [PATCH] 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. --- tests/conftest.py | 4 +- tests/test_websocket_infrastructure.py | 10 ++-- web_server.py | 72 +++++++++++++------------- webui/static/core.js | 22 ++++---- webui/static/helper.js | 4 +- webui/static/settings.js | 14 ++--- webui/static/shared-helpers.js | 14 ++--- webui/static/wishlist-tools.js | 6 +-- 8 files changed, 73 insertions(+), 73 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 03b5ec5f..4adc2f96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,7 @@ from flask_socketio import SocketIO, join_room, leave_room # --------------------------------------------------------------------------- _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'}, '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(): return { - 'spotify': dict(_status_cache['spotify']), + 'metadata_source': dict(_status_cache['metadata_source']), 'media_server': dict(_status_cache['media_server']), 'soulseek': dict(_status_cache['soulseek']), 'active_media_server': _status_cache['media_server'].get('type', 'plex'), diff --git a/tests/test_websocket_infrastructure.py b/tests/test_websocket_infrastructure.py index 392f80aa..dc3d81bf 100644 --- a/tests/test_websocket_infrastructure.py +++ b/tests/test_websocket_infrastructure.py @@ -52,11 +52,11 @@ class TestServiceStatus: status_events = [e for e in received if e['name'] == 'status:update'] assert len(status_events) >= 1 data = status_events[0]['args'][0] - assert 'spotify' in data + assert 'metadata_source' in data assert 'media_server' in data assert 'soulseek' 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): """Socket event data matches HTTP endpoint response exactly.""" @@ -73,7 +73,7 @@ class TestServiceStatus: assert len(status_events) >= 1 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['soulseek'] == http_data['soulseek'] assert ws_data['active_media_server'] == http_data['active_media_server'] @@ -86,13 +86,13 @@ class TestServiceStatus: build = shared_state['build_status_payload'] # Mutate cache - status_cache['spotify']['source'] = 'itunes' + status_cache['metadata_source']['source'] = 'itunes' socketio.emit('status:update', build()) received = client.get_received() status_events = [e for e in received if e['name'] == 'status:update'] data = status_events[-1]['args'][0] - assert data['spotify']['source'] == 'itunes' + assert data['metadata_source']['source'] == 'itunes' # ========================================================================= diff --git a/web_server.py b/web_server.py index 2b4be31d..72cfd393 100644 --- a/web_server.py +++ b/web_server.py @@ -811,12 +811,12 @@ _idle_since = {} _IDLE_GRACE_SECONDS = 5 _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}, 'soulseek': {'connected': False, 'response_time': 0}, } _status_cache_timestamps: dict[str, float] = { - 'spotify': 0, + 'metadata_source': 0, 'media_server': 0, 'soulseek': 0, } @@ -3447,12 +3447,12 @@ def get_status(): active_server = config_manager.get_active_media_server() # 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) 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 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 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. # Probing Spotify here would reset the rate limit timer. music_source = 'deezer' if configured_source == 'spotify' else configured_source - spotify_response_time = 0 + metadata_source_response_time = 0 else: - spotify_start = time.time() - spotify_connected = spotify_client.is_spotify_authenticated() if spotify_client else False - spotify_response_time = (time.time() - spotify_start) * 1000 + metadata_source_start = time.time() + metadata_source_connected = spotify_client.is_spotify_authenticated() if spotify_client else False + metadata_source_response_time = (time.time() - metadata_source_start) * 1000 # Use configured source; fall back to deezer only if Spotify isn't authenticated if configured_source == 'spotify': - music_source = 'spotify' if spotify_connected else 'deezer' + music_source = 'spotify' if metadata_source_connected else 'deezer' else: music_source = configured_source - _status_cache['spotify'] = { - 'connected': spotify_session_active, - 'authenticated': spotify_session_active, - 'response_time': round(spotify_response_time, 1), + _status_cache['metadata_source'] = { + 'connected': metadata_source_authenticated, + 'authenticated': metadata_source_authenticated, + 'response_time': round(metadata_source_response_time, 1), 'source': music_source, 'rate_limited': is_rate_limited, 'rate_limit': rate_limit_info, '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 # Test media server - use EXISTING instances (they have internal caching) @@ -3556,7 +3556,7 @@ def get_status(): active_dl_count += 1 status_data = { - 'spotify': _status_cache['spotify'], + 'metadata_source': _status_cache['metadata_source'], 'media_server': _status_cache['media_server'], 'soulseek': _status_cache['soulseek'], 'active_media_server': active_server, @@ -4186,7 +4186,7 @@ def handle_settings(): if tidal_enrichment_worker: tidal_enrichment_worker.client = tidal_client # 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.") return jsonify({"success": True, "message": "Settings saved successfully."}) except Exception as e: @@ -4804,10 +4804,10 @@ def test_connection_endpoint(): current_time = time.time() if service == 'spotify': spotify_session_active = spotify_client.is_spotify_authenticated() if spotify_client else False - _status_cache['spotify']['connected'] = True - _status_cache['spotify']['authenticated'] = spotify_session_active - _status_cache['spotify']['source'] = _get_metadata_fallback_source() - _status_cache_timestamps['spotify'] = current_time + _status_cache['metadata_source']['connected'] = True + _status_cache['metadata_source']['authenticated'] = spotify_session_active + _status_cache['metadata_source']['source'] = _get_metadata_fallback_source() + _status_cache_timestamps['metadata_source'] = current_time logger.info("Updated Spotify status cache after successful test") elif service in ['plex', 'jellyfin', 'navidrome', 'soulsync']: _status_cache['media_server']['connected'] = True @@ -4972,10 +4972,10 @@ def test_dashboard_connection_endpoint(): current_time = time.time() if service == 'spotify': spotify_session_active = spotify_client.is_spotify_authenticated() if spotify_client else False - _status_cache['spotify']['connected'] = True - _status_cache['spotify']['authenticated'] = spotify_session_active - _status_cache['spotify']['source'] = _get_metadata_fallback_source() - _status_cache_timestamps['spotify'] = current_time + _status_cache['metadata_source']['connected'] = True + _status_cache['metadata_source']['authenticated'] = spotify_session_active + _status_cache['metadata_source']['source'] = _get_metadata_fallback_source() + _status_cache_timestamps['metadata_source'] = current_time logger.info("Updated Spotify status cache after successful dashboard test") elif service in ['plex', 'jellyfin', 'navidrome', 'soulsync']: _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) if profile_client: 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") 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.", @@ -5890,7 +5890,7 @@ def spotify_callback(): _clear_rate_limit() spotify_client._invalidate_auth_cache() # 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 if spotify_enrichment_worker and hasattr(spotify_enrichment_worker, 'client'): spotify_enrichment_worker.client.reload_config() @@ -5900,7 +5900,7 @@ def spotify_callback(): else: logger.warning("Spotify OAuth token exchange succeeded but authentication validation failed") 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") 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.", @@ -5929,7 +5929,7 @@ def spotify_disconnect(): source_label = get_metadata_source_label(active_source) if configured_source == 'spotify': config_manager.set('metadata.fallback_source', active_source) - _status_cache['spotify'] = { + _status_cache['metadata_source'] = { 'connected': False, 'authenticated': False, 'response_time': 0, @@ -5938,7 +5938,7 @@ def spotify_disconnect(): 'rate_limit': 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") return jsonify({ 'success': True, @@ -32073,7 +32073,7 @@ def start_oauth_callback_servers(): _clear_rate_limit() spotify_client._invalidate_auth_cache() # 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 if spotify_enrichment_worker and hasattr(spotify_enrichment_worker, 'client'): spotify_enrichment_worker.client.reload_config() @@ -32086,7 +32086,7 @@ def start_oauth_callback_servers(): else: _oauth_logger.warning("Spotify token exchange succeeded but authentication validation failed") 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") self.send_response(200) 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) # 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 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 - spotify_data['rate_limited'] = is_rl - spotify_data['rate_limit'] = rate_limit_info + metadata_source_data['rate_limited'] = is_rl + metadata_source_data['rate_limit'] = rate_limit_info 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 active_dl_count = 0 @@ -34073,7 +34073,7 @@ def _build_status_payload(): pass return { - 'spotify': spotify_data, + 'metadata_source': metadata_source_data, 'media_server': _status_cache.get('media_server', {}), 'soulseek': soulseek_data, 'active_media_server': config_manager.get_active_media_server(), diff --git a/webui/static/core.js b/webui/static/core.js index 4e3b6ad8..cf0f4809 100644 --- a/webui/static/core.js +++ b/webui/static/core.js @@ -471,24 +471,24 @@ function initializeWebSocket() { function handleServiceStatusUpdate(data) { // Cache for library status card - _lastServiceStatus = data; + _lastStatusPayload = data; if (typeof syncSpotifySettingsAuthState === 'function') { - syncSpotifySettingsAuthState(data?.spotify || null); + syncSpotifySettingsAuthState(data?.metadata_source || null); } if (typeof syncPrimaryMetadataSourceAvailability === 'function') { - syncPrimaryMetadataSourceAvailability(data?.spotify || null); + syncPrimaryMetadataSourceAvailability(data?.metadata_source || null); } if (typeof sanitizeMetadataSourceSelection === 'function') { sanitizeMetadataSourceSelection({ quiet: true }); } // Same logic as fetchAndUpdateServiceStatus response handler - updateServiceStatus('spotify', data.spotify); + updateServiceStatus('spotify', data.metadata_source); updateServiceStatus('media-server', data.media_server); updateServiceStatus('soulseek', data.soulseek); - updateSidebarServiceStatus('spotify', data.spotify); + updateSidebarServiceStatus('spotify', data.metadata_source); updateSidebarServiceStatus('media-server', data.media_server); updateSidebarServiceStatus('soulseek', data.soulseek); @@ -514,10 +514,10 @@ function handleServiceStatusUpdate(data) { if (data.enrichment) renderEnrichmentCards(data.enrichment); // Spotify rate limit / cooldown / recovery - if (data.spotify?.rate_limited && data.spotify.rate_limit) { - handleSpotifyRateLimit(data.spotify.rate_limit); + if (data.metadata_source?.rate_limited && data.metadata_source.rate_limit) { + handleSpotifyRateLimit(data.metadata_source.rate_limit); _spotifyInCooldown = false; - } else if (data.spotify?.post_ban_cooldown > 0) { + } else if (data.metadata_source?.post_ban_cooldown > 0) { if (_spotifyRateLimitShown && !_spotifyInCooldown) { _spotifyRateLimitShown = false; _spotifyInCooldown = true; @@ -881,12 +881,12 @@ const API = { } }; -// Track last service status for library card (used by websocket handler in core + artists) -let _lastServiceStatus = null; +// Track the last `/status` payload (shared service snapshot used across the UI) +let _lastStatusPayload = null; let _isSoulsyncStandalone = false; // Global flag: true when no media server (sync buttons hidden) function getActiveMetadataSource() { - return _lastServiceStatus?.spotify?.source || 'spotify'; + return _lastStatusPayload?.metadata_source?.source || 'spotify'; } // =============================== diff --git a/webui/static/helper.js b/webui/static/helper.js index 3afabac5..a8f97688 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -2974,13 +2974,13 @@ async function _checkSetupStatus() { const completion = _getSetupCompletion(); const results = { ...completion }; - // ── /status — checks services (spotify, media_server, soulseek) ───── + // ── /status — checks metadata_source, media_server, soulseek ──────── try { const resp = await fetch('/status'); if (resp.ok) { const data = await resp.json(); // 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(); _markSetupComplete('metadata-source'); } diff --git a/webui/static/settings.js b/webui/static/settings.js index 8b9aa005..6307e575 100644 --- a/webui/static/settings.js +++ b/webui/static/settings.js @@ -60,7 +60,7 @@ function syncMetadataSourceSelection(source) { function _isMetadataSourceSelectable(source) { if (source === 'spotify') { - return _lastServiceStatus?.spotify?.authenticated === true; + return _lastStatusPayload?.metadata_source?.authenticated === true; } if (source === 'discogs') { const token = document.getElementById('discogs-token'); @@ -173,7 +173,7 @@ function initializeSettings() { if (discogsTokenInput) { discogsTokenInput.addEventListener('input', () => { if (typeof syncPrimaryMetadataSourceAvailability === 'function') { - syncPrimaryMetadataSourceAvailability(_lastServiceStatus?.spotify || null); + syncPrimaryMetadataSourceAvailability(_lastStatusPayload?.metadata_source || null); } sanitizeMetadataSourceSelection({ quiet: true }); }); @@ -207,10 +207,10 @@ function initializeSettings() { // Test button event listeners removed - they use onclick attributes in HTML to avoid double firing if (typeof syncPrimaryMetadataSourceAvailability === 'function') { - syncPrimaryMetadataSourceAvailability(_lastServiceStatus?.spotify || null); + syncPrimaryMetadataSourceAvailability(_lastStatusPayload?.metadata_source || null); } - syncSpotifySettingsAuthState(_lastServiceStatus?.spotify || null); - syncMetadataSourceSelection(_lastServiceStatus?.spotify?.source); + syncSpotifySettingsAuthState(_lastStatusPayload?.metadata_source || null); + syncMetadataSourceSelection(_lastStatusPayload?.metadata_source?.source); sanitizeMetadataSourceSelection({ quiet: true }); if (metadataSourceSelect) { metadataSourceSelect.dataset.lastValidSource = metadataSourceSelect.value; @@ -408,7 +408,7 @@ async function applyServiceStatusGradients() { else header.appendChild(spinner); } }); - syncSpotifySettingsAuthState(_lastServiceStatus?.spotify || null); + syncSpotifySettingsAuthState(_lastStatusPayload?.metadata_source || null); } catch (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 discogsTokenPresent = !!discogsTokenInput?.value?.trim(); let metadataSource = metadataSourceSelect?.value || 'itunes'; - const spotifySessionActive = _lastServiceStatus?.spotify?.authenticated === true; + const spotifySessionActive = _lastStatusPayload?.metadata_source?.authenticated === true; if (metadataSource === 'spotify' && !spotifySessionActive) { metadataSource = 'deezer'; if (metadataSourceSelect) metadataSourceSelect.value = metadataSource; diff --git a/webui/static/shared-helpers.js b/webui/static/shared-helpers.js index 29b2a1fa..7b675fd3 100644 --- a/webui/static/shared-helpers.js +++ b/webui/static/shared-helpers.js @@ -3143,25 +3143,25 @@ async function fetchAndUpdateServiceStatus() { const data = await response.json(); // Cache for library status card - _lastServiceStatus = data; + _lastStatusPayload = data; if (typeof syncSpotifySettingsAuthState === 'function') { - syncSpotifySettingsAuthState(data?.spotify || null); + syncSpotifySettingsAuthState(data?.metadata_source || null); } if (typeof syncPrimaryMetadataSourceAvailability === 'function') { - syncPrimaryMetadataSourceAvailability(data?.spotify || null); + syncPrimaryMetadataSourceAvailability(data?.metadata_source || null); } if (typeof sanitizeMetadataSourceSelection === 'function') { sanitizeMetadataSourceSelection({ quiet: true }); } // Update service status indicators and text (dashboard) - updateServiceStatus('spotify', data.spotify); + updateServiceStatus('spotify', data.metadata_source); updateServiceStatus('media-server', data.media_server); updateServiceStatus('soulseek', data.soulseek); // Update sidebar service status indicators - updateSidebarServiceStatus('spotify', data.spotify); + updateSidebarServiceStatus('spotify', data.metadata_source); updateSidebarServiceStatus('media-server', data.media_server); updateSidebarServiceStatus('soulseek', data.soulseek); @@ -3185,8 +3185,8 @@ async function fetchAndUpdateServiceStatus() { if (data.enrichment) renderEnrichmentCards(data.enrichment); // Check for Spotify rate limit - if (data.spotify && data.spotify.rate_limited && data.spotify.rate_limit) { - handleSpotifyRateLimit(data.spotify.rate_limit); + if (data.metadata_source && data.metadata_source.rate_limited && data.metadata_source.rate_limit) { + handleSpotifyRateLimit(data.metadata_source.rate_limit); } else if (_spotifyRateLimitShown) { handleSpotifyRateLimit(null); } diff --git a/webui/static/wishlist-tools.js b/webui/static/wishlist-tools.js index 3d40bb63..15766b5d 100644 --- a/webui/static/wishlist-tools.js +++ b/webui/static/wishlist-tools.js @@ -6558,8 +6558,8 @@ function updateLibraryStatusCard(dbStats) { const isScanning = window._libraryStatusScanning || false; // Determine state - const serverConnected = _lastServiceStatus && _lastServiceStatus.media_server && _lastServiceStatus.media_server.connected; - const serverType = _lastServiceStatus && _lastServiceStatus.active_media_server; + const serverConnected = _lastStatusPayload && _lastStatusPayload.media_server && _lastStatusPayload.media_server.connected; + const serverType = _lastStatusPayload && _lastStatusPayload.active_media_server; const hasData = tracks > 0; 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; function _capitalize(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }