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:
parent
81852caa59
commit
e0533215da
10 changed files with 141 additions and 33 deletions
|
|
@ -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
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -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()]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -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()]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue