Fix enrichment retry loops, cover art finding dupes, and Spotify rate limit during art scan
- All 9 enrichment workers: stop auto-retrying 'error' status items (was infinite loop) Only 'not_found' items retry after configured days; errors require manual full refresh - Cover art dedup: check both 'pending' AND 'resolved' findings to prevent recreation - Cover art scanner: top-level Spotify rate limit check skips Spotify entirely when banned, falls back to iTunes/Deezer only, logs once instead of spamming 429s
This commit is contained in:
parent
d8217d66ba
commit
429306c7f3
11 changed files with 93 additions and 121 deletions
|
|
@ -38,7 +38,6 @@ class AudioDBWorker:
|
|||
|
||||
# Retry configuration
|
||||
self.retry_days = 30
|
||||
self.error_retry_days = 7 # Retry 'error' items after 7 days
|
||||
|
||||
# Name matching threshold
|
||||
self.name_similarity_threshold = 0.80
|
||||
|
|
@ -197,46 +196,42 @@ class AudioDBWorker:
|
|||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_audiodb_id': row[3]}
|
||||
|
||||
# Priority 4: Retry 'not_found' or 'error' artists after retry_days/error_retry_days
|
||||
# Priority 4: Retry 'not_found' artists after retry_days
|
||||
not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
|
||||
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
|
||||
cursor.execute("""
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE (audiodb_match_status = 'not_found' AND audiodb_last_attempted < ?)
|
||||
OR (audiodb_match_status = 'error' AND audiodb_last_attempted < ?)
|
||||
WHERE audiodb_match_status = 'not_found' AND audiodb_last_attempted < ?
|
||||
ORDER BY audiodb_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
logger.info(f"Retrying artist '{row[1]}' (last attempted before cutoff)")
|
||||
return {'type': 'artist', 'id': row[0], 'name': row[1]}
|
||||
|
||||
# Priority 5: Retry 'not_found' or 'error' albums
|
||||
# Priority 5: Retry 'not_found' albums
|
||||
cursor.execute("""
|
||||
SELECT a.id, a.title, ar.name AS artist_name, ar.audiodb_id AS artist_audiodb_id
|
||||
FROM albums a
|
||||
JOIN artists ar ON a.artist_id = ar.id
|
||||
WHERE (a.audiodb_match_status = 'not_found' AND a.audiodb_last_attempted < ?)
|
||||
OR (a.audiodb_match_status = 'error' AND a.audiodb_last_attempted < ?)
|
||||
WHERE a.audiodb_match_status = 'not_found' AND a.audiodb_last_attempted < ?
|
||||
ORDER BY a.audiodb_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_audiodb_id': row[3]}
|
||||
|
||||
# Priority 6: Retry 'not_found' or 'error' tracks
|
||||
# Priority 6: Retry 'not_found' tracks
|
||||
cursor.execute("""
|
||||
SELECT t.id, t.title, ar.name AS artist_name, ar.audiodb_id AS artist_audiodb_id
|
||||
FROM tracks t
|
||||
JOIN artists ar ON t.artist_id = ar.id
|
||||
WHERE (t.audiodb_match_status = 'not_found' AND t.audiodb_last_attempted < ?)
|
||||
OR (t.audiodb_match_status = 'error' AND t.audiodb_last_attempted < ?)
|
||||
WHERE t.audiodb_match_status = 'not_found' AND t.audiodb_last_attempted < ?
|
||||
ORDER BY t.audiodb_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_audiodb_id': row[3]}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ class DeezerWorker:
|
|||
|
||||
# Retry configuration
|
||||
self.retry_days = 30
|
||||
self.error_retry_days = 7 # Retry 'error' items after 7 days
|
||||
|
||||
# Name matching threshold
|
||||
self.name_similarity_threshold = 0.80
|
||||
|
|
@ -197,46 +196,42 @@ class DeezerWorker:
|
|||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_deezer_id': row[3]}
|
||||
|
||||
# Priority 4: Retry 'not_found' or 'error' artists after retry_days/error_retry_days
|
||||
# Priority 4: Retry 'not_found' artists after retry_days
|
||||
not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
|
||||
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
|
||||
cursor.execute("""
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE (deezer_match_status = 'not_found' AND deezer_last_attempted < ?)
|
||||
OR (deezer_match_status = 'error' AND deezer_last_attempted < ?)
|
||||
WHERE deezer_match_status = 'not_found' AND deezer_last_attempted < ?
|
||||
ORDER BY deezer_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
logger.info(f"Retrying artist '{row[1]}' (last attempted before cutoff)")
|
||||
return {'type': 'artist', 'id': row[0], 'name': row[1]}
|
||||
|
||||
# Priority 5: Retry 'not_found' or 'error' albums
|
||||
# Priority 5: Retry 'not_found' albums
|
||||
cursor.execute("""
|
||||
SELECT a.id, a.title, ar.name AS artist_name, ar.deezer_id AS artist_deezer_id
|
||||
FROM albums a
|
||||
JOIN artists ar ON a.artist_id = ar.id
|
||||
WHERE (a.deezer_match_status = 'not_found' AND a.deezer_last_attempted < ?)
|
||||
OR (a.deezer_match_status = 'error' AND a.deezer_last_attempted < ?)
|
||||
WHERE a.deezer_match_status = 'not_found' AND a.deezer_last_attempted < ?
|
||||
ORDER BY a.deezer_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_deezer_id': row[3]}
|
||||
|
||||
# Priority 6: Retry 'not_found' or 'error' tracks
|
||||
# Priority 6: Retry 'not_found' tracks
|
||||
cursor.execute("""
|
||||
SELECT t.id, t.title, ar.name AS artist_name, ar.deezer_id AS artist_deezer_id
|
||||
FROM tracks t
|
||||
JOIN artists ar ON t.artist_id = ar.id
|
||||
WHERE (t.deezer_match_status = 'not_found' AND t.deezer_last_attempted < ?)
|
||||
OR (t.deezer_match_status = 'error' AND t.deezer_last_attempted < ?)
|
||||
WHERE t.deezer_match_status = 'not_found' AND t.deezer_last_attempted < ?
|
||||
ORDER BY t.deezer_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_deezer_id': row[3]}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ class GeniusWorker:
|
|||
|
||||
# Retry configuration
|
||||
self.retry_days = 30
|
||||
self.error_retry_days = 7
|
||||
|
||||
# Name matching threshold
|
||||
self.name_similarity_threshold = 0.75 # Slightly lower — Genius titles often include featured artists
|
||||
|
|
@ -200,32 +199,29 @@ class GeniusWorker:
|
|||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
||||
# Priority 3: Retry artists
|
||||
# Priority 3: Retry 'not_found' artists
|
||||
not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
|
||||
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
|
||||
cursor.execute("""
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE (genius_match_status = 'not_found' AND genius_last_attempted < ?)
|
||||
OR (genius_match_status = 'error' AND genius_last_attempted < ?)
|
||||
WHERE genius_match_status = 'not_found' AND genius_last_attempted < ?
|
||||
ORDER BY genius_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
logger.info(f"Retrying artist '{row[1]}' (last attempted before cutoff)")
|
||||
return {'type': 'artist', 'id': row[0], 'name': row[1]}
|
||||
|
||||
# Priority 4: Retry tracks
|
||||
# Priority 4: Retry 'not_found' tracks
|
||||
cursor.execute("""
|
||||
SELECT t.id, t.title, ar.name AS artist_name
|
||||
FROM tracks t
|
||||
JOIN artists ar ON t.artist_id = ar.id
|
||||
WHERE (t.genius_match_status = 'not_found' AND t.genius_last_attempted < ?)
|
||||
OR (t.genius_match_status = 'error' AND t.genius_last_attempted < ?)
|
||||
WHERE t.genius_match_status = 'not_found' AND t.genius_last_attempted < ?
|
||||
ORDER BY t.genius_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
|
|
|||
|
|
@ -252,18 +252,17 @@ class iTunesWorker:
|
|||
if row:
|
||||
return {'type': 'track_individual', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
||||
# Priority 6: Retry stale failures
|
||||
# Priority 6: Retry stale not_found items only (errors don't auto-retry —
|
||||
# they require a user-triggered full refresh to prevent infinite retry loops)
|
||||
not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
|
||||
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE (itunes_match_status = 'not_found' AND itunes_last_attempted < ?)
|
||||
OR (itunes_match_status = 'error' AND itunes_last_attempted < ?)
|
||||
WHERE itunes_match_status = 'not_found' AND itunes_last_attempted < ?
|
||||
ORDER BY itunes_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'artist', 'id': row[0], 'name': row[1]}
|
||||
|
|
@ -272,11 +271,10 @@ class iTunesWorker:
|
|||
SELECT a.id, a.title, ar.name AS artist_name
|
||||
FROM albums a
|
||||
JOIN artists ar ON a.artist_id = ar.id
|
||||
WHERE (a.itunes_match_status = 'not_found' AND a.itunes_last_attempted < ?)
|
||||
OR (a.itunes_match_status = 'error' AND a.itunes_last_attempted < ?)
|
||||
WHERE a.itunes_match_status = 'not_found' AND a.itunes_last_attempted < ?
|
||||
ORDER BY a.itunes_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'album_individual', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
|
@ -285,11 +283,10 @@ class iTunesWorker:
|
|||
SELECT t.id, t.title, ar.name AS artist_name
|
||||
FROM tracks t
|
||||
JOIN artists ar ON t.artist_id = ar.id
|
||||
WHERE (t.itunes_match_status = 'not_found' AND t.itunes_last_attempted < ?)
|
||||
OR (t.itunes_match_status = 'error' AND t.itunes_last_attempted < ?)
|
||||
WHERE t.itunes_match_status = 'not_found' AND t.itunes_last_attempted < ?
|
||||
ORDER BY t.itunes_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'track_individual', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
|
|
|||
|
|
@ -211,46 +211,43 @@ class LastFMWorker:
|
|||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
||||
# Priority 4: Retry 'not_found' or 'error' artists
|
||||
# Priority 4: Retry 'not_found' artists only (errors don't auto-retry —
|
||||
# they require a user-triggered full refresh to prevent infinite retry loops)
|
||||
not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
|
||||
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
|
||||
cursor.execute("""
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE (lastfm_match_status = 'not_found' AND lastfm_last_attempted < ?)
|
||||
OR (lastfm_match_status = 'error' AND lastfm_last_attempted < ?)
|
||||
WHERE lastfm_match_status = 'not_found' AND lastfm_last_attempted < ?
|
||||
ORDER BY lastfm_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
logger.info(f"Retrying artist '{row[1]}' (last attempted before cutoff)")
|
||||
return {'type': 'artist', 'id': row[0], 'name': row[1]}
|
||||
|
||||
# Priority 5: Retry albums
|
||||
# Priority 5: Retry not_found albums
|
||||
cursor.execute("""
|
||||
SELECT a.id, a.title, ar.name AS artist_name
|
||||
FROM albums a
|
||||
JOIN artists ar ON a.artist_id = ar.id
|
||||
WHERE (a.lastfm_match_status = 'not_found' AND a.lastfm_last_attempted < ?)
|
||||
OR (a.lastfm_match_status = 'error' AND a.lastfm_last_attempted < ?)
|
||||
WHERE a.lastfm_match_status = 'not_found' AND a.lastfm_last_attempted < ?
|
||||
ORDER BY a.lastfm_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
||||
# Priority 6: Retry tracks
|
||||
# Priority 6: Retry not_found tracks
|
||||
cursor.execute("""
|
||||
SELECT t.id, t.title, ar.name AS artist_name
|
||||
FROM tracks t
|
||||
JOIN artists ar ON t.artist_id = ar.id
|
||||
WHERE (t.lastfm_match_status = 'not_found' AND t.lastfm_last_attempted < ?)
|
||||
OR (t.lastfm_match_status = 'error' AND t.lastfm_last_attempted < ?)
|
||||
WHERE t.lastfm_match_status = 'not_found' AND t.lastfm_last_attempted < ?
|
||||
ORDER BY t.lastfm_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ class MusicBrainzWorker:
|
|||
|
||||
# Retry configuration
|
||||
self.retry_days = 30 # Retry 'not_found' items after 30 days
|
||||
self.error_retry_days = 7 # Retry 'error' items after 7 days
|
||||
|
||||
logger.info("MusicBrainz background worker initialized")
|
||||
|
||||
|
|
@ -201,46 +200,42 @@ class MusicBrainzWorker:
|
|||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
||||
# Priority 4: Retry 'not_found' or 'error' artists after retry_days/error_retry_days
|
||||
# Priority 4: Retry 'not_found' artists after retry_days
|
||||
not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
|
||||
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
|
||||
cursor.execute("""
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE (musicbrainz_match_status = 'not_found' AND musicbrainz_last_attempted < ?)
|
||||
OR (musicbrainz_match_status = 'error' AND musicbrainz_last_attempted < ?)
|
||||
WHERE musicbrainz_match_status = 'not_found' AND musicbrainz_last_attempted < ?
|
||||
ORDER BY musicbrainz_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
logger.info(f"Retrying artist '{row[1]}' (last attempted before cutoff)")
|
||||
return {'type': 'artist', 'id': row[0], 'name': row[1]}
|
||||
|
||||
# Priority 5: Retry 'not_found' or 'error' albums
|
||||
# Priority 5: Retry 'not_found' albums
|
||||
cursor.execute("""
|
||||
SELECT a.id, a.title, ar.name AS artist_name
|
||||
FROM albums a
|
||||
JOIN artists ar ON a.artist_id = ar.id
|
||||
WHERE (a.musicbrainz_match_status = 'not_found' AND a.musicbrainz_last_attempted < ?)
|
||||
OR (a.musicbrainz_match_status = 'error' AND a.musicbrainz_last_attempted < ?)
|
||||
WHERE a.musicbrainz_match_status = 'not_found' AND a.musicbrainz_last_attempted < ?
|
||||
ORDER BY a.musicbrainz_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
||||
# Priority 6: Retry 'not_found' or 'error' tracks
|
||||
# Priority 6: Retry 'not_found' tracks
|
||||
cursor.execute("""
|
||||
SELECT t.id, t.title, ar.name AS artist_name
|
||||
FROM tracks t
|
||||
JOIN artists ar ON t.artist_id = ar.id
|
||||
WHERE (t.musicbrainz_match_status = 'not_found' AND t.musicbrainz_last_attempted < ?)
|
||||
OR (t.musicbrainz_match_status = 'error' AND t.musicbrainz_last_attempted < ?)
|
||||
WHERE t.musicbrainz_match_status = 'not_found' AND t.musicbrainz_last_attempted < ?
|
||||
ORDER BY t.musicbrainz_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ class QobuzWorker:
|
|||
|
||||
# Retry configuration
|
||||
self.retry_days = 30
|
||||
self.error_retry_days = 7
|
||||
|
||||
# Name matching threshold
|
||||
self.name_similarity_threshold = 0.80
|
||||
|
|
@ -220,46 +219,42 @@ class QobuzWorker:
|
|||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_qobuz_id': row[3]}
|
||||
|
||||
# Priority 4: Retry 'not_found' or 'error' artists
|
||||
# Priority 4: Retry 'not_found' artists
|
||||
not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
|
||||
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
|
||||
cursor.execute("""
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE (qobuz_match_status = 'not_found' AND qobuz_last_attempted < ?)
|
||||
OR (qobuz_match_status = 'error' AND qobuz_last_attempted < ?)
|
||||
WHERE qobuz_match_status = 'not_found' AND qobuz_last_attempted < ?
|
||||
ORDER BY qobuz_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
logger.info(f"Retrying artist '{row[1]}' (last attempted before cutoff)")
|
||||
return {'type': 'artist', 'id': row[0], 'name': row[1]}
|
||||
|
||||
# Priority 5: Retry 'not_found' or 'error' albums
|
||||
# Priority 5: Retry 'not_found' albums
|
||||
cursor.execute("""
|
||||
SELECT a.id, a.title, ar.name AS artist_name, ar.qobuz_id AS artist_qobuz_id
|
||||
FROM albums a
|
||||
JOIN artists ar ON a.artist_id = ar.id
|
||||
WHERE (a.qobuz_match_status = 'not_found' AND a.qobuz_last_attempted < ?)
|
||||
OR (a.qobuz_match_status = 'error' AND a.qobuz_last_attempted < ?)
|
||||
WHERE a.qobuz_match_status = 'not_found' AND a.qobuz_last_attempted < ?
|
||||
ORDER BY a.qobuz_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_qobuz_id': row[3]}
|
||||
|
||||
# Priority 6: Retry 'not_found' or 'error' tracks
|
||||
# Priority 6: Retry 'not_found' tracks
|
||||
cursor.execute("""
|
||||
SELECT t.id, t.title, ar.name AS artist_name, ar.qobuz_id AS artist_qobuz_id
|
||||
FROM tracks t
|
||||
JOIN artists ar ON t.artist_id = ar.id
|
||||
WHERE (t.qobuz_match_status = 'not_found' AND t.qobuz_last_attempted < ?)
|
||||
OR (t.qobuz_match_status = 'error' AND t.qobuz_last_attempted < ?)
|
||||
WHERE t.qobuz_match_status = 'not_found' AND t.qobuz_last_attempted < ?
|
||||
ORDER BY t.qobuz_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_qobuz_id': row[3]}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ class MissingCoverArtJob(RepairJob):
|
|||
if context.report_progress:
|
||||
context.report_progress(phase=f'Searching artwork for {total} albums...', total=total)
|
||||
|
||||
spotify_skipped = False # Track if we've logged the rate limit skip
|
||||
|
||||
for i, row in enumerate(albums):
|
||||
if context.check_stop():
|
||||
return result
|
||||
|
|
@ -86,14 +88,23 @@ class MissingCoverArtJob(RepairJob):
|
|||
|
||||
artwork_url = None
|
||||
|
||||
# Check Spotify rate limit at the top level — skip Spotify entirely if banned
|
||||
spotify_available = not context.is_spotify_rate_limited()
|
||||
if not spotify_available and not spotify_skipped:
|
||||
logger.info("Spotify rate limited — skipping Spotify artwork lookups, using fallback only")
|
||||
spotify_skipped = True
|
||||
|
||||
# Try to find artwork URL from APIs
|
||||
if prefer_source == 'spotify':
|
||||
if prefer_source == 'spotify' and spotify_available:
|
||||
artwork_url = self._try_spotify(spotify_album_id, title, artist_name, context)
|
||||
if not artwork_url:
|
||||
artwork_url = self._try_itunes(title, artist_name, context)
|
||||
elif prefer_source == 'spotify' and not spotify_available:
|
||||
# Spotify preferred but rate limited — use fallback only
|
||||
artwork_url = self._try_itunes(title, artist_name, context)
|
||||
else:
|
||||
artwork_url = self._try_itunes(title, artist_name, context)
|
||||
if not artwork_url:
|
||||
if not artwork_url and spotify_available:
|
||||
artwork_url = self._try_spotify(spotify_album_id, title, artist_name, context)
|
||||
|
||||
if artwork_url:
|
||||
|
|
|
|||
|
|
@ -632,16 +632,17 @@ class RepairWorker:
|
|||
conn = self.db._get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Dedup check: skip if same finding already pending
|
||||
# Dedup check: skip if same finding already exists (pending OR recently resolved)
|
||||
cursor.execute("""
|
||||
SELECT id FROM repair_findings
|
||||
WHERE job_id = ? AND finding_type = ? AND status = 'pending'
|
||||
WHERE job_id = ? AND finding_type = ?
|
||||
AND status IN ('pending', 'resolved')
|
||||
AND ((entity_type = ? AND entity_id = ?) OR (file_path = ? AND file_path IS NOT NULL))
|
||||
LIMIT 1
|
||||
""", (job_id, finding_type, entity_type, entity_id, file_path))
|
||||
|
||||
if cursor.fetchone():
|
||||
return # Already exists
|
||||
return # Already exists or was already fixed
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO repair_findings
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ class SpotifyWorker:
|
|||
|
||||
# Retry configuration
|
||||
self.retry_days = 30
|
||||
self.error_retry_days = 7
|
||||
|
||||
# Name matching threshold
|
||||
self.name_similarity_threshold = 0.80
|
||||
|
|
@ -287,18 +286,16 @@ class SpotifyWorker:
|
|||
if row:
|
||||
return {'type': 'track_individual', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
||||
# Priority 6: Retry stale failures
|
||||
# Priority 6: Retry stale 'not_found' failures
|
||||
not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
|
||||
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE (spotify_match_status = 'not_found' AND spotify_last_attempted < ?)
|
||||
OR (spotify_match_status = 'error' AND spotify_last_attempted < ?)
|
||||
WHERE spotify_match_status = 'not_found' AND spotify_last_attempted < ?
|
||||
ORDER BY spotify_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'artist', 'id': row[0], 'name': row[1]}
|
||||
|
|
@ -307,11 +304,10 @@ class SpotifyWorker:
|
|||
SELECT a.id, a.title, ar.name AS artist_name
|
||||
FROM albums a
|
||||
JOIN artists ar ON a.artist_id = ar.id
|
||||
WHERE (a.spotify_match_status = 'not_found' AND a.spotify_last_attempted < ?)
|
||||
OR (a.spotify_match_status = 'error' AND a.spotify_last_attempted < ?)
|
||||
WHERE a.spotify_match_status = 'not_found' AND a.spotify_last_attempted < ?
|
||||
ORDER BY a.spotify_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'album_individual', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
|
@ -320,11 +316,10 @@ class SpotifyWorker:
|
|||
SELECT t.id, t.title, ar.name AS artist_name
|
||||
FROM tracks t
|
||||
JOIN artists ar ON t.artist_id = ar.id
|
||||
WHERE (t.spotify_match_status = 'not_found' AND t.spotify_last_attempted < ?)
|
||||
OR (t.spotify_match_status = 'error' AND t.spotify_last_attempted < ?)
|
||||
WHERE t.spotify_match_status = 'not_found' AND t.spotify_last_attempted < ?
|
||||
ORDER BY t.spotify_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'track_individual', 'id': row[0], 'name': row[1], 'artist': row[2]}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ class TidalWorker:
|
|||
|
||||
# Retry configuration
|
||||
self.retry_days = 30
|
||||
self.error_retry_days = 7
|
||||
|
||||
# Name matching threshold
|
||||
self.name_similarity_threshold = 0.80
|
||||
|
|
@ -232,46 +231,42 @@ class TidalWorker:
|
|||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_tidal_id': row[3]}
|
||||
|
||||
# Priority 4: Retry 'not_found' or 'error' artists
|
||||
# Priority 4: Retry 'not_found' artists
|
||||
not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
|
||||
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
|
||||
cursor.execute("""
|
||||
SELECT id, name
|
||||
FROM artists
|
||||
WHERE (tidal_match_status = 'not_found' AND tidal_last_attempted < ?)
|
||||
OR (tidal_match_status = 'error' AND tidal_last_attempted < ?)
|
||||
WHERE tidal_match_status = 'not_found' AND tidal_last_attempted < ?
|
||||
ORDER BY tidal_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
logger.info(f"Retrying artist '{row[1]}' (last attempted before cutoff)")
|
||||
return {'type': 'artist', 'id': row[0], 'name': row[1]}
|
||||
|
||||
# Priority 5: Retry 'not_found' or 'error' albums
|
||||
# Priority 5: Retry 'not_found' albums
|
||||
cursor.execute("""
|
||||
SELECT a.id, a.title, ar.name AS artist_name, ar.tidal_id AS artist_tidal_id
|
||||
FROM albums a
|
||||
JOIN artists ar ON a.artist_id = ar.id
|
||||
WHERE (a.tidal_match_status = 'not_found' AND a.tidal_last_attempted < ?)
|
||||
OR (a.tidal_match_status = 'error' AND a.tidal_last_attempted < ?)
|
||||
WHERE a.tidal_match_status = 'not_found' AND a.tidal_last_attempted < ?
|
||||
ORDER BY a.tidal_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_tidal_id': row[3]}
|
||||
|
||||
# Priority 6: Retry 'not_found' or 'error' tracks
|
||||
# Priority 6: Retry 'not_found' tracks
|
||||
cursor.execute("""
|
||||
SELECT t.id, t.title, ar.name AS artist_name, ar.tidal_id AS artist_tidal_id
|
||||
FROM tracks t
|
||||
JOIN artists ar ON t.artist_id = ar.id
|
||||
WHERE (t.tidal_match_status = 'not_found' AND t.tidal_last_attempted < ?)
|
||||
OR (t.tidal_match_status = 'error' AND t.tidal_last_attempted < ?)
|
||||
WHERE t.tidal_match_status = 'not_found' AND t.tidal_last_attempted < ?
|
||||
ORDER BY t.tidal_last_attempted ASC
|
||||
LIMIT 1
|
||||
""", (not_found_cutoff, error_cutoff))
|
||||
""", (not_found_cutoff,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_tidal_id': row[3]}
|
||||
|
|
|
|||
Loading…
Reference in a new issue