Merge pull request #472 from Nezreka/fix/extract-ids-recognize-underscore-source

Make extract_external_ids recognize all source-tagging conventions
This commit is contained in:
BoulderBadgeDad 2026-05-02 18:55:39 -07:00 committed by GitHub
commit 21968ade26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 61 additions and 7 deletions

View file

@ -67,15 +67,23 @@ def _get(track: Any, *names: str) -> Optional[str]:
return None
def extract_external_ids(track: Any) -> Dict[str, str]:
def extract_external_ids(track: Any, source_hint: Optional[str] = None) -> Dict[str, str]:
"""Pull every recognized external ID off a metadata-source track.
Handles the source-source naming drift: Spotify tracks expose ``id``
as the Spotify track ID; Deezer tracks expose ``id`` as the Deezer
track ID; iTunes tracks may use ``trackId`` or ``id``. The disamb-
iguating field is ``provider`` / ``source``. Tracks coming from a
SoulSync internal pipeline often carry every known ID set to its
source-specific value we just collect whatever's there.
iguating field is ``provider`` / ``source`` / ``_source``. Tracks
coming from a SoulSync internal pipeline often carry every known ID
set to its source-specific value we just collect whatever's there.
``source_hint`` is the caller's known answer to "where did this
track dict come from?" — used as a fallback when the track itself
doesn't carry a provider / source / _source field. Spotify and
iTunes return raw API responses without provider tags, so the
watchlist scanner passes ``get_primary_source()`` here to make sure
a Spotify-primary scan isn't silently no-opping just because the
raw API track has no provider key.
Returns a dict mapping conceptual ID name ID value. Keys present
in ``EXTERNAL_ID_COLUMNS``. Empty dict when no IDs are available.
@ -106,8 +114,13 @@ def extract_external_ids(track: Any) -> Dict[str, str]:
# Provider field tells us which native ``id`` belongs to. Without
# this, a Deezer track's ``id`` field would be silently ignored
# (we wouldn't know to map it to deezer_id).
provider = (_get(track, 'provider', 'source') or '').lower()
# (we wouldn't know to map it to deezer_id). Convention varies by
# client: Spotify-shaped tracks usually have no provider field,
# Deezer / Discogs / Hydrabase clients tag tracks with ``_source``,
# internal pipeline normalization may use ``source`` or ``provider``.
# Fall back to the caller's source_hint when the track has no
# provider field of its own (Spotify / iTunes raw API responses).
provider = (_get(track, 'provider', 'source', '_source') or source_hint or '').lower()
native_id = _get(track, 'id')
if native_id and provider:
provider_to_key = {

View file

@ -2077,7 +2077,16 @@ class WatchlistScanner:
find_provenance_by_external_id,
)
import os as _os_local
source_ids = extract_external_ids(track)
# Pass the configured primary source as a hint so the
# extractor can disambiguate raw Spotify / iTunes API
# responses that don't carry a provider / source field
# of their own (Deezer / Discogs / Hydrabase clients
# already tag tracks with _source).
try:
_source_hint = get_primary_source()
except Exception:
_source_hint = None
source_ids = extract_external_ids(track, source_hint=_source_hint)
if source_ids:
matched = find_library_track_by_external_id(
self.database,

View file

@ -99,11 +99,43 @@ class TestExtractExternalIdsFromProviderField:
track = {'source': 'deezer', 'id': 'dz1', 'name': 'Hello'}
assert extract_external_ids(track) == {'deezer_id': 'dz1'}
def test_underscore_source_field_treated_same_as_provider(self):
"""Deezer / Discogs / Hydrabase clients tag normalized tracks
with ``_source`` (underscore prefix). Must be recognized as the
provider disambiguator otherwise watchlist scans against those
sources silently miss the ID match and fall through to fuzzy."""
track = {'_source': 'deezer', 'id': 'dz1', 'name': 'Hello'}
assert extract_external_ids(track) == {'deezer_id': 'dz1'}
def test_underscore_source_hydrabase(self):
track = {'_source': 'hydrabase', 'id': 'hyd-soul-1', 'name': 'Hello'}
assert extract_external_ids(track) == {'soul_id': 'hyd-soul-1'}
def test_native_id_without_provider_is_ignored(self):
"""Without a provider field we can't tell which source 'id' belongs to."""
track = {'id': 'unknown', 'name': 'Hello'}
assert extract_external_ids(track) == {}
def test_source_hint_disambiguates_when_track_has_no_provider(self):
"""Spotify and iTunes raw API responses don't carry a provider /
source / _source field. The watchlist scanner passes the
configured primary source as a hint so the extractor can map
the track's native ``id`` field correctly."""
track = {'id': 'sp1', 'name': 'Hello'} # No provider field
assert extract_external_ids(track, source_hint='spotify') == {'spotify_id': 'sp1'}
assert extract_external_ids(track, source_hint='itunes') == {'itunes_id': 'sp1'}
assert extract_external_ids(track, source_hint='deezer') == {'deezer_id': 'sp1'}
def test_source_hint_does_not_override_track_provider(self):
"""If the track already carries an explicit provider, that wins
over the hint defensive against the hint being wrong."""
track = {'_source': 'deezer', 'id': 'dz1', 'name': 'Hello'}
assert extract_external_ids(track, source_hint='spotify') == {'deezer_id': 'dz1'}
def test_source_hint_none_is_ignored(self):
track = {'id': 'unknown', 'name': 'Hello'}
assert extract_external_ids(track, source_hint=None) == {}
class TestExtractExternalIdsMixedAndDefensive:
def test_track_with_multiple_provider_specific_fields(self):