Retry errored items and prevent incomplete Deezer matches

This commit is contained in:
Broque Thomas 2026-02-18 20:04:58 -08:00
parent a7cc558fb3
commit e7e939bdd5
3 changed files with 76 additions and 44 deletions

View file

@ -38,6 +38,7 @@ class AudioDBWorker:
# Retry configuration # Retry configuration
self.retry_days = 30 self.retry_days = 30
self.error_retry_days = 7 # Retry 'error' items after 7 days
# Name matching threshold # Name matching threshold
self.name_similarity_threshold = 0.80 self.name_similarity_threshold = 0.80
@ -184,45 +185,46 @@ class AudioDBWorker:
if row: if row:
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_audiodb_id': row[3]} return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_audiodb_id': row[3]}
# Priority 4: Retry 'not_found' artists after retry_days # Priority 4: Retry 'not_found' or 'error' artists after retry_days/error_retry_days
cutoff_date = datetime.now() - timedelta(days=self.retry_days) not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
cursor.execute(""" cursor.execute("""
SELECT id, name SELECT id, name
FROM artists FROM artists
WHERE audiodb_match_status = 'not_found' WHERE (audiodb_match_status = 'not_found' AND audiodb_last_attempted < ?)
AND audiodb_last_attempted < ? OR (audiodb_match_status = 'error' AND audiodb_last_attempted < ?)
ORDER BY audiodb_last_attempted ASC ORDER BY audiodb_last_attempted ASC
LIMIT 1 LIMIT 1
""", (cutoff_date,)) """, (not_found_cutoff, error_cutoff))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
logger.info(f"Retrying artist '{row[1]}' (last attempted before {cutoff_date})") logger.info(f"Retrying artist '{row[1]}' (last attempted before cutoff)")
return {'type': 'artist', 'id': row[0], 'name': row[1]} return {'type': 'artist', 'id': row[0], 'name': row[1]}
# Priority 5: Retry 'not_found' albums # Priority 5: Retry 'not_found' or 'error' albums
cursor.execute(""" cursor.execute("""
SELECT a.id, a.title, ar.name AS artist_name, ar.audiodb_id AS artist_audiodb_id SELECT a.id, a.title, ar.name AS artist_name, ar.audiodb_id AS artist_audiodb_id
FROM albums a FROM albums a
JOIN artists ar ON a.artist_id = ar.id JOIN artists ar ON a.artist_id = ar.id
WHERE a.audiodb_match_status = 'not_found' WHERE (a.audiodb_match_status = 'not_found' AND a.audiodb_last_attempted < ?)
AND a.audiodb_last_attempted < ? OR (a.audiodb_match_status = 'error' AND a.audiodb_last_attempted < ?)
ORDER BY a.audiodb_last_attempted ASC ORDER BY a.audiodb_last_attempted ASC
LIMIT 1 LIMIT 1
""", (cutoff_date,)) """, (not_found_cutoff, error_cutoff))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_audiodb_id': row[3]} return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_audiodb_id': row[3]}
# Priority 6: Retry 'not_found' tracks # Priority 6: Retry 'not_found' or 'error' tracks
cursor.execute(""" cursor.execute("""
SELECT t.id, t.title, ar.name AS artist_name, ar.audiodb_id AS artist_audiodb_id SELECT t.id, t.title, ar.name AS artist_name, ar.audiodb_id AS artist_audiodb_id
FROM tracks t FROM tracks t
JOIN artists ar ON t.artist_id = ar.id JOIN artists ar ON t.artist_id = ar.id
WHERE t.audiodb_match_status = 'not_found' WHERE (t.audiodb_match_status = 'not_found' AND t.audiodb_last_attempted < ?)
AND t.audiodb_last_attempted < ? OR (t.audiodb_match_status = 'error' AND t.audiodb_last_attempted < ?)
ORDER BY t.audiodb_last_attempted ASC ORDER BY t.audiodb_last_attempted ASC
LIMIT 1 LIMIT 1
""", (cutoff_date,)) """, (not_found_cutoff, error_cutoff))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_audiodb_id': row[3]} return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_audiodb_id': row[3]}

View file

