diff --git a/web_server.py b/web_server.py
index 77da23ad..0ecd0b03 100644
--- a/web_server.py
+++ b/web_server.py
@@ -2318,9 +2318,19 @@ def get_status():
if t.get('status') in ('downloading', 'searching', 'post_processing', 'queued', 'pending'):
active_dl_count += 1
+ # Spotify Free: tell the UI whether Spotify metadata is available even
+ # without auth (so the Settings source selector can offer it).
+ spotify_status = dict(metadata_status['spotify'])
+ try:
+ spotify_status['metadata_available'] = bool(
+ spotify_client and spotify_client.is_spotify_metadata_available()
+ )
+ except Exception:
+ spotify_status['metadata_available'] = bool(spotify_status.get('authenticated'))
+
status_data = {
'metadata_source': metadata_status['metadata_source'],
- 'spotify': metadata_status['spotify'],
+ 'spotify': spotify_status,
'media_server': _status_cache['media_server'],
'soulseek': soulseek_data,
'active_media_server': active_server,
@@ -3665,10 +3675,23 @@ def settings_config_status_endpoint():
Drives the green/yellow header gradient. No API calls — just config reads.
"""
try:
- return jsonify({
+ result = {
service: {'configured': _is_service_configured(service)}
for service in SERVICE_CONFIG_REGISTRY
- })
+ }
+ # Spotify Free: Spotify metadata can be available without credentials
+ # (opt-in no-creds source). Surface that separately so the search source
+ # picker offers Spotify, while `configured` (the Connections indicator)
+ # keeps meaning "has client credentials".
+ if 'spotify' in result:
+ try:
+ meta_avail = bool(spotify_client and spotify_client.is_spotify_metadata_available())
+ except Exception:
+ meta_avail = False
+ result['spotify']['metadata_available'] = (
+ result['spotify']['configured'] or meta_avail
+ )
+ return jsonify(result)
except Exception as e:
logger.error(f"config-status error: {e}")
return jsonify({"error": str(e)}), 500
diff --git a/webui/index.html b/webui/index.html
index 80dac941..d42a28fc 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -3844,8 +3844,15 @@
+
+
+
Fetch Spotify metadata without API credentials, for when you can't or don't want to connect Spotify. Unofficial & best-effort (it can break if Spotify changes), and it can't search albums by name or access your library — only used when Spotify isn't authenticated.
+
-
Choose the primary source for artist, album, and track metadata. Spotify can only be selected while an active Spotify session exists. Discogs requires a personal token. MusicBrainz is always available but rate-limited to 1 req/sec.
+
Choose the primary source for artist, album, and track metadata. Spotify needs an active session — or enable Spotify Free above. Discogs requires a personal token. MusicBrainz is always available but rate-limited to 1 req/sec.
diff --git a/webui/static/settings.js b/webui/static/settings.js
index 4ef1fb1d..94800cde 100644
--- a/webui/static/settings.js
+++ b/webui/static/settings.js
@@ -64,7 +64,9 @@ function syncMetadataSourceSelection(source) {
function _isMetadataSourceSelectable(source) {
if (source === 'spotify') {
- return _lastStatusPayload?.spotify?.authenticated === true;
+ // Selectable with real auth OR when Spotify Free (no-creds) is available.
+ return _lastStatusPayload?.spotify?.authenticated === true
+ || _lastStatusPayload?.spotify?.metadata_available === true;
}
if (source === 'discogs') {
const token = document.getElementById('discogs-token');
@@ -1045,6 +1047,8 @@ async function loadSettingsData() {
// Populate Metadata source setting
document.getElementById('metadata-fallback-source').value = settings.metadata?.fallback_source || 'deezer';
+ const spotifyFreeToggle = document.getElementById('metadata-spotify-free');
+ if (spotifyFreeToggle) spotifyFreeToggle.checked = settings.metadata?.spotify_free === true;
// Populate Hydrabase settings
const hbConfig = settings.hydrabase || {};
@@ -2842,7 +2846,8 @@ async function saveSettings(quiet = false) {
token: document.getElementById('discogs-token').value,
},
metadata: {
- fallback_source: metadataSource
+ fallback_source: metadataSource,
+ spotify_free: document.getElementById('metadata-spotify-free')?.checked === true
},
hydrabase: {
url: document.getElementById('hydrabase-url').value,
diff --git a/webui/static/shared-helpers.js b/webui/static/shared-helpers.js
index a55b7027..7eb1d78e 100644
--- a/webui/static/shared-helpers.js
+++ b/webui/static/shared-helpers.js
@@ -110,6 +110,10 @@ async function fetchSourceConfiguredMap() {
for (const src of SOURCE_ORDER) {
if (_ALWAYS_CONFIGURED_SOURCES.has(src)) {
map[src] = true;
+ } else if (src === 'spotify') {
+ // Spotify Free: available without credentials when the
+ // opt-in no-creds source is on (metadata_available).
+ map[src] = !!(data[src] && (data[src].configured || data[src].metadata_available));
} else {
map[src] = !!(data[src] && data[src].configured);
}