From 2003e58358704d5111fef0b045423dcfec8dbe70 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 15 Mar 2026 16:01:48 -0700 Subject: [PATCH] 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 --- core/repair_jobs/album_completeness.py | 4 ++-- core/repair_jobs/base.py | 12 ++++++++++++ core/repair_jobs/metadata_gap_filler.py | 2 +- core/repair_jobs/missing_cover_art.py | 3 +++ core/repair_jobs/track_number_repair.py | 6 +++--- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/core/repair_jobs/album_completeness.py b/core/repair_jobs/album_completeness.py index 227a9399..69686b60 100644 --- a/core/repair_jobs/album_completeness.py +++ b/core/repair_jobs/album_completeness.py @@ -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: diff --git a/core/repair_jobs/base.py b/core/repair_jobs/base.py index ce35cd09..41178780 100644 --- a/core/repair_jobs/base.py +++ b/core/repair_jobs/base.py @@ -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 diff --git a/core/repair_jobs/metadata_gap_filler.py b/core/repair_jobs/metadata_gap_filler.py index e9b0b0c9..b2186ded 100644 --- a/core/repair_jobs/metadata_gap_filler.py +++ b/core/repair_jobs/metadata_gap_filler.py @@ -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: diff --git a/core/repair_jobs/missing_cover_art.py b/core/repair_jobs/missing_cover_art.py index 6edb4fdf..d0824732 100644 --- a/core/repair_jobs/missing_cover_art.py +++ b/core/repair_jobs/missing_cover_art.py @@ -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) diff --git a/core/repair_jobs/track_number_repair.py b/core/repair_jobs/track_number_repair.py index 60a1d91b..1d706af4 100644 --- a/core/repair_jobs/track_number_repair.py +++ b/core/repair_jobs/track_number_repair.py @@ -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']: