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:
parent
36267618a3
commit
e2bd0e1871
7 changed files with 121 additions and 106 deletions
|
|
@ -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,
|
||||||
*,
|
*,
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -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']
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue