Reduce Spotify status polling

- cache Spotify auth and rate-limit status separately from the generic metadata source snapshot
- refresh Spotify status only on explicit auth/disconnect/test paths or after the TTL expires
- keep the legacy OAuth callback paths aligned with the same invalidation helper
This commit is contained in:
Antti Kettunen 2026-05-02 11:29:43 +03:00
parent e2bd0e1871
commit 3c7187fb32
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7

View file

@ -821,6 +821,24 @@ _status_cache_timestamps: dict[str, float] = {
'soulseek': 0, 'soulseek': 0,
} }
STATUS_CACHE_TTL = 120 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 dev_mode_enabled = False
_hydrabase_ws = None _hydrabase_ws = None
@ -4155,7 +4173,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['metadata_source'] = 0 _invalidate_metadata_status_caches()
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:
@ -4772,8 +4790,7 @@ def test_connection_endpoint():
if success: if success:
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 _invalidate_metadata_status_caches()
_status_cache_timestamps['metadata_source'] = 0
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
@ -4937,8 +4954,7 @@ def test_dashboard_connection_endpoint():
if success: if success:
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 _invalidate_metadata_status_caches()
_status_cache_timestamps['metadata_source'] = 0
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
@ -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) 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['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") 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.",
@ -5853,7 +5869,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['metadata_source'] = 0 _invalidate_metadata_status_caches()
# 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()
@ -5863,7 +5879,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['metadata_source'] = 0 _invalidate_metadata_status_caches()
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.",
@ -5892,7 +5908,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_timestamps['metadata_source'] = 0 _invalidate_metadata_status_caches()
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,
@ -32027,7 +32043,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['metadata_source'] = 0 _invalidate_metadata_status_caches()
# 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()
@ -32040,7 +32056,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['metadata_source'] = 0 _invalidate_metadata_status_caches()
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')
@ -34029,7 +34045,15 @@ def _build_status_payload():
def _build_spotify_status_payload(): def _build_spotify_status_payload():
"""Build a Spotify-specific status snapshot for auth and rate-limit state.""" """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: try:
# Always include fresh rate limit info because it can change independently of cache TTL. # 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 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 authenticated = spotify_client.is_spotify_authenticated() if spotify_client else False
status_data.update({ _spotify_status_cache.update({
'connected': authenticated, 'connected': authenticated,
'authenticated': authenticated, 'authenticated': authenticated,
'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,
}) })
_spotify_status_timestamp = current_time
except Exception: except Exception:
pass pass
return status_data return dict(_spotify_status_cache)
def _build_watchlist_count_payload(profile_id=1): def _build_watchlist_count_payload(profile_id=1):
"""Build the same payload used by GET /api/watchlist/count.""" """Build the same payload used by GET /api/watchlist/count."""