@ -38,6 +38,7 @@ class DeezerWorker:
# Retry configuration # Retry configuration
self.retry_days = 30 self.retry_days = 30
self.error_retry_days = 7 # Retry 'error' items after 7 days
# Name matching threshold # Name matching threshold
self.name_similarity_threshold = 0.80 self.name_similarity_threshold = 0.80
@ -184,45 +185,46 @@ class DeezerWorker:
if row: if row:
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_deezer_id': row[3]} return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_deezer_id': row[3]}
# Priority 4: Retry 'not_found' artists after retry_days # Priority 4: Retry 'not_found' or 'error' artists after retry_days/error_retry_days
cutoff_date = datetime.now() - timedelta(days=self.retry_days) not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
cursor.execute(""" cursor.execute("""
SELECT id, name SELECT id, name
FROM artists FROM artists
WHERE deezer_match_status = 'not_found' WHERE (deezer_match_status = 'not_found' AND deezer_last_attempted < ?)
AND deezer_last_attempted < ? OR (deezer_match_status = 'error' AND deezer_last_attempted < ?)
ORDER BY deezer_last_attempted ASC ORDER BY deezer_last_attempted ASC
LIMIT 1 LIMIT 1
""", (cutoff_date,)) """, (not_found_cutoff, error_cutoff))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
logger.info(f"Retrying artist '{row[1]}' (last attempted before {cutoff_date})") logger.info(f"Retrying artist '{row[1]}' (last attempted before cutoff)")
return {'type': 'artist', 'id': row[0], 'name': row[1]} return {'type': 'artist', 'id': row[0], 'name': row[1]}
# Priority 5: Retry 'not_found' albums # Priority 5: Retry 'not_found' or 'error' albums
cursor.execute(""" cursor.execute("""
SELECT a.id, a.title, ar.name AS artist_name, ar.deezer_id AS artist_deezer_id SELECT a.id, a.title, ar.name AS artist_name, ar.deezer_id AS artist_deezer_id
FROM albums a FROM albums a
JOIN artists ar ON a.artist_id = ar.id JOIN artists ar ON a.artist_id = ar.id
WHERE a.deezer_match_status = 'not_found' WHERE (a.deezer_match_status = 'not_found' AND a.deezer_last_attempted < ?)
AND a.deezer_last_attempted < ? OR (a.deezer_match_status = 'error' AND a.deezer_last_attempted < ?)
ORDER BY a.deezer_last_attempted ASC ORDER BY a.deezer_last_attempted ASC
LIMIT 1 LIMIT 1
""", (cutoff_date,)) """, (not_found_cutoff, error_cutoff))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_deezer_id': row[3]} return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_deezer_id': row[3]}
# Priority 6: Retry 'not_found' tracks # Priority 6: Retry 'not_found' or 'error' tracks
cursor.execute(""" cursor.execute("""
SELECT t.id, t.title, ar.name AS artist_name, ar.deezer_id AS artist_deezer_id SELECT t.id, t.title, ar.name AS artist_name, ar.deezer_id AS artist_deezer_id
FROM tracks t FROM tracks t
JOIN artists ar ON t.artist_id = ar.id JOIN artists ar ON t.artist_id = ar.id
WHERE t.deezer_match_status = 'not_found' WHERE (t.deezer_match_status = 'not_found' AND t.deezer_last_attempted < ?)
AND t.deezer_last_attempted < ? OR (t.deezer_match_status = 'error' AND t.deezer_last_attempted < ?)
ORDER BY t.deezer_last_attempted ASC ORDER BY t.deezer_last_attempted ASC
LIMIT 1 LIMIT 1
""", (cutoff_date,)) """, (not_found_cutoff, error_cutoff))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_deezer_id': row[3]} return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2], 'artist_deezer_id': row[3]}
@ -358,7 +360,20 @@ class DeezerWorker:
# Fetch full album details for label, genres, explicit # Fetch full album details for label, genres, explicit
deezer_album_id = result.get('id') deezer_album_id = result.get('id')
full_album = self.client.get_album(deezer_album_id) if deezer_album_id else None full_album = None
if deezer_album_id:
try:
full_album = self.client.get_album(deezer_album_id)
except Exception as e:
logger.warning(f"Failed to fetch full album details for '{album_name}' (Deezer ID: {deezer_album_id}): {e}")
if full_album is None:
# Full details fetch failed — mark as error so it retries later
# rather than storing a match without label/genres/explicit
self._mark_status('album', album_id, 'error')
self.stats['errors'] += 1
logger.warning(f"Album '{album_name}' matched but full details unavailable, will retry")
return
self._update_album(album_id, result, full_album) self._update_album(album_id, result, full_album)
self.stats['matched'] += 1 self.stats['matched'] += 1
@ -385,7 +400,20 @@ class DeezerWorker:
# Fetch full track details for BPM # Fetch full track details for BPM
deezer_track_id = result.get('id') deezer_track_id = result.get('id')
full_track = self.client.get_track(deezer_track_id) if deezer_track_id else None full_track = None
if deezer_track_id:
try:
full_track = self.client.get_track(deezer_track_id)
except Exception as e:
logger.warning(f"Failed to fetch full track details for '{track_name}' (Deezer ID: {deezer_track_id}): {e}")
if full_track is None:
# Full details fetch failed — mark as error so it retries later
# rather than storing a match without BPM/explicit
self._mark_status('track', track_id, 'error')
self.stats['errors'] += 1
logger.warning(f"Track '{track_name}' matched but full details unavailable, will retry")
return
self._update_track(track_id, result, full_track) self._update_track(track_id, result, full_track)
self.stats['matched'] += 1 self.stats['matched'] += 1

