fix: pause Spotify worker on non-Spotify primary + cut daily budget to 500

The Spotify enrichment worker was auto-starting unconditionally at boot,
hammering /v1/search to match every track in the library against the
Spotify catalog regardless of which metadata source the user had
actually chosen as their primary. Users on Deezer, iTunes, Discogs,
or Hydrabase saw multi-hour 429 bans (typically 14400s) on Spotify
even though they never wanted Spotify-driven enrichment in the first
place — the worker generated dead API traffic the user neither asked
for nor benefited from.

Compounded by Spotify's February 2026 API tightening:
- /v1/search max limit cut from 50 to 10 per request, default from
  20 to 5 — every track now needs more pagination, more requests.
- Sustained-rate detection more aggressive — repeated calls over
  hours trigger automated long-form bans even when each individual
  30-second window is well under the rolling limit.

Result: a user on Deezer would see their Spotify connection get banned
for 4 hours after about 30 tracks of enrichment activity, with no
recourse other than manually pausing the worker each session.

Two-part fix:

1. Boot gate (web_server.py): only auto-start the worker when
   `get_primary_source() == 'spotify'`. Otherwise initialize in the
   paused state with an explanatory log line. The settings UI manual
   unpause control remains functional for users who explicitly want
   background Spotify enrichment regardless of primary source.

   Boot logic:
   - User manually paused (existing config) → stays paused (preserved).
   - Primary = 'spotify' → starts running (preserved).
   - Primary != 'spotify' → starts paused with log line.

2. Daily budget reduction (core/spotify_worker.py): drop from 3000 to
   500 items per calendar day. The 3000 cap was set when /v1/search
   returned 50 results per call; now that it caps at 10, each track
   needs roughly 5x the API load to find a confident match. 500/day
   keeps the worker productive without crossing Spotify's hidden
   sustained-rate detection threshold.

The runtime side of the boot gate — auto-pausing when the user
switches primary source mid-session — is out of scope. The settings
UI already exposes the manual toggle, and primary-source switches are
infrequent enough that requiring a manual unpause after the fact is
acceptable.

Full suite: 1355 passing. Ruff clean.
This commit is contained in:
Broque Thomas 2026-04-29 12:00:49 -07:00
parent 58a4c1905b
commit 1d5f1e2047
2 changed files with 25 additions and 5 deletions

View file

@ -55,8 +55,13 @@ class SpotifyWorker:
self.inter_item_sleep = 1.5 # Between top-level items (each can trigger 5+ paginated calls)
self.batch_inter_item_sleep = 0.1 # Between local matches within a batch (no API calls)
# Daily budget — caps how many items this worker processes per calendar day
self.daily_budget = 3000
# Daily budget — caps how many items this worker processes per calendar day.
# Lowered from 3000 to 500 after Spotify's February 2026 API tightening
# (/v1/search max limit cut from 50 to 10) increased the per-track API call
# cost. Sustained 3000-item runs were tripping Spotify's automated abuse
# detection and earning multi-hour 429 bans. 500/day keeps the worker
# productive without crossing the threshold.
self.daily_budget = 500
self._daily_items_processed = 0
self._daily_date = date.today()

View file

@ -37103,18 +37103,33 @@ def deezer_resume():
# ================================================================================================
# --- Spotify Worker Initialization ---
# The Spotify enrichment worker calls `/v1/search` continuously to match library
# tracks against Spotify's catalog. After Spotify's February 2026 API tightening
# (search limit cut from 50→10, sustained-rate detection more aggressive), running
# this worker when the user has chosen a non-Spotify primary metadata source
# (Deezer, iTunes, Discogs, Hydrabase) generates dead API traffic that triggers
# multi-hour 429 bans and disrupts the user's actual selected source.
#
# Gate the worker at boot: only auto-start when Spotify is the configured primary
# source. Users on other sources can manually unpause the worker from settings if
# they explicitly want background Spotify enrichment.
spotify_enrichment_worker = None
try:
from core.metadata_service import get_primary_source as _get_primary_source
from database.music_database import MusicDatabase
spotify_enrichment_db = MusicDatabase()
spotify_enrichment_worker = SpotifyWorker(database=spotify_enrichment_db)
if config_manager.get('spotify_enrichment_paused', False):
_primary = _get_primary_source()
_user_paused = config_manager.get('spotify_enrichment_paused', False)
if _user_paused or _primary != 'spotify':
spotify_enrichment_worker.paused = True # Set BEFORE start() to prevent race condition
spotify_enrichment_worker.start()
if spotify_enrichment_worker.paused:
if not spotify_enrichment_worker.paused:
logger.info("Spotify enrichment worker initialized and started")
elif _user_paused:
logger.info("Spotify enrichment worker initialized (paused — restored from config)")
else:
logger.info("Spotify enrichment worker initialized and started")
logger.info(f"Spotify enrichment worker initialized (paused — primary metadata source is '{_primary}', not Spotify)")
except Exception as e:
logger.error(f"Spotify enrichment worker initialization failed: {e}")
spotify_enrichment_worker = None