#798: Spotify Free UI — enable toggle + source availability
Surfaces the opt-in Spotify Free source so it's usable end-to-end: - Settings: 'Enable Spotify Free (no credentials)' toggle that saves metadata.spotify_free (load + save wired). Clear best-effort/limitations note. - config-status: adds spotify.metadata_available (configured OR free-available), keeping the configured flag = has-credentials so the Connections indicator stays honest. Search source picker shows Spotify when metadata_available. - status payload: adds spotify.metadata_available; the Settings primary-source selector now allows picking Spotify when authed OR free-available. Verified gate composition: OFF by default (no surprise scraping); ON + no auth + installed -> available & serving; AUTHED -> official always wins (free never runs); missing package -> gracefully unavailable. JS + integrity + 111 tests green.
This commit is contained in:
parent
0eff9e3708
commit
2667eaec87
4 changed files with 45 additions and 6 deletions
|
|
@ -2318,9 +2318,19 @@ def get_status():
|
||||||
if t.get('status') in ('downloading', 'searching', 'post_processing', 'queued', 'pending'):
|
if t.get('status') in ('downloading', 'searching', 'post_processing', 'queued', 'pending'):
|
||||||
active_dl_count += 1
|
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 = {
|
status_data = {
|
||||||
'metadata_source': metadata_status['metadata_source'],
|
'metadata_source': metadata_status['metadata_source'],
|
||||||
'spotify': metadata_status['spotify'],
|
'spotify': spotify_status,
|
||||||
'media_server': _status_cache['media_server'],
|
'media_server': _status_cache['media_server'],
|
||||||
'soulseek': soulseek_data,
|
'soulseek': soulseek_data,
|
||||||
'active_media_server': active_server,
|
'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.
|
Drives the green/yellow header gradient. No API calls — just config reads.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return jsonify({
|
result = {
|
||||||
service: {'configured': _is_service_configured(service)}
|
service: {'configured': _is_service_configured(service)}
|
||||||
for service in SERVICE_CONFIG_REGISTRY
|
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:
|
except Exception as e:
|
||||||
logger.error(f"config-status error: {e}")
|
logger.error(f"config-status error: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
|
||||||
|
|
@ -3844,8 +3844,15 @@
|
||||||
<option value="musicbrainz">MusicBrainz</option>
|
<option value="musicbrainz">MusicBrainz</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="metadata-spotify-free">
|
||||||
|
<span>Enable Spotify Free (no credentials)</span>
|
||||||
|
</label>
|
||||||
|
<div class="callback-help">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.</div>
|
||||||
|
</div>
|
||||||
<div class="callback-info">
|
<div class="callback-info">
|
||||||
<div class="callback-help">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.</div>
|
<div class="callback-help">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.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,9 @@ function syncMetadataSourceSelection(source) {
|
||||||
|
|
||||||
function _isMetadataSourceSelectable(source) {
|
function _isMetadataSourceSelectable(source) {
|
||||||
if (source === 'spotify') {
|
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') {
|
if (source === 'discogs') {
|
||||||
const token = document.getElementById('discogs-token');
|
const token = document.getElementById('discogs-token');
|
||||||
|
|
@ -1045,6 +1047,8 @@ async function loadSettingsData() {
|
||||||
|
|
||||||
// Populate Metadata source setting
|
// Populate Metadata source setting
|
||||||
document.getElementById('metadata-fallback-source').value = settings.metadata?.fallback_source || 'deezer';
|
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
|
// Populate Hydrabase settings
|
||||||
const hbConfig = settings.hydrabase || {};
|
const hbConfig = settings.hydrabase || {};
|
||||||
|
|
@ -2842,7 +2846,8 @@ async function saveSettings(quiet = false) {
|
||||||
token: document.getElementById('discogs-token').value,
|
token: document.getElementById('discogs-token').value,
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
fallback_source: metadataSource
|
fallback_source: metadataSource,
|
||||||
|
spotify_free: document.getElementById('metadata-spotify-free')?.checked === true
|
||||||
},
|
},
|
||||||
hydrabase: {
|
hydrabase: {
|
||||||
url: document.getElementById('hydrabase-url').value,
|
url: document.getElementById('hydrabase-url').value,
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,10 @@ async function fetchSourceConfiguredMap() {
|
||||||
for (const src of SOURCE_ORDER) {
|
for (const src of SOURCE_ORDER) {
|
||||||
if (_ALWAYS_CONFIGURED_SOURCES.has(src)) {
|
if (_ALWAYS_CONFIGURED_SOURCES.has(src)) {
|
||||||
map[src] = true;
|
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 {
|
} else {
|
||||||
map[src] = !!(data[src] && data[src].configured);
|
map[src] = !!(data[src] && data[src].configured);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue