Split metadata source and Spotify status

- Keep the primary metadata provider snapshot generic and move Spotify auth/rate-limit details into a separate status object.
- Update the websocket fixture and dashboard/settings consumers to read the two buckets independently.
This commit is contained in:
Antti Kettunen 2026-05-02 11:05:01 +03:00
parent 36267618a3
commit e2bd0e1871
No known key found for this signature in database
GPG key ID: C6B2A3D250359BD7
7 changed files with 121 additions and 106 deletions

View file

@ -9,6 +9,7 @@ from __future__ import annotations
import threading import threading
import hashlib import hashlib
import time
from typing import Any, Callable, Dict, Optional from typing import Any, Callable, Dict, Optional
from utils.logging_config import get_logger from utils.logging_config import get_logger
@ -341,6 +342,44 @@ def get_primary_client(
) )
def get_primary_source_status(
*,
spotify_client_factory: Optional[MetadataClientFactory] = None,
itunes_client_factory: Optional[MetadataClientFactory] = None,
deezer_client_factory: Optional[MetadataClientFactory] = None,
discogs_client_factory: Optional[MetadataClientFactory] = None,
) -> Dict[str, Any]:
"""Return a generic status snapshot for the active primary metadata source."""
source = _get_config_value("metadata.fallback_source", "deezer") or "deezer"
started = time.time()
connected = False
try:
client = get_client_for_source(
source,
spotify_client_factory=spotify_client_factory,
itunes_client_factory=itunes_client_factory,
deezer_client_factory=deezer_client_factory,
discogs_client_factory=discogs_client_factory,
)
if source == "spotify":
connected = bool(client and client.is_spotify_authenticated())
elif source == "hydrabase":
connected = bool(client and (client.is_connected() if hasattr(client, "is_connected") else client.is_authenticated()))
elif client is not None and hasattr(client, "is_authenticated"):
connected = bool(client.is_authenticated())
else:
connected = client is not None
except Exception:
connected = False
return {
"source": source,
"connected": connected,
"response_time": round((time.time() - started) * 1000, 1),
}
def get_client_for_source( def get_client_for_source(
source: str, source: str,
*, *,

View file

@ -17,7 +17,8 @@ from flask_socketio import SocketIO, join_room, leave_room
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_DEFAULT_STATUS_CACHE = { _DEFAULT_STATUS_CACHE = {
'metadata_source': {'connected': True, 'authenticated': True, 'response_time': 12.5, 'source': 'spotify'}, 'metadata_source': {'connected': True, 'response_time': 12.5, 'source': 'spotify'},
'spotify': {'connected': True, 'authenticated': True, 'rate_limited': False, 'rate_limit': None, 'post_ban_cooldown': None},
'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'},
} }
@ -256,6 +257,7 @@ wishlist_stats_state = copy.deepcopy(_DEFAULT_WISHLIST_STATS)
def _build_status_payload(): def _build_status_payload():
return { return {
'metadata_source': dict(_status_cache['metadata_source']), 'metadata_source': dict(_status_cache['metadata_source']),
'spotify': dict(_status_cache['spotify']),
'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

@ -53,10 +53,12 @@ class TestServiceStatus:
assert len(status_events) >= 1 assert len(status_events) >= 1
data = status_events[0]['args'][0] data = status_events[0]['args'][0]
assert 'metadata_source' in data assert 'metadata_source' in data
assert 'spotify' 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['metadata_source'] assert 'source' in data['metadata_source']
assert 'authenticated' in data['spotify']
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."""
@ -74,6 +76,7 @@ class TestServiceStatus:
ws_data = status_events[0]['args'][0] ws_data = status_events[0]['args'][0]
assert ws_data['metadata_source'] == http_data['metadata_source'] assert ws_data['metadata_source'] == http_data['metadata_source']
assert ws_data['spotify'] == http_data['spotify']
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']

View file

@ -811,7 +811,7 @@ _idle_since = {}
_IDLE_GRACE_SECONDS = 5 _IDLE_GRACE_SECONDS = 5
_status_cache = { _status_cache = {
'metadata_source': {'connected': False, 'authenticated': False, 'response_time': 0, 'source': 'itunes'}, 'metadata_source': {'connected': 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},
} }
@ -3446,41 +3446,9 @@ def get_status():
current_time = time.time() current_time = time.time()
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 primary metadata provider and Spotify separately
if current_time - _status_cache_timestamps['metadata_source'] > STATUS_CACHE_TTL: if current_time - _status_cache_timestamps['metadata_source'] > STATUS_CACHE_TTL:
# Guard against spotify_client being None (partial init) _status_cache['metadata_source'] = metadata_registry.get_primary_source_status()
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
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'
if is_rate_limited or cooldown_remaining > 0:
# 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
metadata_source_response_time = 0
else:
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 metadata_source_connected else 'deezer'
else:
music_source = configured_source
_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['metadata_source'] = current_time _status_cache_timestamps['metadata_source'] = current_time
# else: use cached value # else: use cached value
@ -3557,6 +3525,7 @@ def get_status():
status_data = { status_data = {
'metadata_source': _status_cache['metadata_source'], 'metadata_source': _status_cache['metadata_source'],
'spotify': _build_spotify_status_payload(),
'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,
@ -4804,10 +4773,7 @@ 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['metadata_source']['connected'] = True _status_cache_timestamps['metadata_source'] = 0
_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") 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 +4938,7 @@ 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['metadata_source']['connected'] = True _status_cache_timestamps['metadata_source'] = 0
_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") 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
@ -5929,16 +5892,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['metadata_source'] = { _status_cache_timestamps['metadata_source'] = 0
'connected': False,
'authenticated': False,
'response_time': 0,
'source': active_source,
'rate_limited': False,
'rate_limit': None,
'post_ban_cooldown': None
}
_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,
@ -34050,17 +34004,8 @@ def _build_status_payload():
download_mode = config_manager.get('download_source.mode', 'hybrid') download_mode = config_manager.get('download_source.mode', 'hybrid')
soulseek_data = dict(_status_cache.get('soulseek', {})) soulseek_data = dict(_status_cache.get('soulseek', {}))
soulseek_data['source'] = download_mode soulseek_data['source'] = download_mode
# 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
metadata_source_data = dict(_status_cache.get('metadata_source', {})) metadata_source_data = dict(_status_cache.get('metadata_source', {}))
is_rl = spotify_client.is_rate_limited() if spotify_client else False spotify_data = _build_spotify_status_payload()
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
metadata_source_data['rate_limited'] = is_rl
metadata_source_data['rate_limit'] = rate_limit_info
if cooldown_remaining > 0:
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
@ -34074,6 +34019,7 @@ def _build_status_payload():
return { return {
'metadata_source': metadata_source_data, 'metadata_source': metadata_source_data,
'spotify': spotify_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(),
@ -34081,6 +34027,29 @@ def _build_status_payload():
'active_downloads': active_dl_count, 'active_downloads': active_dl_count,
} }
def _build_spotify_status_payload():
"""Build a Spotify-specific status snapshot for auth and rate-limit state."""
status_data = {}
try:
# Always include fresh rate limit info because it can change independently of cache TTL.
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
authenticated = spotify_client.is_spotify_authenticated() if spotify_client else False
status_data.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,
})
except Exception:
pass
return status_data
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."""
try: try:

View file

@ -474,21 +474,21 @@ function handleServiceStatusUpdate(data) {
_lastStatusPayload = data; _lastStatusPayload = data;
if (typeof syncSpotifySettingsAuthState === 'function') { if (typeof syncSpotifySettingsAuthState === 'function') {
syncSpotifySettingsAuthState(data?.metadata_source || null); syncSpotifySettingsAuthState(data?.spotify || null);
} }
if (typeof syncPrimaryMetadataSourceAvailability === 'function') { if (typeof syncPrimaryMetadataSourceAvailability === 'function') {
syncPrimaryMetadataSourceAvailability(data?.metadata_source || null); syncPrimaryMetadataSourceAvailability(data?.spotify || 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.metadata_source); updateServiceStatus('spotify', data.metadata_source, data.spotify);
updateServiceStatus('media-server', data.media_server); updateServiceStatus('media-server', data.media_server);
updateServiceStatus('soulseek', data.soulseek); updateServiceStatus('soulseek', data.soulseek);
updateSidebarServiceStatus('spotify', data.metadata_source); updateSidebarServiceStatus('spotify', data.metadata_source, data.spotify);
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.metadata_source?.rate_limited && data.metadata_source.rate_limit) { if (data.spotify?.rate_limited && data.spotify.rate_limit) {
handleSpotifyRateLimit(data.metadata_source.rate_limit); handleSpotifyRateLimit(data.spotify.rate_limit);
_spotifyInCooldown = false; _spotifyInCooldown = false;
} else if (data.metadata_source?.post_ban_cooldown > 0) { } else if (data.spotify?.post_ban_cooldown > 0) {
if (_spotifyRateLimitShown && !_spotifyInCooldown) { if (_spotifyRateLimitShown && !_spotifyInCooldown) {
_spotifyRateLimitShown = false; _spotifyRateLimitShown = false;
_spotifyInCooldown = true; _spotifyInCooldown = true;

View file

@ -60,7 +60,7 @@ function syncMetadataSourceSelection(source) {
function _isMetadataSourceSelectable(source) { function _isMetadataSourceSelectable(source) {
if (source === 'spotify') { if (source === 'spotify') {
return _lastStatusPayload?.metadata_source?.authenticated === true; return _lastStatusPayload?.spotify?.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(_lastStatusPayload?.metadata_source || null); syncPrimaryMetadataSourceAvailability(_lastStatusPayload?.spotify || null);
} }
sanitizeMetadataSourceSelection({ quiet: true }); sanitizeMetadataSourceSelection({ quiet: true });
}); });
@ -207,9 +207,9 @@ 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(_lastStatusPayload?.metadata_source || null); syncPrimaryMetadataSourceAvailability(_lastStatusPayload?.spotify || null);
} }
syncSpotifySettingsAuthState(_lastStatusPayload?.metadata_source || null); syncSpotifySettingsAuthState(_lastStatusPayload?.spotify || null);
syncMetadataSourceSelection(_lastStatusPayload?.metadata_source?.source); syncMetadataSourceSelection(_lastStatusPayload?.metadata_source?.source);
sanitizeMetadataSourceSelection({ quiet: true }); sanitizeMetadataSourceSelection({ quiet: true });
if (metadataSourceSelect) { if (metadataSourceSelect) {
@ -408,7 +408,7 @@ async function applyServiceStatusGradients() {
else header.appendChild(spinner); else header.appendChild(spinner);
} }
}); });
syncSpotifySettingsAuthState(_lastStatusPayload?.metadata_source || null); syncSpotifySettingsAuthState(_lastStatusPayload?.spotify || 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 = _lastStatusPayload?.metadata_source?.authenticated === true; const spotifySessionActive = _lastStatusPayload?.spotify?.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

@ -3146,22 +3146,22 @@ async function fetchAndUpdateServiceStatus() {
_lastStatusPayload = data; _lastStatusPayload = data;
if (typeof syncSpotifySettingsAuthState === 'function') { if (typeof syncSpotifySettingsAuthState === 'function') {
syncSpotifySettingsAuthState(data?.metadata_source || null); syncSpotifySettingsAuthState(data?.spotify || null);
} }
if (typeof syncPrimaryMetadataSourceAvailability === 'function') { if (typeof syncPrimaryMetadataSourceAvailability === 'function') {
syncPrimaryMetadataSourceAvailability(data?.metadata_source || null); syncPrimaryMetadataSourceAvailability(data?.spotify || 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.metadata_source); updateServiceStatus('spotify', data.metadata_source, data.spotify);
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.metadata_source); updateSidebarServiceStatus('spotify', data.metadata_source, data.spotify);
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.metadata_source && data.metadata_source.rate_limited && data.metadata_source.rate_limit) { if (data.spotify && data.spotify.rate_limited && data.spotify.rate_limit) {
handleSpotifyRateLimit(data.metadata_source.rate_limit); handleSpotifyRateLimit(data.spotify.rate_limit);
} else if (_spotifyRateLimitShown) { } else if (_spotifyRateLimitShown) {
handleSpotifyRateLimit(null); handleSpotifyRateLimit(null);
} }
@ -3232,14 +3232,16 @@ function getMetadataSourceLabel(source) {
return 'Unmapped'; return 'Unmapped';
} }
function getSpotifyStatusPresentation(statusData) { function getMetadataSourcePresentation(metadataStatus, spotifyStatus) {
const sourceLabel = getMetadataSourceLabel(statusData?.source); const source = metadataStatus?.source;
const rateLimited = !!(statusData?.rate_limited && statusData?.rate_limit); const sourceLabel = getMetadataSourceLabel(source);
const cooldown = !!(statusData?.post_ban_cooldown > 0); const connected = metadataStatus?.connected === true;
const sessionActive = statusData?.authenticated === true || (statusData?.authenticated === undefined && statusData?.source === 'spotify'); const sessionActive = spotifyStatus?.authenticated === true || (source === 'spotify' && connected);
const rateLimited = !!(source === 'spotify' && spotifyStatus?.rate_limited && spotifyStatus?.rate_limit);
const cooldown = !!(source === 'spotify' && spotifyStatus?.post_ban_cooldown > 0);
if (rateLimited) { if (rateLimited) {
const remaining = statusData.rate_limit?.remaining_seconds || 0; const remaining = spotifyStatus.rate_limit?.remaining_seconds || 0;
return { return {
statusClass: 'rate-limited', statusClass: 'rate-limited',
statusText: `Spotify paused \u2014 ${formatRateLimitDuration(remaining)}`, statusText: `Spotify paused \u2014 ${formatRateLimitDuration(remaining)}`,
@ -3250,7 +3252,7 @@ function getSpotifyStatusPresentation(statusData) {
} }
if (cooldown) { if (cooldown) {
const remaining = statusData.post_ban_cooldown; const remaining = spotifyStatus.post_ban_cooldown;
return { return {
statusClass: 'rate-limited', statusClass: 'rate-limited',
statusText: `Spotify recovering \u2014 ${formatRateLimitDuration(remaining)}`, statusText: `Spotify recovering \u2014 ${formatRateLimitDuration(remaining)}`,
@ -3260,32 +3262,32 @@ function getSpotifyStatusPresentation(statusData) {
}; };
} }
if (statusData?.source && statusData.source !== 'spotify') { if (source) {
return { return {
statusClass: 'connected', statusClass: connected ? 'connected' : 'disconnected',
statusText: sourceLabel, statusText: connected ? (source === 'spotify' ? `Connected (${metadataStatus?.response_time}ms)` : sourceLabel) : 'Disconnected',
dotClass: 'connected', dotClass: connected ? 'connected' : 'disconnected',
dotTitle: sourceLabel, dotTitle: connected ? sourceLabel : 'Disconnected',
sessionActive sessionActive
}; };
} }
return { return {
statusClass: 'connected', statusClass: 'disconnected',
statusText: `Connected (${statusData?.response_time}ms)`, statusText: 'Disconnected',
dotClass: 'connected', dotClass: 'disconnected',
dotTitle: '', dotTitle: 'Disconnected',
sessionActive sessionActive
}; };
} }
function updateServiceStatus(service, statusData) { function updateServiceStatus(service, statusData, spotifyStatus = null) {
const indicator = document.getElementById(`${service}-status-indicator`); const indicator = document.getElementById(`${service}-status-indicator`);
const statusText = document.getElementById(`${service}-status-text`); const statusText = document.getElementById(`${service}-status-text`);
if (indicator && statusText) { if (indicator && statusText) {
if (service === 'spotify') { if (service === 'spotify') {
const presentation = getSpotifyStatusPresentation(statusData || {}); const presentation = getMetadataSourcePresentation(statusData || {}, spotifyStatus || {});
indicator.className = `service-card-indicator ${presentation.statusClass}`; indicator.className = `service-card-indicator ${presentation.statusClass}`;
statusText.textContent = presentation.statusText; statusText.textContent = presentation.statusText;
statusText.className = `service-card-status-text ${presentation.statusClass}`; statusText.className = `service-card-status-text ${presentation.statusClass}`;
@ -3312,7 +3314,7 @@ function updateServiceStatus(service, statusData) {
} }
// Keep the Spotify action buttons aligned with the actual auth session. // Keep the Spotify action buttons aligned with the actual auth session.
const spotifySessionActive = getSpotifyStatusPresentation(statusData || {}).sessionActive; const spotifySessionActive = spotifyStatus?.authenticated === true;
const authBtn = document.querySelector('button[onclick="authenticateSpotify()"]'); const authBtn = document.querySelector('button[onclick="authenticateSpotify()"]');
const disconnectBtn = document.getElementById('spotify-disconnect-btn'); const disconnectBtn = document.getElementById('spotify-disconnect-btn');
if (authBtn) { if (authBtn) {
@ -3322,7 +3324,7 @@ function updateServiceStatus(service, statusData) {
disconnectBtn.style.display = spotifySessionActive ? '' : 'none'; disconnectBtn.style.display = spotifySessionActive ? '' : 'none';
} }
syncPrimaryMetadataSourceAvailability(statusData); syncPrimaryMetadataSourceAvailability(spotifyStatus);
const responseTimeElement = document.getElementById('spotify-response-time'); const responseTimeElement = document.getElementById('spotify-response-time');
if (responseTimeElement) { if (responseTimeElement) {
@ -3348,7 +3350,7 @@ function updateServiceStatus(service, statusData) {
} }
} }
function updateSidebarServiceStatus(service, statusData) { function updateSidebarServiceStatus(service, statusData, spotifyStatus = null) {
const indicator = document.getElementById(`${service}-indicator`); const indicator = document.getElementById(`${service}-indicator`);
if (indicator) { if (indicator) {
const dot = indicator.querySelector('.status-dot'); const dot = indicator.querySelector('.status-dot');
@ -3356,7 +3358,7 @@ function updateSidebarServiceStatus(service, statusData) {
if (dot) { if (dot) {
if (service === 'spotify') { if (service === 'spotify') {
const presentation = getSpotifyStatusPresentation(statusData || {}); const presentation = getMetadataSourcePresentation(statusData || {}, spotifyStatus || {});
dot.className = `status-dot ${presentation.dotClass}`; dot.className = `status-dot ${presentation.dotClass}`;
dot.title = presentation.dotTitle; dot.title = presentation.dotTitle;
} else { } else {