From 3fcfa900bd228540b928441404939187b40e4eb5 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 5 Jun 2026 22:08:07 -0700 Subject: [PATCH] Show "Running (Spotify Free)" instead of "rate limited" while the worker bridges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #798 follow-up. When the real Spotify API is banned but the worker keeps matching via the no-creds Spotify Free source, every status surface still read the literal rate_limited=True flag and showed "Rate Limited / waiting Nm" — so the dashboard bubble looked paused/stuck even though the worker (visible in Manage Workers) was actively matching. - spotify_worker.get_stats() adds a `using_free` flag: rate_limited AND is_spotify_metadata_available(). Computed ONLY when rate-limited, where is_spotify_authenticated() returns False without an API probe, so the 2s status loop pays no quota cost. - Dashboard bubble (enrichment.js): when using_free, the bubble is 'active', the tooltip says "Running (Spotify Free)" and "Now: X (via Spotify Free)" instead of "Rate Limited / Waiting Nm". Clicking it pauses (works) rather than hitting the resume-blocked toast. - Manage Workers (enrichment-manager.js): status pill shows "Running (Spotify Free)"; the warning banner is replaced with a calm "matching via Spotify Free until the ban lifts" note. The flag flows through both feeds (the /api/enrichment/spotify/status poll and the WebSocket enrichment:* push) since both serialize get_stats(). Genuinely-stuck (no-free) workers still show "Rate Limited". --- core/spotify_worker.py | 9 +++++++++ webui/static/enrichment-manager.js | 6 ++++++ webui/static/enrichment.js | 13 ++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/core/spotify_worker.py b/core/spotify_worker.py index f8e8cc3a..966d964f 100644 --- a/core/spotify_worker.py +++ b/core/spotify_worker.py @@ -126,11 +126,19 @@ class SpotifyWorker: rate_limit_info = self.client.get_rate_limit_info() if rate_limited else None in_cooldown = self.client.get_post_ban_cooldown_remaining() > 0 authenticated = self.client.sp is not None + # Is the worker still serving via the no-creds Spotify Free source + # despite the real-API ban? Only check WHEN rate-limited: during a + # ban is_spotify_authenticated() returns False without an API probe, + # so is_spotify_metadata_available() reduces to "is free available" + # (no quota cost). Lets the UI show "via Spotify Free" instead of a + # misleading "rate limited / waiting" while the worker keeps matching. + using_free = bool(rate_limited and self.client.is_spotify_metadata_available()) except Exception: authenticated = False rate_limited = False rate_limit_info = None in_cooldown = False + using_free = False return { 'enabled': True, @@ -139,6 +147,7 @@ class SpotifyWorker: 'idle': is_idle, 'authenticated': authenticated, 'rate_limited': rate_limited, + 'using_free': using_free, 'rate_limit': rate_limit_info, 'daily_budget': self._get_daily_budget_info(), 'current_item': self.current_item, diff --git a/webui/static/enrichment-manager.js b/webui/static/enrichment-manager.js index 9f8b0c28..a6dea8af 100644 --- a/webui/static/enrichment-manager.js +++ b/webui/static/enrichment-manager.js @@ -274,6 +274,9 @@ async function _emPollSelected() { function _emStatusInfo(status) { if (!status || !status.enabled) return { cls: 'disabled', label: 'Disabled' }; + // Rate-limited on the real API but still matching via the no-creds Spotify + // Free source — show as running, not stuck (#798 bridge). + if (status.using_free) return { cls: 'running', label: 'Running (Spotify Free)' }; if (status.rate_limited) return { cls: 'ratelimited', label: 'Rate-limited' }; if (status.paused) return { cls: 'paused', label: 'Paused' }; if (status.idle) return { cls: 'idle', label: 'Idle' }; @@ -709,6 +712,9 @@ function _emUpdateHeaderLive() { cls += ' em-banner--warn'; html = '⚙️ This source isn’t configured — add its credentials in Settings. ' + 'Browsing works, but matches and retries won’t run until it’s set up.'; + } else if (status && status.using_free) { + // Real API banned but bridging via the no-creds Spotify Free source. + html = '✓ Spotify is rate-limited — matching via Spotify Free until the ban lifts.'; } else if (status && status.rate_limited) { cls += ' em-banner--warn'; const rl = status.rate_limit || {}; diff --git a/webui/static/enrichment.js b/webui/static/enrichment.js index 36208c30..649920e5 100644 --- a/webui/static/enrichment.js +++ b/webui/static/enrichment.js @@ -457,6 +457,10 @@ function updateSpotifyEnrichmentStatusFromData(data) { const notAuthenticated = data.authenticated === false; const isRateLimited = data.rate_limited === true; + // The real API is banned but the worker is still matching via the no-creds + // Spotify Free source — treat it as running, not stuck (#798 bridge). + const bridgingFree = data.using_free === true; + const rateLimitedStuck = isRateLimited && !bridgingFree; const budgetExhausted = data.daily_budget && data.daily_budget.exhausted; button.classList.remove('active', 'paused', 'complete', 'no-auth'); @@ -464,7 +468,7 @@ function updateSpotifyEnrichmentStatusFromData(data) { button.classList.add('paused'); } else if (notAuthenticated) { button.classList.add('no-auth'); - } else if (isRateLimited || budgetExhausted) { + } else if (rateLimitedStuck || budgetExhausted) { button.classList.add('paused'); } else if (data.idle) { button.classList.add('complete'); @@ -479,7 +483,8 @@ function updateSpotifyEnrichmentStatusFromData(data) { if (tooltipStatus) { if (data.paused) { tooltipStatus.textContent = 'Paused'; } else if (notAuthenticated) { tooltipStatus.textContent = 'Not Authenticated'; } - else if (isRateLimited) { tooltipStatus.textContent = 'Rate Limited'; } + else if (rateLimitedStuck) { tooltipStatus.textContent = 'Rate Limited'; } + else if (bridgingFree) { tooltipStatus.textContent = 'Running (Spotify Free)'; } else if (budgetExhausted) { tooltipStatus.textContent = 'Daily Limit Reached'; } else if (data.idle) { tooltipStatus.textContent = 'Complete'; } else if (data.running) { tooltipStatus.textContent = 'Running'; } @@ -491,10 +496,12 @@ function updateSpotifyEnrichmentStatusFromData(data) { tooltipCurrent.textContent = notAuthenticated ? 'Connect Spotify in Settings to enrich' : 'Click to resume'; } else if (notAuthenticated) { tooltipCurrent.textContent = 'Connect Spotify in Settings to enrich'; - } else if (isRateLimited) { + } else if (rateLimitedStuck) { const info = data.rate_limit || {}; const remaining = info.remaining_seconds || 0; tooltipCurrent.textContent = remaining > 0 ? `Waiting ${Math.ceil(remaining / 60)}m for rate limit to clear` : 'Waiting for rate limit to clear'; + } else if (bridgingFree && data.current_item && data.current_item.name) { + tooltipCurrent.textContent = `Now: ${data.current_item.name} (via Spotify Free)`; } else if (budgetExhausted) { const resets = data.daily_budget.resets_in_seconds || 0; const hours = Math.floor(resets / 3600);