From 12038bde0864825655ff3989787499dca4a2dc35 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 5 Jun 2026 21:50:08 -0700 Subject: [PATCH] Let the Spotify worker resume during a rate-limit ban when Free can bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #798 follow-up. The enrichment worker's own loop already bridges to the no-creds Spotify Free source during a rate-limit ban (its guard checks is_spotify_metadata_available()). But the resume button's pre-check (_spotify_resume_pre_check) blocked resume on ANY rate-limit with no awareness of Free — so a Free-opted-in user who got rate-limited was locked out of restarting the worker, unable to fall through to the free API. Fix: the resume guard now mirrors the worker. Block only when rate-limited AND nothing can serve (plain auth, no Free) — where resuming would just sleep out the ban. When Free is available it serves during the ban (is_spotify_authenticated() is False while banned, so is_spotify_metadata_available() reports the free source), so resume is allowed and the worker bridges via Free, then returns to real auth once the ban lifts. Stays real-API-first; Free is only the bridge. The rule is pinned in a pure helper should_block_rate_limited_resume() next to the other gate functions, with 3 tests. Full suite clean (only pre-existing soundcloud /app env failures remain). --- core/spotify_free_metadata.py | 14 ++++++++++++++ tests/test_spotify_free_metadata.py | 22 ++++++++++++++++++++++ web_server.py | 20 ++++++++++++++++---- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/core/spotify_free_metadata.py b/core/spotify_free_metadata.py index 87df34f2..5b037183 100644 --- a/core/spotify_free_metadata.py +++ b/core/spotify_free_metadata.py @@ -65,6 +65,20 @@ def should_offer_spotify_metadata(authenticated: bool, free_available: bool) -> return authenticated or free_available +def should_block_rate_limited_resume(rate_limited: bool, metadata_available: bool) -> bool: + """Whether to refuse resuming the Spotify enrichment worker. + + The worker's own loop bridges to the no-creds free source during a ban + (its rate-limit guard checks ``is_spotify_metadata_available()``). The + resume button must mirror that: block ONLY when rate-limited AND nothing + can serve (plain auth, no free) — otherwise resuming just sleeps. When the + free fallback is available, ``metadata_available`` is True during a ban + (``is_spotify_authenticated()`` returns False while banned), so resume is + allowed and the worker bridges via free. + """ + return rate_limited and not metadata_available + + # -------------------------------------------------------------------------- # Normalizers (pure — unit-testable against captured fixtures) # -------------------------------------------------------------------------- diff --git a/tests/test_spotify_free_metadata.py b/tests/test_spotify_free_metadata.py index e09c367d..20ddae39 100644 --- a/tests/test_spotify_free_metadata.py +++ b/tests/test_spotify_free_metadata.py @@ -10,12 +10,34 @@ from __future__ import annotations from core.spotify_free_metadata import ( normalize_artist, + should_block_rate_limited_resume, should_offer_spotify_metadata, should_use_free_fallback, spotify_free_installed, ) +# --------------------------------------------------------------------------- +# should_block_rate_limited_resume — the worker resume guard +# --------------------------------------------------------------------------- + +def test_resume_blocked_when_rate_limited_and_nothing_serves(): + # Plain auth, no free: resuming during a ban would just sleep → block. + assert should_block_rate_limited_resume(rate_limited=True, metadata_available=False) is True + + +def test_resume_allowed_when_free_can_bridge(): + # Rate-limited but free is available (metadata_available True during a ban + # because is_spotify_authenticated() is False while banned) → allow resume, + # the worker bridges via free. + assert should_block_rate_limited_resume(rate_limited=True, metadata_available=True) is False + + +def test_resume_never_blocked_when_not_rate_limited(): + assert should_block_rate_limited_resume(rate_limited=False, metadata_available=False) is False + assert should_block_rate_limited_resume(rate_limited=False, metadata_available=True) is False + + # --------------------------------------------------------------------------- # should_use_free_fallback — the activation gate # --------------------------------------------------------------------------- diff --git a/web_server.py b/web_server.py index dc84cabb..05a40160 100644 --- a/web_server.py +++ b/web_server.py @@ -34883,12 +34883,24 @@ from core.enrichment.services import ( def _spotify_resume_pre_check(): - """Mirror the inline Spotify rate-limit guard from the legacy - ``/api/spotify-enrichment/resume`` route. Returns - ``(429, message)`` to short-circuit when banned, ``None`` when ok.""" + """Guard the Spotify enrichment worker's resume button against a rate-limit + ban. Returns ``(429, message)`` to short-circuit when banned, ``None`` ok. + + Mirrors the worker's own loop: if the opt-in Spotify-Free fallback can + serve enrichment, allow resume and let the worker bridge via the no-creds + source during the ban. Block only when rate-limited AND nothing can serve + (plain auth, no free) — where resuming would just sleep. + """ try: if _spotify_rate_limited(): - return (429, 'Cannot resume while Spotify is rate limited') + from core.spotify_free_metadata import should_block_rate_limited_resume + try: + metadata_available = bool(spotify_client.is_spotify_metadata_available()) + except Exception as e: + logger.debug("spotify free-availability check failed: %s", e) + metadata_available = False + if should_block_rate_limited_resume(True, metadata_available): + return (429, 'Cannot resume while Spotify is rate limited') except Exception as e: logger.debug("spotify rate-limit pre-check failed: %s", e) return None