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 = {
'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'),

View file

@ -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'
# =========================================================================

View file

@ -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(),

View file

@ -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';
}
// ===============================

View file

@ -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');
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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) : ''; }