diff --git a/web_server.py b/web_server.py index c78d9be6..ecb19d5c 100644 --- a/web_server.py +++ b/web_server.py @@ -821,6 +821,24 @@ _status_cache_timestamps: dict[str, float] = { 'soulseek': 0, } STATUS_CACHE_TTL = 120 +SPOTIFY_STATUS_TTL_ACTIVE = 15 +SPOTIFY_STATUS_TTL_IDLE = 300 + +_spotify_status_cache = { + 'connected': False, + 'authenticated': False, + 'rate_limited': False, + 'rate_limit': None, + 'post_ban_cooldown': None, +} +_spotify_status_timestamp = 0.0 + + +def _invalidate_metadata_status_caches(): + """Mark the metadata-source and Spotify status snapshots stale.""" + global _spotify_status_timestamp + _status_cache_timestamps['metadata_source'] = 0 + _spotify_status_timestamp = 0 dev_mode_enabled = False _hydrabase_ws = None @@ -4155,7 +4173,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['metadata_source'] = 0 + _invalidate_metadata_status_caches() logger.info("Service clients re-initialized with new settings.") return jsonify({"success": True, "message": "Settings saved successfully."}) except Exception as e: @@ -4772,8 +4790,7 @@ def test_connection_endpoint(): if success: current_time = time.time() if service == 'spotify': - spotify_session_active = spotify_client.is_spotify_authenticated() if spotify_client else False - _status_cache_timestamps['metadata_source'] = 0 + _invalidate_metadata_status_caches() logger.info("Updated Spotify status cache after successful test") elif service in ['plex', 'jellyfin', 'navidrome', 'soulsync']: _status_cache['media_server']['connected'] = True @@ -4937,8 +4954,7 @@ def test_dashboard_connection_endpoint(): if success: current_time = time.time() if service == 'spotify': - spotify_session_active = spotify_client.is_spotify_authenticated() if spotify_client else False - _status_cache_timestamps['metadata_source'] = 0 + _invalidate_metadata_status_caches() logger.info("Updated Spotify status cache after successful dashboard test") elif service in ['plex', 'jellyfin', 'navidrome', 'soulsync']: _status_cache['media_server']['connected'] = True @@ -5817,7 +5833,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['metadata_source'] = 0 + _invalidate_metadata_status_caches() 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.", @@ -5853,7 +5869,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['metadata_source'] = 0 + _invalidate_metadata_status_caches() # 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() @@ -5863,7 +5879,7 @@ def spotify_callback(): else: logger.warning("Spotify OAuth token exchange succeeded but authentication validation failed") spotify_client._invalidate_auth_cache() - _status_cache_timestamps['metadata_source'] = 0 + _invalidate_metadata_status_caches() 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.", @@ -5892,7 +5908,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_timestamps['metadata_source'] = 0 + _invalidate_metadata_status_caches() add_activity_item("", "Spotify Disconnected", f"Using {source_label} for metadata", "Now") return jsonify({ 'success': True, @@ -32027,7 +32043,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['metadata_source'] = 0 + _invalidate_metadata_status_caches() # 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() @@ -32040,7 +32056,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['metadata_source'] = 0 + _invalidate_metadata_status_caches() 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') @@ -34029,7 +34045,15 @@ def _build_status_payload(): def _build_spotify_status_payload(): """Build a Spotify-specific status snapshot for auth and rate-limit state.""" - status_data = {} + import time + + global _spotify_status_timestamp + current_time = time.time() + configured_source = config_manager.get('metadata.fallback_source', 'deezer') or 'deezer' + ttl = SPOTIFY_STATUS_TTL_ACTIVE if configured_source == 'spotify' else SPOTIFY_STATUS_TTL_IDLE + + if _spotify_status_timestamp and current_time - _spotify_status_timestamp <= ttl: + return dict(_spotify_status_cache) try: # Always include fresh rate limit info because it can change independently of cache TTL. @@ -34038,17 +34062,18 @@ def _build_spotify_status_payload(): cooldown_remaining = spotify_client.get_post_ban_cooldown_remaining() if spotify_client else 0 authenticated = spotify_client.is_spotify_authenticated() if spotify_client else False - status_data.update({ + _spotify_status_cache.update({ 'connected': authenticated, 'authenticated': authenticated, 'rate_limited': is_rate_limited, 'rate_limit': rate_limit_info, 'post_ban_cooldown': cooldown_remaining if cooldown_remaining > 0 else None, }) + _spotify_status_timestamp = current_time except Exception: pass - return status_data + return dict(_spotify_status_cache) def _build_watchlist_count_payload(profile_id=1): """Build the same payload used by GET /api/watchlist/count."""