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:
parent
2962267d36
commit
8ce26d19fa
2 changed files with 105 additions and 4 deletions
|
|
@ -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')
|
||||
|
|
|
|||
76
tests/test_deezer_worker_artist_id_guard.py
Normal file
76
tests/test_deezer_worker_artist_id_guard.py
Normal 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')]
|
||||
Loading…
Reference in a new issue