Fix enrichment workers looping on tracks with NULL IDs

Workers would endlessly match the same track because UPDATE WHERE id =
NULL matches 0 rows in SQL. Added AND id IS NOT NULL to all enrichment
queries (individual, batch EXISTS, and batch fetch) across all 9
workers. Also added process-level guard for belt-and-suspenders safety.

Fix Deezer get_track → get_track_details method name mismatch.
This commit is contained in:
Broque Thomas 2026-03-20 09:36:25 -07:00
parent 81852caa59
commit e0533215da
10 changed files with 141 additions and 33 deletions

View file

@ -129,6 +129,18 @@ class AudioDBWorker:
continue
self.current_item = item
# Guard: skip items with None/NULL IDs to prevent infinite enrichment loops
item_id = item.get('id') or item.get('artist_id') or item.get('album_id')
if item_id is None:
logger.warning(f"Skipping {item.get('type', 'unknown')} with NULL id: {item.get('name', '?')} — marking as error")
try:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
continue
self._process_item(item)
@ -151,7 +163,7 @@ class AudioDBWorker:
cursor.execute("""
SELECT id, name
FROM artists
WHERE audiodb_match_status IS NULL
WHERE audiodb_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
LIMIT 1
""")
@ -164,7 +176,7 @@ class AudioDBWorker:
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 IS NULL
WHERE a.audiodb_match_status IS NULL AND a.id IS NOT NULL
ORDER BY a.id ASC
LIMIT 1
""")
@ -177,7 +189,7 @@ class AudioDBWorker:
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 IS NULL
WHERE t.audiodb_match_status IS NULL AND t.id IS NOT NULL
ORDER BY t.id ASC
LIMIT 1
""")

View file

@ -129,6 +129,18 @@ class DeezerWorker:
continue
self.current_item = item
# Guard: skip items with None/NULL IDs to prevent infinite enrichment loops
item_id = item.get('id') or item.get('artist_id') or item.get('album_id')
if item_id is None:
logger.warning(f"Skipping {item.get('type', 'unknown')} with NULL id: {item.get('name', '?')} — marking as error")
try:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
continue
self._process_item(item)
@ -151,7 +163,7 @@ class DeezerWorker:
cursor.execute("""
SELECT id, name
FROM artists
WHERE deezer_match_status IS NULL
WHERE deezer_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
LIMIT 1
""")
@ -164,7 +176,7 @@ class DeezerWorker:
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 IS NULL
WHERE a.deezer_match_status IS NULL AND a.id IS NOT NULL
ORDER BY a.id ASC
LIMIT 1
""")
@ -177,7 +189,7 @@ class DeezerWorker:
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 IS NULL
WHERE t.deezer_match_status IS NULL AND t.id IS NOT NULL
ORDER BY t.id ASC
LIMIT 1
""")

View file

