diff --git a/core/spotify_client.py b/core/spotify_client.py index 1141be95..df2a3272 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -13,7 +13,7 @@ logger = get_logger("spotify_client") # Global rate limiting variables _last_api_call_time = 0 _api_call_lock = threading.Lock() -MIN_API_INTERVAL = 0.2 # 200ms between API calls (more conservative to avoid bans) +MIN_API_INTERVAL = 0.35 # 350ms between API calls (~171/min, under Spotify's ~180/min limit) # Request queuing for burst handling import queue diff --git a/web_server.py b/web_server.py index 0d3a6d48..8977acba 100644 --- a/web_server.py +++ b/web_server.py @@ -20496,6 +20496,8 @@ def start_watchlist_scan(): # Start the scan in a background thread def run_scan(): + _enrichment_was_running = False + _itunes_enrichment_was_running = False try: global watchlist_scan_state, watchlist_auto_scanning, watchlist_auto_scanning_timestamp from core.watchlist_scanner import WatchlistScanner @@ -20561,6 +20563,16 @@ def start_watchlist_scan(): scan_results = [] + # Pause enrichment workers during scan to reduce API contention + if spotify_enrichment_worker and not spotify_enrichment_worker.paused: + spotify_enrichment_worker.pause() + _enrichment_was_running = True + print("⏸️ Paused Spotify enrichment worker during watchlist scan") + if itunes_enrichment_worker and not itunes_enrichment_worker.paused: + itunes_enrichment_worker.pause() + _itunes_enrichment_was_running = True + print("⏸️ Paused iTunes enrichment worker during watchlist scan") + # Dynamic delay calculation based on scan scope lookback_period = scanner._get_lookback_period_setting() is_full_discography = (lookback_period == 'all') @@ -20843,6 +20855,14 @@ def start_watchlist_scan(): watchlist_scan_state['error'] = str(e) finally: + # Resume enrichment workers if we paused them + if _enrichment_was_running and spotify_enrichment_worker: + spotify_enrichment_worker.resume() + print("▶️ Resumed Spotify enrichment worker after watchlist scan") + if _itunes_enrichment_was_running and itunes_enrichment_worker: + itunes_enrichment_worker.resume() + print("▶️ Resumed iTunes enrichment worker after watchlist scan") + # Always reset flag when scan completes (success or error) with watchlist_timer_lock: watchlist_auto_scanning = False @@ -21236,6 +21256,9 @@ def _process_watchlist_scan_automatically(): print("🤖 [Auto-Watchlist] Timer triggered - starting automatic watchlist scan...") + _enrichment_was_running = False + _itunes_enrichment_was_running = False + try: # CRITICAL FIX: Use smart stuck detection BEFORE acquiring lock # This prevents deadlock and handles stuck flags (2-hour timeout) @@ -21326,6 +21349,16 @@ def _process_watchlist_scan_automatically(): scan_results = [] + # Pause enrichment workers during scan to reduce API contention + if spotify_enrichment_worker and not spotify_enrichment_worker.paused: + spotify_enrichment_worker.pause() + _enrichment_was_running = True + print("⏸️ [Auto-Watchlist] Paused Spotify enrichment worker during scan") + if itunes_enrichment_worker and not itunes_enrichment_worker.paused: + itunes_enrichment_worker.pause() + _itunes_enrichment_was_running = True + print("⏸️ [Auto-Watchlist] Paused iTunes enrichment worker during scan") + # Dynamic delay calculation based on scan scope lookback_period = scanner._get_lookback_period_setting() is_full_discography = (lookback_period == 'all') @@ -21595,6 +21628,14 @@ def _process_watchlist_scan_automatically(): watchlist_scan_state['error'] = str(e) finally: + # Resume enrichment workers if we paused them + if _enrichment_was_running and spotify_enrichment_worker: + spotify_enrichment_worker.resume() + print("▶️ [Auto-Watchlist] Resumed Spotify enrichment worker after scan") + if _itunes_enrichment_was_running and itunes_enrichment_worker: + itunes_enrichment_worker.resume() + print("▶️ [Auto-Watchlist] Resumed iTunes enrichment worker after scan") + # Always reset flag and schedule next scan with watchlist_timer_lock: watchlist_auto_scanning = False diff --git a/webui/static/script.js b/webui/static/script.js index 9bf65bd1..f0e5cd98 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -36705,14 +36705,19 @@ async function updateSpotifyEnrichmentStatus() { const artistsComplete = artists.matched >= artists.total; const albumsComplete = albums.matched >= albums.total; - if (currentType === 'artist' || (!artistsComplete && !currentType)) { + // Prioritize currentType over completion-based inference + if (currentType === 'artist') { progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`; - } else if (currentType.includes('album') || (artistsComplete && !albumsComplete)) { + } else if (currentType.includes('album')) { progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`; - } else if (currentType.includes('track') || (artistsComplete && albumsComplete)) { + } else if (currentType.includes('track')) { progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`; - } else { + } else if (!artistsComplete) { progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`; + } else if (!albumsComplete) { + progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`; + } else { + progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`; } tooltipProgress.textContent = progressText; @@ -36828,14 +36833,19 @@ async function updateiTunesEnrichmentStatus() { const artistsComplete = artists.matched >= artists.total; const albumsComplete = albums.matched >= albums.total; - if (currentType === 'artist' || (!artistsComplete && !currentType)) { + // Prioritize currentType over completion-based inference + if (currentType === 'artist') { progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`; - } else if (currentType.includes('album') || (artistsComplete && !albumsComplete)) { + } else if (currentType.includes('album')) { progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`; - } else if (currentType.includes('track') || (artistsComplete && albumsComplete)) { + } else if (currentType.includes('track')) { progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`; - } else { + } else if (!artistsComplete) { progressText = `Artists: ${artists.matched || 0} / ${artists.total || 0} (${artists.percent || 0}%)`; + } else if (!albumsComplete) { + progressText = `Albums: ${albums.matched || 0} / ${albums.total || 0} (${albums.percent || 0}%)`; + } else { + progressText = `Tracks: ${tracks.matched || 0} / ${tracks.total || 0} (${tracks.percent || 0}%)`; } tooltipProgress.textContent = progressText;