Add Spotify rate limit guards to all repair jobs

Repair jobs were making Spotify API calls without checking the global
rate limit ban, causing them to churn through every item getting rejected
one by one during a ban. Now all Spotify calls in repair jobs check
context.is_spotify_rate_limited() first and skip/fallback gracefully.

- Add is_spotify_rate_limited() helper to JobContext (base.py)
- Guard calls in track_number_repair, album_completeness,
  missing_cover_art, and metadata_gap_filler
- Jobs fall through to iTunes/MusicBrainz fallbacks when rate-limited
pull/253/head
Broque Thomas 2 months ago
parent 186671aa2e
commit 2003e58358

@ -90,7 +90,7 @@ class AlbumCompletenessJob(RepairJob):
# If we don't know the expected track count, try to get it from API
expected_total = db_track_count
if not expected_total and context.spotify_client:
if not expected_total and context.spotify_client and not context.is_spotify_rate_limited():
try:
album_data = context.spotify_client.get_album(spotify_album_id)
if album_data:
@ -106,7 +106,7 @@ class AlbumCompletenessJob(RepairJob):
# Album is incomplete — try to find which tracks are missing
missing_tracks = []
if context.spotify_client:
if context.spotify_client and not context.is_spotify_rate_limited():
try:
api_tracks = context.spotify_client.get_album_tracks(spotify_album_id)
if api_tracks and 'items' in api_tracks:

@ -41,6 +41,18 @@ class JobContext:
"""Return True if the worker should stop."""
return self.should_stop() if self.should_stop else False
def is_spotify_rate_limited(self) -> bool:
"""Check if Spotify is currently under a global rate limit ban.
Jobs should call this before making Spotify API calls in their
scan loops to avoid churning through items uselessly.
"""
try:
from core.spotify_client import SpotifyClient
return SpotifyClient.is_rate_limited()
except Exception:
return False
def wait_if_paused(self):
"""Block until unpaused or stopped. Returns True if should stop."""
import time

@ -109,7 +109,7 @@ class MetadataGapFillerJob(RepairJob):
found_fields = {}
# Try Spotify enrichment first (most reliable for ISRC)
if spotify_track_id and context.spotify_client:
if spotify_track_id and context.spotify_client and not context.is_spotify_rate_limited():
try:
track_data = context.spotify_client.get_track_details(spotify_track_id)
if track_data:

@ -147,6 +147,9 @@ class MissingCoverArtJob(RepairJob):
return None
try:
if context.is_spotify_rate_limited():
return None
# If we have a Spotify album ID, fetch directly
if spotify_album_id and client.is_spotify_authenticated():
album_data = client.get_album(spotify_album_id)

@ -314,7 +314,7 @@ class TrackNumberRepairJob(RepairJob):
# Fallback 3: Spotify track ID → discover album ID
client = context.spotify_client
if spotify_track_id and client and client.is_spotify_authenticated():
if spotify_track_id and client and client.is_spotify_authenticated() and not context.is_spotify_rate_limited():
try:
track_details = client.get_track_details(spotify_track_id)
if track_details and track_details.get('album', {}).get('id'):
@ -328,7 +328,7 @@ class TrackNumberRepairJob(RepairJob):
logger.debug("Spotify track lookup failed for %s: %s", spotify_track_id, e)
# Fallback 4: Search Spotify/iTunes by album name + artist
if album_name and client:
if album_name and client and not context.is_spotify_rate_limited():
try:
query = f"{artist_name} {album_name}" if artist_name else album_name
results = client.search_albums(query, limit=5)
@ -1031,7 +1031,7 @@ def _get_album_tracklist(album_id: str, context: JobContext,
# Try Spotify first
client = context.spotify_client
if client and client.is_spotify_authenticated():
if client and client.is_spotify_authenticated() and not context.is_spotify_rate_limited():
try:
data = client.get_album_tracks(album_id)
if data and 'items' in data and data['items']:

Loading…
Cancel
Save