Apply artist-id name-guard to audiodb/qobuz/tidal workers too

The blind 'correct the parent artist's source id from an album/track match'
logic was copy-pasted into four enrichment workers; the Deezer fix only covered
one. AudioDB, Qobuz, and Tidal had the identical bug and would corrupt their own
id columns (and re-corrupt after any cleanup).

All three now gate the correction on a name match between the result's artist
and the parent artist (audiodb reads result['strArtist']; qobuz/tidal thread the
result artist name in from their callers, as Deezer does). Regression tests
cover mismatch-skips and match-corrects for each.
This commit is contained in:
BoulderBadgeDad 2026-06-05 07:47:59 -07:00
parent 8ce26d19fa
commit 85549197e6
4 changed files with 162 additions and 9 deletions

View file

@ -273,8 +273,13 @@ class AudioDBWorker:
def _verify_artist_id(self, item: Dict[str, Any], result: Dict[str, Any]) -> bool:
"""Verify that the result's artist ID matches the parent artist's stored AudioDB ID.
If mismatched, the album/track search is more specific (uses artist+title),
so we trust it and correct the parent artist's audiodb_id."""
so we trust it and correct the parent artist's audiodb_id — BUT only when
the result's artist *name* matches our parent artist. Without that guard,
a collaboration/compilation (a track our library credits to one artist
that lives on another artist's album) would stamp the wrong AudioDB id
onto our artist. See the Deezer fix for the full write-up."""
parent_audiodb_id = item.get('artist_audiodb_id')
if not parent_audiodb_id:
return True
@ -284,6 +289,18 @@ class AudioDBWorker:
return True
if str(result_artist_id) != str(parent_audiodb_id):
parent_name = item.get('artist') or ''
result_artist_name = result.get('strArtist') or ''
if (result_artist_name and parent_name
and not self._name_matches(parent_name, result_artist_name)):
logger.info(
f"Skipping artist-ID correction from {item['type']} "
f"'{item['name']}': result artist '{result_artist_name}' "
f"≠ parent '{parent_name}' (collab/compilation, not a "
f"correction)"
)
return True
logger.info(
f"Artist ID correction from {item['type']} '{item['name']}': "
f"updating parent artist AudioDB ID from {parent_audiodb_id} to {result_artist_id}"

View file

@ -301,13 +301,29 @@ class QobuzWorker:
logger.debug(f"Name similarity: '{query_name}' vs '{result_name}' = {similarity:.2f}")
return similarity >= self.name_similarity_threshold
def _verify_artist_id(self, item: Dict[str, Any], result_artist_id) -> bool:
"""Verify/correct parent artist's Qobuz ID based on album/track match"""
def _verify_artist_id(self, item: Dict[str, Any], result_artist_id,
result_artist_name: Optional[str] = None) -> bool:
"""Verify/correct parent artist's Qobuz ID based on album/track match.
Only corrects when the result's artist *name* matches our parent artist —
otherwise a collaboration/compilation would stamp the wrong Qobuz id onto
our artist. See the Deezer fix for the full write-up."""
parent_qobuz_id = item.get('artist_qobuz_id')
if not parent_qobuz_id or not result_artist_id:
return True
if str(result_artist_id) != str(parent_qobuz_id):
parent_name = item.get('artist') or ''
if (result_artist_name and parent_name
and not self._name_matches(parent_name, result_artist_name)):
logger.info(
f"Skipping artist-ID correction from {item['type']} "
f"'{item['name']}': result artist '{result_artist_name}' "
f"≠ parent '{parent_name}' (collab/compilation, not a "
f"correction)"
)
return True
logger.info(
f"Artist ID correction from {item['type']} '{item['name']}': "
f"updating parent artist Qobuz ID from {parent_qobuz_id} to {result_artist_id}"
@ -467,7 +483,8 @@ class QobuzWorker:
# Verify artist ID
result_artist = result.get('artist', {})
result_artist_id = result_artist.get('id') if result_artist else None
self._verify_artist_id(item, result_artist_id)
result_artist_name = result_artist.get('name') if result_artist else None
self._verify_artist_id(item, result_artist_id, result_artist_name)
# Fetch full album details
qobuz_album_id = result.get('id')
@ -528,7 +545,8 @@ class QobuzWorker:
# Verify artist ID
result_artist = result.get('artist', result.get('performer', {}))
result_artist_id = result_artist.get('id') if result_artist else None
self._verify_artist_id(item, result_artist_id)
result_artist_name = result_artist.get('name') if result_artist else None
self._verify_artist_id(item, result_artist_id, result_artist_name)
# Fetch full track details
qobuz_track_id = result.get('id')

View file

@ -313,13 +313,29 @@ class TidalWorker:
logger.debug(f"Name similarity: '{query_name}' vs '{result_name}' = {similarity:.2f}")
return similarity >= self.name_similarity_threshold
def _verify_artist_id(self, item: Dict[str, Any], result_artist_id) -> bool:
"""Verify/correct parent artist's Tidal ID based on album/track match"""
def _verify_artist_id(self, item: Dict[str, Any], result_artist_id,
result_artist_name: Optional[str] = None) -> bool:
"""Verify/correct parent artist's Tidal ID based on album/track match.
Only corrects when the result's artist *name* matches our parent artist —
otherwise a collaboration/compilation would stamp the wrong Tidal id onto
our artist. See the Deezer fix for the full write-up."""
parent_tidal_id = item.get('artist_tidal_id')
if not parent_tidal_id or not result_artist_id:
return True
if str(result_artist_id) != str(parent_tidal_id):
parent_name = item.get('artist') or ''
if (result_artist_name and parent_name
and not self._name_matches(parent_name, result_artist_name)):
logger.info(
f"Skipping artist-ID correction from {item['type']} "
f"'{item['name']}': result artist '{result_artist_name}' "
f"≠ parent '{parent_name}' (collab/compilation, not a "
f"correction)"
)
return True
logger.info(
f"Artist ID correction from {item['type']} '{item['name']}': "
f"updating parent artist Tidal ID from {parent_tidal_id} to {result_artist_id}"
@ -479,7 +495,8 @@ class TidalWorker:
# Verify artist ID
result_artist = result.get('artist', {})
result_artist_id = result_artist.get('id') if result_artist else None
self._verify_artist_id(item, result_artist_id)
result_artist_name = result_artist.get('name') if result_artist else None
self._verify_artist_id(item, result_artist_id, result_artist_name)
# Fetch full album details
tidal_album_id = result.get('id')
@ -533,7 +550,8 @@ class TidalWorker:
# Verify artist ID
result_artist = result.get('artist', {})
result_artist_id = result_artist.get('id') if result_artist else None
self._verify_artist_id(item, result_artist_id)
result_artist_name = result_artist.get('name') if result_artist else None
self._verify_artist_id(item, result_artist_id, result_artist_name)
# Fetch full track details
tidal_track_id = result.get('id')

View file

@ -0,0 +1,100 @@
"""Regression: the album/track-driven 'artist id correction' in every
enrichment worker that has it must NOT overwrite an artist's source id unless
the result's artist name actually matches.
This is the same Kendrick/Jorja bug fixed in the Deezer worker (see
tests/test_deezer_worker_artist_id_guard.py), proven here to be closed in the
three other workers that copy-pasted the pattern: AudioDB, Qobuz, Tidal.
A track our library credits to Jorja Smith that lives on Kendrick's curated
'Black Panther' album resolves to that album, whose primary artist is Kendrick.
Without a name check, each worker would 'correct' Jorja's source id to
Kendrick's — corrupting it (and sharing one id across unrelated artists).
"""
from __future__ import annotations
from core.audiodb_worker import AudioDBWorker
from core.qobuz_worker import QobuzWorker
from core.tidal_worker import TidalWorker
def _stub(cls, correct_attr):
"""Build a bare worker instance (no __init__/clients) wired to record
corrections instead of writing to a db."""
w = cls.__new__(cls)
w.name_similarity_threshold = 0.80
w._corrections = []
setattr(w, correct_attr, lambda item, cid: w._corrections.append((item['id'], cid)))
return w
def _item(artist_name, parent_id, id_key):
return {'type': 'track', 'id': 1, 'name': 'Some Track',
'artist': artist_name, id_key: parent_id}
# --------------------------------------------------------------------------
# AudioDB — _verify_artist_id(item, result_dict); name is result['strArtist'].
# --------------------------------------------------------------------------
def test_audiodb_skips_correction_on_name_mismatch():
w = _stub(AudioDBWorker, '_correct_artist_audiodb_id')
item = _item('Jorja Smith', '111', 'artist_audiodb_id')
w._verify_artist_id(item, {'idArtist': '999', 'strArtist': 'Kendrick Lamar'})
assert w._corrections == []
def test_audiodb_corrects_on_name_match():
w = _stub(AudioDBWorker, '_correct_artist_audiodb_id')
item = _item('Kendrick Lamar', '111', 'artist_audiodb_id')
w._verify_artist_id(item, {'idArtist': '999', 'strArtist': 'Kendrick Lamar'})
assert w._corrections == [(1, '999')]
# --------------------------------------------------------------------------
# Qobuz — _verify_artist_id(item, result_artist_id, result_artist_name).
# --------------------------------------------------------------------------
def test_qobuz_skips_correction_on_name_mismatch():
w = _stub(QobuzWorker, '_correct_artist_qobuz_id')
item = _item('Jorja Smith', '111', 'artist_qobuz_id')
w._verify_artist_id(item, '999', 'Kendrick Lamar')
assert w._corrections == []
def test_qobuz_corrects_on_name_match():
w = _stub(QobuzWorker, '_correct_artist_qobuz_id')
item = _item('Kendrick Lamar', '111', 'artist_qobuz_id')
w._verify_artist_id(item, '999', 'Kendrick Lamar')
assert w._corrections == [(1, '999')]
# --------------------------------------------------------------------------
# Tidal — _verify_artist_id(item, result_artist_id, result_artist_name).
# --------------------------------------------------------------------------
def test_tidal_skips_correction_on_name_mismatch():
w = _stub(TidalWorker, '_correct_artist_tidal_id')
item = _item('Jorja Smith', '111', 'artist_tidal_id')
w._verify_artist_id(item, '999', 'Kendrick Lamar')
assert w._corrections == []
def test_tidal_corrects_on_name_match():
w = _stub(TidalWorker, '_correct_artist_tidal_id')
item = _item('Kendrick Lamar', '111', 'artist_tidal_id')
w._verify_artist_id(item, '999', 'Kendrick Lamar')
assert w._corrections == [(1, '999')]
# --------------------------------------------------------------------------
# Shared: a missing result name preserves the old "trust the search" behavior
# (only the workers that pass an id+name — qobuz/tidal — exercise this path).
# --------------------------------------------------------------------------
def test_qobuz_missing_result_name_preserves_old_behavior():
w = _stub(QobuzWorker, '_correct_artist_qobuz_id')
item = _item('Kendrick Lamar', '111', 'artist_qobuz_id')
w._verify_artist_id(item, '999', None)
assert w._corrections == [(1, '999')]