Raise Spotify API interval; pause enrichments

Increase Spotify client rate limit and reduce API contention during watchlist scans. Changes:

- core/spotify_client.py: Bumped MIN_API_INTERVAL from 0.2s to 0.35s (~171 calls/min) to stay safely under Spotify's ~180/min limit.
- web_server.py: In start_watchlist_scan and automatic scan flow, pause spotify_enrichment_worker and itunes_enrichment_worker before scanning (tracking with _enrichment_was_running/_itunes_enrichment_was_running) and resume them in finally blocks; added console prints for pause/resume. This prevents enrichment workers from contending for API quota during long scans.
- webui/static/script.js: Improved enrichment status tooltip logic to prioritize explicit currentType and then fall back to completion-based inference with explicit branches for artists, albums, and tracks for clearer progress text.

These changes aim to avoid API rate violations and make scan progress display more predictable.
This commit is contained in:
Broque Thomas 2026-02-23 21:22:27 -08:00
parent 317d5c1770
commit 1c34967fd3
3 changed files with 60 additions and 9 deletions

View file

@ -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

View file

@ -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

View file

@ -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;