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 || {};