diff --git a/core/spotify_worker.py b/core/spotify_worker.py index 62283ebd..4444ff47 100644 --- a/core/spotify_worker.py +++ b/core/spotify_worker.py @@ -107,11 +107,13 @@ class SpotifyWorker: # every 60 seconds indefinitely, wasting API quota and risking rate limits. # Instead, use sp presence as a lightweight proxy for "configured". rate_limited = self.client.is_rate_limited() + 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 except Exception: authenticated = False rate_limited = False + rate_limit_info = None in_cooldown = False return { @@ -121,6 +123,7 @@ class SpotifyWorker: 'idle': is_idle, 'authenticated': authenticated, 'rate_limited': rate_limited, + 'rate_limit': rate_limit_info, 'daily_budget': self._get_daily_budget_info(), 'current_item': self.current_item, 'stats': self.stats.copy(), diff --git a/web_server.py b/web_server.py index 7cec5ef0..9b5664ae 100644 --- a/web_server.py +++ b/web_server.py @@ -19071,6 +19071,15 @@ def get_version_info(): "title": "What's New in SoulSync", "subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes", "sections": [ + { + "title": "🔧 Fix Enrichment Widget Showing 'Running' When Rate Limited", + "description": "Enrichment tooltip now shows Rate Limited or Daily Limit Reached instead of stuck on Running", + "features": [ + "• Shows 'Rate Limited' with countdown when Spotify rate limit is active", + "• Shows 'Daily Limit Reached' with reset time when daily budget is exhausted", + "• Shows 'Waiting for next item...' instead of blank when no current item" + ] + }, { "title": "🧹 Metadata Cache Maintenance", "description": "The cache evictor now runs four maintenance phases to keep the metadata cache clean", diff --git a/webui/static/helper.js b/webui/static/helper.js index c01620a5..1f0f0201 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3403,6 +3403,7 @@ function closeHelperSearch() { const WHATS_NEW = { '2.1': [ // Newest features first + { title: 'Fix Enrichment Status Widget', desc: 'Enrichment tooltip now shows Rate Limited or Daily Limit instead of stuck on Running' }, { title: 'Cache Maintenance', desc: 'Cache evictor now cleans junk entities, orphaned searches, and stale MusicBrainz nulls' }, { title: 'Fix Wishlist Download Selection', desc: 'Download Selection now only downloads checked tracks instead of the entire category' }, { title: 'Fix Tidal OAuth in Docker', desc: 'Tidal redirect URI now uses configured setting instead of Docker container hostname' }, diff --git a/webui/static/script.js b/webui/static/script.js index 6d4805eb..0255d9dc 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -54012,12 +54012,16 @@ function updateSpotifyEnrichmentStatusFromData(data) { if (!button) return; const notAuthenticated = data.authenticated === false; + const isRateLimited = data.rate_limited === true; + const budgetExhausted = data.daily_budget && data.daily_budget.exhausted; button.classList.remove('active', 'paused', 'complete', 'no-auth'); if (data.paused) { button.classList.add('paused'); } else if (notAuthenticated) { button.classList.add('no-auth'); + } else if (isRateLimited || budgetExhausted) { + button.classList.add('paused'); } else if (data.idle) { button.classList.add('complete'); } else if (data.running && !data.paused) { @@ -54031,6 +54035,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 (budgetExhausted) { tooltipStatus.textContent = 'Daily Limit Reached'; } else if (data.idle) { tooltipStatus.textContent = 'Complete'; } else if (data.running) { tooltipStatus.textContent = 'Running'; } else { tooltipStatus.textContent = 'Idle'; } @@ -54041,10 +54047,21 @@ 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) { + 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 (budgetExhausted) { + const resets = data.daily_budget.resets_in_seconds || 0; + const hours = Math.floor(resets / 3600); + const mins = Math.floor((resets % 3600) / 60); + tooltipCurrent.textContent = `Resets in ${hours}h ${mins}m`; } else if (data.idle) { tooltipCurrent.textContent = 'All items processed'; } else if (data.current_item && data.current_item.name) { tooltipCurrent.textContent = `Now: ${data.current_item.name}`; + } else { + tooltipCurrent.textContent = 'Waiting for next item...'; } }