@ -143,6 +143,18 @@ class GeniusWorker:
continue
self.current_item = item
# Guard: skip items with None/NULL IDs to prevent infinite enrichment loops
item_id = item.get('id') or item.get('artist_id') or item.get('album_id')
if item_id is None:
logger.warning(f"Skipping {item.get('type', 'unknown')} with NULL id: {item.get('name', '?')} — marking as error")
try:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
continue
self._process_item(item)
# Genius rate limiting is conservative (500ms per call) + lyrics scraping
@ -167,7 +179,7 @@ class GeniusWorker:
cursor.execute("""
SELECT id, name
FROM artists
WHERE genius_match_status IS NULL
WHERE genius_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
LIMIT 1
""")
@ -180,7 +192,7 @@ class GeniusWorker:
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 IS NULL
WHERE t.genius_match_status IS NULL AND t.id IS NOT NULL
ORDER BY t.id ASC
LIMIT 1
""")

View file

@ -130,6 +130,18 @@ class iTunesWorker:
continue
self.current_item = item
# Guard: skip items with None/NULL IDs to prevent infinite enrichment loops
item_id = item.get('id') or item.get('artist_id') or item.get('album_id')
if item_id is None:
logger.warning(f"Skipping {item.get('type', 'unknown')} with NULL id: {item.get('name', '?')} — marking as error")
try:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
continue
self._process_item(item)
# Sleep depends on item type — search items need more delay
@ -158,7 +170,7 @@ class iTunesWorker:
cursor.execute("""
SELECT id, name
FROM artists
WHERE itunes_match_status IS NULL
WHERE itunes_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
LIMIT 1
""")
@ -174,7 +186,7 @@ class iTunesWorker:
AND ar.itunes_artist_id IS NOT NULL
AND EXISTS (
SELECT 1 FROM albums al
WHERE al.artist_id = ar.id AND al.itunes_match_status IS NULL
WHERE al.artist_id = ar.id AND al.itunes_match_status IS NULL AND al.id IS NOT NULL
)
ORDER BY ar.id ASC
LIMIT 1
@ -198,7 +210,7 @@ class iTunesWorker:
AND al.itunes_album_id IS NOT NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.album_id = al.id AND t.itunes_match_status IS NULL
WHERE t.album_id = al.id AND t.itunes_match_status IS NULL AND t.id IS NOT NULL
)
ORDER BY al.id ASC
LIMIT 1
@ -219,7 +231,7 @@ 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 IS NULL
WHERE a.itunes_match_status IS NULL AND a.id IS NOT NULL
ORDER BY a.id ASC
LIMIT 1
""")
@ -232,7 +244,7 @@ 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 IS NULL
WHERE t.itunes_match_status IS NULL AND t.id IS NOT NULL
ORDER BY t.id ASC
LIMIT 1
""")
@ -717,7 +729,7 @@ class iTunesWorker:
cursor = conn.cursor()
cursor.execute("""
SELECT id, title, track_number FROM tracks
WHERE album_id = ? AND itunes_match_status IS NULL
WHERE album_id = ? AND itunes_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
""", (album_id,))
return [{'id': row[0], 'title': row[1], 'track_number': row[2]} for row in cursor.fetchall()]

View file

@ -143,6 +143,18 @@ class LastFMWorker:
continue
self.current_item = item
# Guard: skip items with None/NULL IDs to prevent infinite enrichment loops
item_id = item.get('id') or item.get('artist_id') or item.get('album_id')
if item_id is None:
logger.warning(f"Skipping {item.get('type', 'unknown')} with NULL id: {item.get('name', '?')} — marking as error")
try:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
continue
self._process_item(item)
# Last.fm allows 5 req/sec but we use multiple calls per item
@ -165,7 +177,7 @@ class LastFMWorker:
cursor.execute("""
SELECT id, name
FROM artists
WHERE lastfm_match_status IS NULL
WHERE lastfm_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
LIMIT 1
""")
@ -178,7 +190,7 @@ class LastFMWorker:
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 IS NULL
WHERE a.lastfm_match_status IS NULL AND a.id IS NOT NULL
ORDER BY a.id ASC
LIMIT 1
""")
@ -191,7 +203,7 @@ class LastFMWorker:
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 IS NULL
WHERE t.lastfm_match_status IS NULL AND t.id IS NOT NULL
ORDER BY t.id ASC
LIMIT 1
""")

View file

@ -130,6 +130,18 @@ class MusicBrainzWorker:
# Set current item for UI tracking
self.current_item = item
# Guard: skip items with None/NULL IDs to prevent infinite enrichment loops
item_id = item.get('id') or item.get('artist_id') or item.get('album_id')
if item_id is None:
logger.warning(f"Skipping {item.get('type', 'unknown')} with NULL id: {item.get('name', '?')} — marking as error")
try:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
continue
# Process the item
self._process_item(item)
@ -155,7 +167,7 @@ class MusicBrainzWorker:
cursor.execute("""
SELECT id, name
FROM artists
WHERE musicbrainz_match_status IS NULL
WHERE musicbrainz_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
LIMIT 1
""")
@ -168,7 +180,7 @@ class MusicBrainzWorker:
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 IS NULL
WHERE a.musicbrainz_match_status IS NULL AND a.id IS NOT NULL
ORDER BY a.id ASC
LIMIT 1
""")
@ -181,7 +193,7 @@ class MusicBrainzWorker:
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 IS NULL
WHERE t.musicbrainz_match_status IS NULL AND t.id IS NOT NULL
ORDER BY t.id ASC
LIMIT 1
""")

View file

@ -151,6 +151,18 @@ class QobuzWorker:
continue
self.current_item = item
# Guard: skip items with None/NULL IDs to prevent infinite enrichment loops
item_id = item.get('id') or item.get('artist_id') or item.get('album_id')
if item_id is None:
logger.warning(f"Skipping {item.get('type', 'unknown')} with NULL id: {item.get('name', '?')} — marking as error")
try:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
continue
self._process_item(item)
@ -174,7 +186,7 @@ class QobuzWorker:
cursor.execute("""
SELECT id, name
FROM artists
WHERE qobuz_match_status IS NULL
WHERE qobuz_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
LIMIT 1
""")
@ -187,7 +199,7 @@ class QobuzWorker:
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 IS NULL
WHERE a.qobuz_match_status IS NULL AND a.id IS NOT NULL
ORDER BY a.id ASC
LIMIT 1
""")
@ -200,7 +212,7 @@ class QobuzWorker:
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 IS NULL
WHERE t.qobuz_match_status IS NULL AND t.id IS NOT NULL
ORDER BY t.id ASC
LIMIT 1
""")

View file

@ -168,6 +168,18 @@ class SpotifyWorker:
continue
self.current_item = item
# Guard: skip items with None/NULL IDs to prevent infinite enrichment loops
item_id = item.get('id') or item.get('artist_id') or item.get('album_id')
if item_id is None:
logger.warning(f"Skipping {item.get('type', 'unknown')} with NULL id: {item.get('name', '?')} — marking as error")
try:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
continue
self._process_item(item)
time.sleep(self.inter_item_sleep)
@ -193,7 +205,7 @@ class SpotifyWorker:
cursor.execute("""
SELECT id, name
FROM artists
WHERE spotify_match_status IS NULL
WHERE spotify_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
LIMIT 1
""")
@ -209,7 +221,7 @@ class SpotifyWorker:
AND ar.spotify_artist_id IS NOT NULL
AND EXISTS (
SELECT 1 FROM albums al
WHERE al.artist_id = ar.id AND al.spotify_match_status IS NULL
WHERE al.artist_id = ar.id AND al.spotify_match_status IS NULL AND al.id IS NOT NULL
)
ORDER BY ar.id ASC
LIMIT 1
@ -233,7 +245,7 @@ class SpotifyWorker:
AND al.spotify_album_id IS NOT NULL
AND EXISTS (
SELECT 1 FROM tracks t
WHERE t.album_id = al.id AND t.spotify_match_status IS NULL
WHERE t.album_id = al.id AND t.spotify_match_status IS NULL AND t.id IS NOT NULL
)
ORDER BY al.id ASC
LIMIT 1
@ -254,7 +266,7 @@ 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 IS NULL
WHERE a.spotify_match_status IS NULL AND a.id IS NOT NULL
ORDER BY a.id ASC
LIMIT 1
""")
@ -267,7 +279,7 @@ 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 IS NULL
WHERE t.spotify_match_status IS NULL AND t.id IS NOT NULL
ORDER BY t.id ASC
LIMIT 1
""")
@ -758,7 +770,7 @@ class SpotifyWorker:
cursor = conn.cursor()
cursor.execute("""
SELECT id, title, track_number FROM tracks
WHERE album_id = ? AND spotify_match_status IS NULL
WHERE album_id = ? AND spotify_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
""", (album_id,))
return [{'id': row[0], 'title': row[1], 'track_number': row[2]} for row in cursor.fetchall()]

View file

@ -164,6 +164,18 @@ class TidalWorker:
continue
self.current_item = item
# Guard: skip items with None/NULL IDs to prevent infinite enrichment loops
item_id = item.get('id') or item.get('artist_id') or item.get('album_id')
if item_id is None:
logger.warning(f"Skipping {item.get('type', 'unknown')} with NULL id: {item.get('name', '?')} — marking as error")
try:
itype = item.get('type', '')
table = 'artists' if 'artist' in itype else ('albums' if 'album' in itype else 'tracks')
# Can't mark status without an ID — just skip
except Exception:
pass
continue
self._process_item(item)
@ -186,7 +198,7 @@ class TidalWorker:
cursor.execute("""
SELECT id, name
FROM artists
WHERE tidal_match_status IS NULL
WHERE tidal_match_status IS NULL AND id IS NOT NULL
ORDER BY id ASC
LIMIT 1
""")
@ -199,7 +211,7 @@ class TidalWorker:
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 IS NULL
WHERE a.tidal_match_status IS NULL AND a.id IS NOT NULL
ORDER BY a.id ASC
LIMIT 1
""")
@ -212,7 +224,7 @@ class TidalWorker:
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 IS NULL
WHERE t.tidal_match_status IS NULL AND t.id IS NOT NULL
ORDER BY t.id ASC
LIMIT 1
""")

View file

@ -15364,7 +15364,7 @@ def _embed_source_ids(audio_file, metadata: dict):
print(f"🎵 Deezer track matched: {dz_track_id}")
# Get full track details for BPM and ISRC
dz_details = dz_client.get_track(dz_track_id)
dz_details = dz_client.get_track_details(dz_track_id)
if dz_details:
bpm_val = dz_details.get('bpm')
if bpm_val and bpm_val > 0: