Fix enrichment widget showing Running when rate limited (#217)

The tooltip only checked paused/authenticated/idle/running states.
When Spotify was rate limited or daily budget exhausted, the worker
thread was still alive (sleeping in guards) so it showed "Running"
with no current item and stale 0% progress.

Now checks rate_limited and daily_budget.exhausted before running:
- Rate limited: "Rate Limited — Waiting Xm for rate limit to clear"
- Budget exhausted: "Daily Limit Reached — Resets in Xh Xm"
- No current item: "Waiting for next item..." instead of blank

Also adds rate_limit info object to get_stats() response for the
countdown display.
This commit is contained in:
Broque Thomas 2026-03-30 08:12:56 -07:00
parent 59587162cd
commit 7133595e0d
4 changed files with 30 additions and 0 deletions

View file

@ -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(),

View file

@ -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",

View file

@ -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' },

View file

@ -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...';
}
}