Throttle Spotify pagination and harden watchlist scanner against rate limits
- Add rate limiting to all 4 Spotify pagination loops (get_artist_albums, get_user_playlists, get_playlist_tracks, get_album_tracks) — these called sp.next() bypassing the rate_limited decorator entirely, causing unthrottled API calls that triggered 429 bans - Track pagination calls in API rate monitor (separate endpoint names) - Increase DELAY_BETWEEN_ARTISTS from 2s to 4s in watchlist scanner - Abort watchlist scan immediately if Spotify rate limit detected mid-scan instead of continuing to hammer the API
This commit is contained in:
parent
559b89353f
commit
e42fe995d3
2 changed files with 52 additions and 9 deletions
|
|
@ -662,8 +662,18 @@ class SpotifyClient:
|
||||||
playlist = Playlist.from_spotify_playlist(playlist_data, tracks)
|
playlist = Playlist.from_spotify_playlist(playlist_data, tracks)
|
||||||
playlists.append(playlist)
|
playlists.append(playlist)
|
||||||
|
|
||||||
results = self.sp.next(results) if results['next'] else None
|
if results['next']:
|
||||||
|
with _api_call_lock:
|
||||||
|
elapsed = time.time() - _last_api_call_time
|
||||||
|
if elapsed < MIN_API_INTERVAL:
|
||||||
|
time.sleep(MIN_API_INTERVAL - elapsed)
|
||||||
|
globals()['_last_api_call_time'] = time.time()
|
||||||
|
from core.api_call_tracker import api_call_tracker
|
||||||
|
api_call_tracker.record_call('spotify', endpoint='get_user_playlists_page')
|
||||||
|
results = self.sp.next(results)
|
||||||
|
else:
|
||||||
|
results = None
|
||||||
|
|
||||||
logger.info(f"Retrieved {len(playlists)} playlists")
|
logger.info(f"Retrieved {len(playlists)} playlists")
|
||||||
return playlists
|
return playlists
|
||||||
|
|
||||||
|
|
@ -931,7 +941,17 @@ class SpotifyClient:
|
||||||
track = Track.from_spotify_track(track_data)
|
track = Track.from_spotify_track(track_data)
|
||||||
tracks.append(track)
|
tracks.append(track)
|
||||||
|
|
||||||
results = self.sp.next(results) if results['next'] else None
|
if results['next']:
|
||||||
|
with _api_call_lock:
|
||||||
|
elapsed = time.time() - _last_api_call_time
|
||||||
|
if elapsed < MIN_API_INTERVAL:
|
||||||
|
time.sleep(MIN_API_INTERVAL - elapsed)
|
||||||
|
globals()['_last_api_call_time'] = time.time()
|
||||||
|
from core.api_call_tracker import api_call_tracker
|
||||||
|
api_call_tracker.record_call('spotify', endpoint='get_playlist_tracks_page')
|
||||||
|
results = self.sp.next(results)
|
||||||
|
else:
|
||||||
|
results = None
|
||||||
|
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
|
@ -1263,9 +1283,16 @@ class SpotifyClient:
|
||||||
# Collect all tracks starting with first page
|
# Collect all tracks starting with first page
|
||||||
all_tracks = first_page['items'][:]
|
all_tracks = first_page['items'][:]
|
||||||
|
|
||||||
# Fetch remaining pages if they exist
|
# Fetch remaining pages if they exist — throttle pagination
|
||||||
next_page = first_page
|
next_page = first_page
|
||||||
while next_page.get('next'):
|
while next_page.get('next'):
|
||||||
|
with _api_call_lock:
|
||||||
|
elapsed = time.time() - _last_api_call_time
|
||||||
|
if elapsed < MIN_API_INTERVAL:
|
||||||
|
time.sleep(MIN_API_INTERVAL - elapsed)
|
||||||
|
globals()['_last_api_call_time'] = time.time()
|
||||||
|
from core.api_call_tracker import api_call_tracker
|
||||||
|
api_call_tracker.record_call('spotify', endpoint='get_album_tracks_page')
|
||||||
next_page = self.sp.next(next_page)
|
next_page = self.sp.next(next_page)
|
||||||
if next_page and 'items' in next_page:
|
if next_page and 'items' in next_page:
|
||||||
all_tracks.extend(next_page['items'])
|
all_tracks.extend(next_page['items'])
|
||||||
|
|
@ -1338,8 +1365,19 @@ class SpotifyClient:
|
||||||
albums.append(album)
|
albums.append(album)
|
||||||
raw_items.append(album_data)
|
raw_items.append(album_data)
|
||||||
|
|
||||||
# Get next batch if available
|
# Get next batch if available — throttle pagination to respect rate limits
|
||||||
results = self.sp.next(results) if results['next'] else None
|
if results['next']:
|
||||||
|
# Enforce same rate limit as decorated calls
|
||||||
|
with _api_call_lock:
|
||||||
|
elapsed = time.time() - _last_api_call_time
|
||||||
|
if elapsed < MIN_API_INTERVAL:
|
||||||
|
time.sleep(MIN_API_INTERVAL - elapsed)
|
||||||
|
globals()['_last_api_call_time'] = time.time()
|
||||||
|
from core.api_call_tracker import api_call_tracker
|
||||||
|
api_call_tracker.record_call('spotify', endpoint='get_artist_albums_page')
|
||||||
|
results = self.sp.next(results)
|
||||||
|
else:
|
||||||
|
results = None
|
||||||
|
|
||||||
logger.info(f"Retrieved {len(albums)} albums for artist {artist_id}")
|
logger.info(f"Retrieved {len(albums)} albums for artist {artist_id}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from utils.logging_config import get_logger
|
||||||
logger = get_logger("watchlist_scanner")
|
logger = get_logger("watchlist_scanner")
|
||||||
|
|
||||||
# Rate limiting constants for watchlist operations
|
# Rate limiting constants for watchlist operations
|
||||||
DELAY_BETWEEN_ARTISTS = 2.0 # 2 seconds between different artists
|
DELAY_BETWEEN_ARTISTS = 4.0 # 4 seconds between different artists (was 2s, increased to reduce Spotify rate limit risk)
|
||||||
DELAY_BETWEEN_ALBUMS = 0.5 # 500ms between albums for same artist
|
DELAY_BETWEEN_ALBUMS = 0.5 # 500ms between albums for same artist
|
||||||
DELAY_BETWEEN_API_BATCHES = 1.0 # 1 second between API batch operations
|
DELAY_BETWEEN_API_BATCHES = 1.0 # 1 second between API batch operations
|
||||||
|
|
||||||
|
|
@ -621,15 +621,20 @@ class WatchlistScanner:
|
||||||
|
|
||||||
scan_results = []
|
scan_results = []
|
||||||
for i, artist in enumerate(watchlist_artists):
|
for i, artist in enumerate(watchlist_artists):
|
||||||
|
# Abort scan if Spotify is rate limited — don't keep hammering
|
||||||
|
if self.spotify_client and hasattr(self.spotify_client, 'is_rate_limited') and self.spotify_client.is_rate_limited():
|
||||||
|
logger.warning(f"⚠️ Spotify rate limited — aborting watchlist scan after {i}/{len(watchlist_artists)} artists")
|
||||||
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.scan_artist(artist)
|
result = self.scan_artist(artist)
|
||||||
scan_results.append(result)
|
scan_results.append(result)
|
||||||
|
|
||||||
if result.success:
|
if result.success:
|
||||||
logger.info(f"✅ Scanned {artist.artist_name}: {result.new_tracks_found} new tracks found")
|
logger.info(f"✅ Scanned {artist.artist_name}: {result.new_tracks_found} new tracks found")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"❌ Failed to scan {artist.artist_name}: {result.error_message}")
|
logger.warning(f"❌ Failed to scan {artist.artist_name}: {result.error_message}")
|
||||||
|
|
||||||
# Rate limiting: Add delay between artists to avoid hitting Spotify API limits
|
# Rate limiting: Add delay between artists to avoid hitting Spotify API limits
|
||||||
# This is critical to prevent getting banned for 6+ hours
|
# This is critical to prevent getting banned for 6+ hours
|
||||||
if i < len(watchlist_artists) - 1: # Don't delay after the last artist
|
if i < len(watchlist_artists) - 1: # Don't delay after the last artist
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue