Spotify: rename "Spotify Free" → "Spotify (no auth)", default enrichment to it
Per Boulder's calls on the new enrichment toggle: - Naming: "Spotify Free" was misleading (it's a hybrid — pick it, connect an account, and sync still uses your official playlists). Relabel the user-facing strings to "Spotify (no auth)" — the real distinction is needs-credentials vs not. Internal value/key (spotify_free, _free_*) unchanged, so no migration. - Default ON: metadata.spotify_free_enrichment now defaults True (worker + UI load both treat unset as on). So bulk enrichment runs on the no-auth path by default and the official account is reserved for interactive search/sync; turn the toggle off to enrich through the connected account. The toggle overrides auth for the worker (authed users still enrich via no-auth) — matching the intended model. - Worker runs on the toggle alone: is_spotify_metadata_available() now honors _prefer_free (+ package installed), so the worker enriches via no-auth even with no account connected and no 'no-auth' source selected. Only fires on a client carrying the flag (the worker's own), so interactive/watchlist availability is unchanged. - UI: moved the toggle from "Metadata Source" to the Spotify section next to the auth fields, always visible, on by default. Help notes the genre trade-off. Tests: prefer_free makes metadata available without auth/source (and is inert without the package); interactive availability unaffected. 218 Spotify tests pass.
This commit is contained in:
parent
fd3ce8ba6e
commit
90174de4b2
5 changed files with 71 additions and 28 deletions
|
|
@ -633,6 +633,13 @@ class SpotifyClient:
|
|||
watchlist) use THIS instead of ``is_spotify_authenticated()`` so the
|
||||
free source is reachable. Does NOT change auth semantics."""
|
||||
from core.spotify_free_metadata import should_offer_spotify_metadata
|
||||
# The enrichment worker's prefer-free opt-in (set on its own client)
|
||||
# makes the no-auth source the active path even without auth or the
|
||||
# 'no-auth Spotify' source choice — so metadata IS available to it. This
|
||||
# only fires on a client carrying _prefer_free (the worker's), so
|
||||
# interactive/watchlist availability is unchanged.
|
||||
if getattr(self, '_prefer_free', False) and self._free_installed():
|
||||
return True
|
||||
try:
|
||||
authed = self.is_spotify_authenticated()
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -213,26 +213,29 @@ class SpotifyWorker:
|
|||
interruptible_sleep(self._stop_event, min(remaining, 60)) # Check again every 60s max
|
||||
continue
|
||||
|
||||
# Is the worker serving via the no-creds Spotify Free source this
|
||||
# iteration? The daily budget and post-ban cooldown both exist to
|
||||
# protect the REAL authenticated API from bans — they don't apply
|
||||
# to free (a different, anonymous path). Computed once and reused
|
||||
# below; the loop already probes auth, so no extra quota cost.
|
||||
# Worker opt-in: when the user enables Spotify Free for enrichment
|
||||
# (metadata.spotify_free_enrichment), prefer the no-creds source
|
||||
# even while authed + under budget — bulk enrichment is the work
|
||||
# that bans the real API, so this spares the official quota for
|
||||
# interactive use. _free_active() honors this flag (and still falls
|
||||
# back to official only if free can't serve); set only on the
|
||||
# worker's own client, so interactive paths stay official-first.
|
||||
# Cheap config read; harmless when Spotify Free isn't installed.
|
||||
# Enrichment runs on the no-auth Spotify source by DEFAULT
|
||||
# (metadata.spotify_free_enrichment, ON unless turned off): bulk
|
||||
# enrichment is the workload that bans the real API, so we keep it
|
||||
# off your connected account's quota and reserve official Spotify
|
||||
# for interactive search + playlist sync. The flag overrides auth
|
||||
# for the worker (authed users still enrich via the no-auth path)
|
||||
# and also lets the worker run with no auth at all. _free_active()
|
||||
# and is_spotify_metadata_available() both honor it; set only on
|
||||
# the worker's OWN client, so interactive paths stay official-first.
|
||||
# Harmless when the no-auth package isn't installed (the methods
|
||||
# fall back to official, then iTunes/Deezer).
|
||||
try:
|
||||
from config.settings import config_manager as _cfg
|
||||
self.client._prefer_free = bool(
|
||||
_cfg.get('metadata.spotify_free_enrichment', False))
|
||||
_cfg.get('metadata.spotify_free_enrichment', True))
|
||||
except Exception: # noqa: S110 — prefer-free toggle is best-effort
|
||||
self.client._prefer_free = False
|
||||
self.client._prefer_free = True
|
||||
|
||||
# Is the worker serving via the no-auth Spotify source this
|
||||
# iteration? The daily budget and post-ban cooldown both exist to
|
||||
# protect the REAL authenticated API from bans — they don't apply
|
||||
# to the no-auth path. Computed once and reused below; the loop
|
||||
# already probes auth, so no extra quota cost.
|
||||
budget_exhausted = self._is_daily_budget_exhausted()
|
||||
|
||||
# Daily budget is a REAL-API ban protection. When it's spent, if
|
||||
|
|
|
|||
|
|
@ -393,3 +393,34 @@ def test_search_albums_diverts_to_free_when_budget_exhausted_and_authed():
|
|||
results = c.search_albums('Kendrick Lamar GNX', limit=5,
|
||||
artist='Kendrick Lamar', album='GNX')
|
||||
assert len(results) == 1 and results[0].id == 'al2'
|
||||
|
||||
|
||||
# ── default-ON enrichment: prefer_free makes metadata available to the worker ──
|
||||
|
||||
def _metadata_available(prefer_free, installed, authed=False, selected=False):
|
||||
c = SpotifyClient.__new__(SpotifyClient)
|
||||
if prefer_free:
|
||||
c._prefer_free = True
|
||||
with patch.object(SpotifyClient, 'is_spotify_authenticated', return_value=authed), \
|
||||
patch('core.spotify_client.config_manager') as cm, \
|
||||
patch('core.spotify_client._is_globally_rate_limited', return_value=False), \
|
||||
patch.object(_sfm, 'spotify_free_installed', return_value=installed):
|
||||
cm.get.side_effect = lambda k, d=None: selected if k == 'metadata.spotify_free' else d
|
||||
return c.is_spotify_metadata_available()
|
||||
|
||||
|
||||
def test_prefer_free_makes_metadata_available_without_auth_or_source():
|
||||
# Default-ON enrichment: the worker runs via the no-auth path on the toggle
|
||||
# alone — no account connected, no 'no-auth Spotify' source selected.
|
||||
assert _metadata_available(prefer_free=True, installed=True) is True
|
||||
|
||||
|
||||
def test_prefer_free_metadata_unavailable_without_package():
|
||||
assert _metadata_available(prefer_free=True, installed=False) is False
|
||||
|
||||
|
||||
def test_interactive_metadata_availability_unaffected_by_prefer_free():
|
||||
# A client WITHOUT _prefer_free (interactive/global): no auth + no source -> unavailable.
|
||||
assert _metadata_available(prefer_free=False, installed=True) is False
|
||||
# ...and authed is available as before.
|
||||
assert _metadata_available(prefer_free=False, installed=True, authed=True) is True
|
||||
|
|
|
|||
|
|
@ -3839,7 +3839,7 @@
|
|||
<label>Primary metadata source:</label>
|
||||
<select id="metadata-fallback-source">
|
||||
<option value="spotify">Spotify</option>
|
||||
<option value="spotify_free">Spotify Free (no credentials)</option>
|
||||
<option value="spotify_free">Spotify (no auth)</option>
|
||||
<option value="itunes">iTunes / Apple Music</option>
|
||||
<option value="deezer">Deezer</option>
|
||||
<option value="discogs">Discogs</option>
|
||||
|
|
@ -3849,20 +3849,11 @@
|
|||
<div class="callback-info">
|
||||
<div class="callback-help">Where artist, album, and track metadata comes from:<br>
|
||||
• <strong>Spotify</strong> — official Spotify; connect your account in the Spotify section below.<br>
|
||||
• <strong>Spotify Free</strong> — the same Spotify catalog with no account needed. It's unofficial and best-effort (may break if Spotify changes its site), can't search albums by name (album results come from iTunes/Deezer instead), and can't read your personal library or playlists.<br>
|
||||
• <strong>Spotify (no auth)</strong> — the same Spotify catalog with no account needed. It's unofficial and best-effort (may break if Spotify changes its site) and can't read your personal library or playlists.<br>
|
||||
• <strong>Deezer</strong> / <strong>iTunes</strong> — free, no account.<br>
|
||||
• <strong>MusicBrainz</strong> — free, but limited to 1 request/sec.<br>
|
||||
• <strong>Discogs</strong> — needs a personal access token.<br><br>
|
||||
Tip: if you pick <strong>Spotify Free</strong> and later connect a real Spotify account, it uses the official account normally and only falls back to free while Spotify is rate-limited — then switches back automatically.</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 12px;">
|
||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-weight: normal;">
|
||||
<input type="checkbox" id="metadata-spotify-free-enrichment" style="width: auto; margin: 0;">
|
||||
<span>Use Spotify Free for background enrichment</span>
|
||||
</label>
|
||||
<div class="callback-info">
|
||||
<div class="callback-help">Runs the background enrichment worker on the no-credentials Spotify Free source even when your Spotify account is connected — sparing your official API quota (and avoiding rate-limit bans) for interactive search and playlist sync, since bulk enrichment is what usually trips Spotify's limits. Needs the bundled Spotify Free package. Trade-off: Spotify Free can't provide artist genres. Leave off to enrich via your official account as usual.</div>
|
||||
</div>
|
||||
Tip: if you pick <strong>Spotify (no auth)</strong> and later connect a real Spotify account, it uses the official account normally and only falls back to no-auth while Spotify is rate-limited — then switches back automatically.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -3902,6 +3893,15 @@
|
|||
onclick="disconnectSpotify()" style="display: none;">🔌
|
||||
Disconnect</button>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 14px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 14px;">
|
||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-weight: normal;">
|
||||
<input type="checkbox" id="metadata-spotify-free-enrichment" style="width: auto; margin: 0;">
|
||||
<span>Use Spotify (no auth) for background enrichment</span>
|
||||
</label>
|
||||
<div class="callback-info">
|
||||
<div class="callback-help">Runs the background metadata enrichment worker on the no-auth Spotify source instead of this connected account — keeping bulk enrichment off your official API quota (and dodging rate-limit bans), so your account is reserved for interactive search and playlist sync. <strong>On by default.</strong> Turn off to enrich through your connected account instead. Works even with no account connected. Note: the no-auth source can't supply artist genres.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1062,7 +1062,9 @@ async function loadSettingsData() {
|
|||
? 'spotify_free' : _fbSrc;
|
||||
document.getElementById('metadata-fallback-source').value = _metaSel;
|
||||
const _efEl = document.getElementById('metadata-spotify-free-enrichment');
|
||||
if (_efEl) _efEl.checked = settings.metadata?.spotify_free_enrichment === true;
|
||||
// Default ON: unset (undefined) reads as enabled, matching the worker's
|
||||
// config default (metadata.spotify_free_enrichment defaults True).
|
||||
if (_efEl) _efEl.checked = settings.metadata?.spotify_free_enrichment !== false;
|
||||
|
||||
// Populate Hydrabase settings
|
||||
const hbConfig = settings.hydrabase || {};
|
||||
|
|
|
|||
Loading…
Reference in a new issue