From 90174de4b27ce38531eccd893154bf908050435a Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Tue, 9 Jun 2026 12:55:19 -0700 Subject: [PATCH] =?UTF-8?q?Spotify:=20rename=20"Spotify=20Free"=20?= =?UTF-8?q?=E2=86=92=20"Spotify=20(no=20auth)",=20default=20enrichment=20t?= =?UTF-8?q?o=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/spotify_client.py | 7 ++++++ core/spotify_worker.py | 33 ++++++++++++++++------------- tests/test_spotify_free_metadata.py | 31 +++++++++++++++++++++++++++ webui/index.html | 24 ++++++++++----------- webui/static/settings.js | 4 +++- 5 files changed, 71 insertions(+), 28 deletions(-) diff --git a/core/spotify_client.py b/core/spotify_client.py index 64c3c495..17824d09 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -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: diff --git a/core/spotify_worker.py b/core/spotify_worker.py index 0d83354e..ffc6b07d 100644 --- a/core/spotify_worker.py +++ b/core/spotify_worker.py @@ -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 diff --git a/tests/test_spotify_free_metadata.py b/tests/test_spotify_free_metadata.py index 6df8bdc2..41d6cefd 100644 --- a/tests/test_spotify_free_metadata.py +++ b/tests/test_spotify_free_metadata.py @@ -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 diff --git a/webui/index.html b/webui/index.html index 066ae278..a0c5722b 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3839,7 +3839,7 @@ - Use Spotify Free for background enrichment - -
-
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.
-
+ Tip: if you pick Spotify (no auth) 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. @@ -3902,6 +3893,15 @@ onclick="disconnectSpotify()" style="display: none;">🔌 Disconnect +
+ +
+
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. On by default. 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.
+
+
diff --git a/webui/static/settings.js b/webui/static/settings.js index d9d1e33c..f1212f2e 100644 --- a/webui/static/settings.js +++ b/webui/static/settings.js @@ -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 || {};