Fix Deezer enrichment stamping one artist id onto multiple artists

Root cause of the duplicate deezer_id corruption: when enriching an album or
track, _verify_artist_id 'corrected' the parent artist's deezer_id to the
search result's primary-artist id whenever they differed — with NO name check.
For a collaboration/compilation track (e.g. one our library credits to Jorja
Smith that lives on Kendrick Lamar's curated 'Black Panther' album), the result
resolves to Kendrick's album, so Kendrick's id (525046) got written onto Jorja,
Vince Staples, SOB X RBE, etc. — many artists ending up with the same id.

Now the correction only fires when the result's primary-artist NAME matches the
parent artist (the album/track-artist path now mirrors _process_artist, which
already name-checks). Mismatches are logged and skipped as collab/compilation.

Note: this prevents new corruption; existing wrong ids in a library aren't
auto-repaired (per-artist enrichment preserves an existing deezer_id).
This commit is contained in:
BoulderBadgeDad 2026-06-05 07:32:51 -07:00
parent 2962267d36
commit 8ce26d19fa
2 changed files with 105 additions and 4 deletions

View file

@ -278,10 +278,19 @@ class DeezerWorker:
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:
def _verify_artist_id(self, item: Dict[str, Any], result_artist_id,
result_artist_name: Optional[str] = None) -> bool:
"""Verify that the result's artist ID matches the parent artist's stored Deezer ID.
If mismatched, the album/track search is more specific (uses artist+title),
so we trust it and correct the parent artist's deezer_id."""
so we trust it and correct the parent artist's deezer_id — BUT only when
the result's artist *name* actually matches our parent artist. Without
that guard, a collaboration or compilation track (e.g. a track our
library credits to Jorja Smith that lives on Kendrick Lamar's curated
"Black Panther" album) would search up to an album whose Deezer primary
artist is someone else (Kendrick), and we'd stamp that wrong Deezer ID
onto our artist corrupting it (and causing duplicate ids shared across
unrelated artists)."""
parent_deezer_id = item.get('artist_deezer_id')
if not parent_deezer_id:
return True
@ -290,6 +299,20 @@ class DeezerWorker:
return True
if str(result_artist_id) != str(parent_deezer_id):
# Guard: only correct when the album/track's primary artist is the
# SAME artist by name. A mismatch means it's a collab/compilation,
# not a stale-id correction.
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 Deezer ID from {parent_deezer_id} to {result_artist_id}"
@ -430,7 +453,8 @@ class DeezerWorker:
# 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 for label, genres, explicit
deezer_album_id = result.get('id')
@ -481,7 +505,8 @@ class DeezerWorker:
# 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 for BPM
deezer_track_id = result.get('id')

View file

@ -0,0 +1,76 @@
"""Regression: Deezer enrichment must not overwrite an artist's deezer_id from a
collaboration/compilation track whose primary artist is someone else.
The Kendrick/Jorja bug: a track our library credits to Jorja Smith lives on
Kendrick Lamar's curated "Black Panther" album. The album/track search resolves
to that album, whose Deezer primary artist is Kendrick (id 525046). The old
``_verify_artist_id`` "corrected" Jorja's deezer_id to 525046 with no name
check stamping one Deezer id across several unrelated artists, which later
broke the artist-detail page (it matched the wrong library artist by id).
The fix gates the correction on a name match between the result's primary
artist and our parent artist.
"""
from __future__ import annotations
from core.deezer_worker import DeezerWorker
def _worker():
# Bypass __init__ — it wants real clients/db. We only exercise the pure
# _verify_artist_id / _name_matches / _normalize_name logic.
w = DeezerWorker.__new__(DeezerWorker)
w.name_similarity_threshold = 0.80
w._corrections = []
w._correct_artist_deezer_id = lambda item, cid: w._corrections.append((item['id'], cid))
return w
def _item(artist_name, parent_deezer_id):
return {
'type': 'track', 'id': 1, 'name': 'Some Track',
'artist': artist_name, 'artist_deezer_id': parent_deezer_id,
}
def test_no_correction_when_result_artist_name_differs():
# Jorja Smith (deezer 999) but the track resolved to Kendrick's album
# (Deezer artist 525046, 'Kendrick Lamar') → must NOT overwrite.
w = _worker()
w._verify_artist_id(_item('Jorja Smith', '999'), '525046', 'Kendrick Lamar')
assert w._corrections == []
def test_correction_when_names_match():
# Same artist, stale/wrong stored id → legitimate correction proceeds.
w = _worker()
w._verify_artist_id(_item('Kendrick Lamar', '111'), '525046', 'Kendrick Lamar')
assert w._corrections == [(1, '525046')]
def test_name_match_tolerates_minor_variation():
# Fuzzy match (feat. suffix / casing) still counts as the same artist.
w = _worker()
w._verify_artist_id(_item('Kendrick Lamar', '111'), '525046', 'KENDRICK LAMAR')
assert w._corrections == [(1, '525046')]
def test_no_correction_when_ids_already_equal():
w = _worker()
w._verify_artist_id(_item('Whoever', '525046'), '525046', 'Anyone')
assert w._corrections == []
def test_no_parent_id_is_noop():
w = _worker()
w._verify_artist_id(_item('X', None), '525046', 'Y')
assert w._corrections == []
def test_missing_result_name_preserves_old_behavior():
# No artist name on the result → can't name-check; keep the original
# "trust the more specific album/track search" behavior.
w = _worker()
w._verify_artist_id(_item('Kendrick Lamar', '111'), '525046', None)
assert w._corrections == [(1, '525046')]