View file

@ -34,6 +34,7 @@ class MusicBrainzWorker:
# Retry configuration # Retry configuration
self.retry_days = 30 # Retry 'not_found' items after 30 days 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") logger.info("MusicBrainz background worker initialized")
@ -188,45 +189,46 @@ class MusicBrainzWorker:
if row: if row:
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2]} return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2]}
# Priority 4: Retry 'not_found' artists after retry_days # Priority 4: Retry 'not_found' or 'error' artists after retry_days/error_retry_days
cutoff_date = datetime.now() - timedelta(days=self.retry_days) not_found_cutoff = datetime.now() - timedelta(days=self.retry_days)
error_cutoff = datetime.now() - timedelta(days=self.error_retry_days)
cursor.execute(""" cursor.execute("""
SELECT id, name SELECT id, name
FROM artists FROM artists
WHERE musicbrainz_match_status = 'not_found' WHERE (musicbrainz_match_status = 'not_found' AND musicbrainz_last_attempted < ?)
AND musicbrainz_last_attempted < ? OR (musicbrainz_match_status = 'error' AND musicbrainz_last_attempted < ?)
ORDER BY musicbrainz_last_attempted ASC ORDER BY musicbrainz_last_attempted ASC
LIMIT 1 LIMIT 1
""", (cutoff_date,)) """, (not_found_cutoff, error_cutoff))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
logger.info(f"Retrying artist '{row[1]}' (last attempted: {cutoff_date})") logger.info(f"Retrying artist '{row[1]}' (last attempted before cutoff)")
return {'type': 'artist', 'id': row[0], 'name': row[1]} return {'type': 'artist', 'id': row[0], 'name': row[1]}
# Priority 5: Retry 'not_found' albums # Priority 5: Retry 'not_found' or 'error' albums
cursor.execute(""" cursor.execute("""
SELECT a.id, a.title, ar.name AS artist_name SELECT a.id, a.title, ar.name AS artist_name
FROM albums a FROM albums a
JOIN artists ar ON a.artist_id = ar.id JOIN artists ar ON a.artist_id = ar.id
WHERE a.musicbrainz_match_status = 'not_found' WHERE (a.musicbrainz_match_status = 'not_found' AND a.musicbrainz_last_attempted < ?)
AND a.musicbrainz_last_attempted < ? OR (a.musicbrainz_match_status = 'error' AND a.musicbrainz_last_attempted < ?)
ORDER BY a.musicbrainz_last_attempted ASC ORDER BY a.musicbrainz_last_attempted ASC
LIMIT 1 LIMIT 1
""", (cutoff_date,)) """, (not_found_cutoff, error_cutoff))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2]} return {'type': 'album', 'id': row[0], 'name': row[1], 'artist': row[2]}
# Priority 6: Retry 'not_found' tracks # Priority 6: Retry 'not_found' or 'error' tracks
cursor.execute(""" cursor.execute("""
SELECT t.id, t.title, ar.name AS artist_name SELECT t.id, t.title, ar.name AS artist_name
FROM tracks t FROM tracks t
JOIN artists ar ON t.artist_id = ar.id JOIN artists ar ON t.artist_id = ar.id
WHERE t.musicbrainz_match_status = 'not_found' WHERE (t.musicbrainz_match_status = 'not_found' AND t.musicbrainz_last_attempted < ?)
AND t.musicbrainz_last_attempted < ? OR (t.musicbrainz_match_status = 'error' AND t.musicbrainz_last_attempted < ?)
ORDER BY t.musicbrainz_last_attempted ASC ORDER BY t.musicbrainz_last_attempted ASC
LIMIT 1 LIMIT 1
""", (cutoff_date,)) """, (not_found_cutoff, error_cutoff))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2]} return {'type': 'track', 'id': row[0], 'name': row[1], 'artist': row[2]}