Third round of the multi-artist report. The earlier fixes (Deezer contributors upgrade, _artists_list, feat_in_title/artist_separator) were all in place and correct — but gated on source == 'deezer', and on the real Search → Download Now path NOTHING carried the source: core/search/sources.py serialized tracks with no source field, search.js's enrichedTrack didn't add one, so get_import_source() resolved '' and the whole Deezer-specific block silently skipped. Files were tagged with only the primary artist until a Retag (which rebuilds context with the source set — exactly why retagging always fixed it). The earlier tests passed because they set context['source'] directly — the one field the real flow never had (same mock-drift as the #823 append tests). Reproduced with Netti93's exact track (deezer 3966840171) through the real extract_source_metadata: before — source '', artists ['August Burns Red']; after — source 'deezer', contributors fetched, artists ['August Burns Red', 'Polaris'], title 'Sonic Salvation (feat. Polaris)' per feat_in_title. Fix, three layers: - core/search/sources.py: serialized tracks/albums/artists carry "source" (the canonical name the orchestrator already passes; '' when unnamed). - core/imports/context.py get_import_source: also reads '_source' from the nested dicts (track_info/original_search/album/artist) — additionally fixes the discography/wishlist flows, which always passed '_source' that nothing read. - search.js: enrichedTrack + the album-download path carry source through to the download task. Tests: real-payload staging-shaped contexts (source in track_info, '_source' shape, and the pre-fix sourceless shape staying safe — mocked Deezer client), serializer source-field tests, resolver fallback tests; exact-shape serializer tests updated for the new key. 1977 import/metadata/search tests pass (the only 2 failures are the known soundcloud ones).
357 lines
13 KiB
Python
357 lines
13 KiB
Python
import pytest
|
|
|
|
from core.imports.context import (
|
|
build_import_album_info,
|
|
detect_album_info_web,
|
|
get_import_clean_album,
|
|
get_import_clean_artist,
|
|
get_import_clean_title,
|
|
get_import_has_clean_metadata,
|
|
get_import_has_full_metadata,
|
|
get_import_source,
|
|
get_import_source_ids,
|
|
get_library_source_id_columns,
|
|
get_source_tag_names,
|
|
normalize_import_context,
|
|
)
|
|
|
|
|
|
def test_normalize_import_context_promotes_neutral_fields_without_legacy_aliases():
|
|
context = {
|
|
"source": "deezer",
|
|
"spotify_artist": {"name": "Artist One", "id": "artist-1"},
|
|
"spotify_album": {
|
|
"name": "Album One",
|
|
"id": "album-1",
|
|
"release_date": "2024-01-01",
|
|
"total_tracks": 12,
|
|
"album_type": "album",
|
|
"image_url": "https://img.example/album.jpg",
|
|
},
|
|
"track_info": {
|
|
"name": "Song One",
|
|
"id": "track-1",
|
|
"track_number": 3,
|
|
"disc_number": 2,
|
|
"artists": [{"name": "Artist One"}],
|
|
},
|
|
"original_search_result": {
|
|
"spotify_clean_title": "Song One",
|
|
"spotify_clean_album": "Album One",
|
|
"spotify_clean_artist": "Artist One",
|
|
},
|
|
"has_clean_spotify_data": True,
|
|
"has_full_spotify_metadata": False,
|
|
}
|
|
|
|
normalized = normalize_import_context(context)
|
|
|
|
assert normalized["artist"]["name"] == "Artist One"
|
|
assert normalized["album"]["name"] == "Album One"
|
|
assert "spotify_artist" not in normalized
|
|
assert "spotify_album" not in normalized
|
|
assert normalized["original_search_result"]["clean_title"] == "Song One"
|
|
assert normalized["original_search_result"]["clean_album"] == "Album One"
|
|
assert normalized["original_search_result"]["clean_artist"] == "Artist One"
|
|
assert "spotify_clean_title" not in normalized["original_search_result"]
|
|
assert "spotify_clean_album" not in normalized["original_search_result"]
|
|
assert "spotify_clean_artist" not in normalized["original_search_result"]
|
|
assert get_import_clean_title(normalized) == "Song One"
|
|
assert get_import_clean_album(normalized) == "Album One"
|
|
assert get_import_clean_artist(normalized) == "Artist One"
|
|
assert get_import_source(normalized) == "deezer"
|
|
assert get_import_has_clean_metadata(normalized) is True
|
|
assert get_import_has_full_metadata(normalized) is False
|
|
|
|
|
|
def test_normalize_import_context_promotes_legacy_source_alias():
|
|
context = {
|
|
"_source": "spotify",
|
|
"artist": {"name": "Artist One", "id": "artist-1"},
|
|
"album": {"name": "Album One", "id": "album-1"},
|
|
"track_info": {"name": "Song One", "id": "track-1"},
|
|
"original_search_result": {"title": "Song One"},
|
|
}
|
|
|
|
normalized = normalize_import_context(context)
|
|
|
|
assert normalized["source"] == "spotify"
|
|
assert "_source" not in normalized
|
|
assert get_import_source(normalized) == "spotify"
|
|
|
|
|
|
def test_normalize_import_context_promotes_search_result_when_original_search_missing():
|
|
context = {
|
|
"source": "spotify",
|
|
"track_info": {"name": "Song One", "id": "track-1"},
|
|
"search_result": {
|
|
"title": "Song One",
|
|
"album": "Album One",
|
|
"artist": "Artist One",
|
|
"spotify_clean_title": "Song One",
|
|
"spotify_clean_album": "Album One",
|
|
"spotify_clean_artist": "Artist One",
|
|
},
|
|
}
|
|
|
|
normalized = normalize_import_context(context)
|
|
|
|
assert normalized["original_search_result"]["clean_title"] == "Song One"
|
|
assert normalized["original_search_result"]["clean_album"] == "Album One"
|
|
assert normalized["original_search_result"]["clean_artist"] == "Artist One"
|
|
assert "spotify_clean_title" not in normalized["original_search_result"]
|
|
assert "spotify_clean_album" not in normalized["original_search_result"]
|
|
assert "spotify_clean_artist" not in normalized["original_search_result"]
|
|
assert get_import_clean_title(normalized) == "Song One"
|
|
assert get_import_clean_album(normalized) == "Album One"
|
|
assert get_import_clean_artist(normalized) == "Artist One"
|
|
|
|
|
|
def test_neutral_import_context_helpers_work_without_legacy_aliases():
|
|
context = {
|
|
"source": "deezer",
|
|
"artist": {"name": "Artist One", "id": "artist-1"},
|
|
"album": {
|
|
"name": "Album One",
|
|
"id": "album-1",
|
|
"release_date": "2024-01-01",
|
|
"total_tracks": 12,
|
|
"album_type": "album",
|
|
"image_url": "https://img.example/album.jpg",
|
|
},
|
|
"track_info": {
|
|
"name": "Song One",
|
|
"id": "track-1",
|
|
"track_number": 3,
|
|
"disc_number": 2,
|
|
"artists": [{"name": "Artist One"}],
|
|
},
|
|
"original_search_result": {
|
|
"title": "Song One",
|
|
"artist": "Artist One",
|
|
"album": "Album One",
|
|
"clean_title": "Song One",
|
|
"clean_album": "Album One",
|
|
"clean_artist": "Artist One",
|
|
},
|
|
"has_clean_metadata": True,
|
|
"has_full_metadata": True,
|
|
}
|
|
|
|
assert get_import_clean_title(context) == "Song One"
|
|
assert get_import_clean_album(context) == "Album One"
|
|
assert get_import_clean_artist(context) == "Artist One"
|
|
assert get_import_source(context) == "deezer"
|
|
|
|
normalized = normalize_import_context(context)
|
|
assert normalized["artist"]["name"] == "Artist One"
|
|
assert normalized["album"]["name"] == "Album One"
|
|
assert normalized["original_search_result"]["clean_title"] == "Song One"
|
|
assert "spotify_artist" not in normalized
|
|
assert "spotify_album" not in normalized
|
|
assert get_import_has_clean_metadata(normalized) is True
|
|
assert get_import_has_full_metadata(normalized) is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"source,expected_tags,expected_columns",
|
|
[
|
|
(
|
|
"spotify",
|
|
{"track": "SPOTIFY_TRACK_ID", "artist": "SPOTIFY_ARTIST_ID", "album": "SPOTIFY_ALBUM_ID"},
|
|
{"artist": "spotify_artist_id", "album": "spotify_album_id", "track": "spotify_track_id"},
|
|
),
|
|
(
|
|
"itunes",
|
|
{"track": "ITUNES_TRACK_ID", "artist": "ITUNES_ARTIST_ID", "album": "ITUNES_ALBUM_ID"},
|
|
{"artist": "itunes_artist_id", "album": "itunes_album_id", "track": "itunes_track_id"},
|
|
),
|
|
(
|
|
"deezer",
|
|
{"track": "DEEZER_TRACK_ID", "artist": "DEEZER_ARTIST_ID", "album": None},
|
|
{"artist": "deezer_id", "album": "deezer_id", "track": "deezer_id"},
|
|
),
|
|
(
|
|
"hydrabase",
|
|
{"track": None, "artist": None, "album": None},
|
|
{"artist": "soul_id", "album": "soul_id", "track": "soul_id", "track_album": "album_soul_id"},
|
|
),
|
|
(
|
|
"discogs",
|
|
{"track": None, "artist": None, "album": None},
|
|
{"artist": "discogs_id", "album": "discogs_id", "track": None},
|
|
),
|
|
],
|
|
)
|
|
def test_source_tag_and_library_column_mappings(source, expected_tags, expected_columns):
|
|
assert get_source_tag_names(source) == expected_tags
|
|
assert get_library_source_id_columns(source) == expected_columns
|
|
|
|
|
|
def test_get_import_source_ids_prefers_nested_source_specific_ids():
|
|
context = normalize_import_context(
|
|
{
|
|
"source": "deezer",
|
|
"artist": {"deezer_id": "deezer-artist-1"},
|
|
"album": {"deezer_id": "deezer-album-1"},
|
|
"track_info": {"deezer_id": "deezer-track-1"},
|
|
"original_search_result": {"source_artist_id": "deezer-artist-1"},
|
|
}
|
|
)
|
|
|
|
assert get_import_source_ids(context) == {
|
|
"track_id": "deezer-track-1",
|
|
"artist_id": "deezer-artist-1",
|
|
"album_id": "deezer-album-1",
|
|
}
|
|
|
|
|
|
def test_get_import_source_ids_prefers_spotify_ids_over_numeric_fallbacks():
|
|
context = normalize_import_context(
|
|
{
|
|
"source": "spotify",
|
|
"artist": {
|
|
"id": "396753",
|
|
"spotify_artist_id": "sp-artist-1",
|
|
"deezer_id": "396753",
|
|
},
|
|
"album": {
|
|
"id": "284076172",
|
|
"spotify_album_id": "sp-album-1",
|
|
"deezer_id": "284076172",
|
|
},
|
|
"track_info": {
|
|
"id": "1607091752",
|
|
"spotify_track_id": "sp-track-1",
|
|
"deezer_id": "1607091752",
|
|
},
|
|
}
|
|
)
|
|
|
|
assert get_import_source_ids(context) == {
|
|
"track_id": "sp-track-1",
|
|
"artist_id": "sp-artist-1",
|
|
"album_id": "sp-album-1",
|
|
}
|
|
|
|
|
|
def test_build_import_album_info_uses_normalized_album_context():
|
|
context = normalize_import_context(
|
|
{
|
|
"source": "deezer",
|
|
"artist": {"name": "Artist One"},
|
|
"album": {
|
|
"name": "Album One",
|
|
"image_url": "https://img.example/album.jpg",
|
|
"release_date": "2024-05-01",
|
|
"total_tracks": 8,
|
|
"album_type": "album",
|
|
},
|
|
"track_info": {
|
|
"name": "Song One",
|
|
"track_number": 4,
|
|
"disc_number": 2,
|
|
"duration_ms": 240000,
|
|
"artists": [{"name": "Artist One"}],
|
|
},
|
|
"original_search_result": {
|
|
"title": "Song One",
|
|
"album": "Album One",
|
|
"clean_title": "Song One",
|
|
"clean_album": "Album One",
|
|
"clean_artist": "Artist One",
|
|
},
|
|
}
|
|
)
|
|
|
|
album_info = build_import_album_info(context)
|
|
|
|
assert album_info["is_album"] is True
|
|
assert album_info["album_name"] == "Album One"
|
|
assert album_info["track_number"] == 4
|
|
assert album_info["disc_number"] == 2
|
|
assert album_info["clean_track_name"] == "Song One"
|
|
assert album_info["album_image_url"] == "https://img.example/album.jpg"
|
|
assert album_info["source"] == "deezer"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"album_name,track_name,artist_name",
|
|
[
|
|
("Song One", "Song One", "Artist One"),
|
|
("Artist One", "Different Song", "Artist One"),
|
|
],
|
|
)
|
|
def test_detect_album_info_web_returns_none_for_ambiguous_album_names(album_name, track_name, artist_name):
|
|
context = normalize_import_context(
|
|
{
|
|
"source": "deezer",
|
|
"artist": {"name": artist_name},
|
|
"album": {"name": album_name, "total_tracks": 12, "album_type": "album"},
|
|
"track_info": {"name": track_name, "track_number": 4, "disc_number": 1},
|
|
"original_search_result": {
|
|
"title": track_name,
|
|
"clean_title": track_name,
|
|
"clean_album": album_name,
|
|
"clean_artist": artist_name,
|
|
},
|
|
}
|
|
)
|
|
|
|
assert detect_album_info_web(context) is None
|
|
|
|
|
|
def test_detect_album_info_web_forces_album_when_track_and_artist_differ():
|
|
context = normalize_import_context(
|
|
{
|
|
"source": "deezer",
|
|
"artist": {"name": "Artist One"},
|
|
"album": {
|
|
"name": "Album One",
|
|
"image_url": "https://img.example/album.jpg",
|
|
"release_date": "2024-05-01",
|
|
"total_tracks": 1,
|
|
"album_type": "album",
|
|
},
|
|
"track_info": {
|
|
"name": "Song One",
|
|
"track_number": 4,
|
|
"disc_number": 2,
|
|
"duration_ms": 240000,
|
|
"artists": [{"name": "Artist One"}],
|
|
},
|
|
"original_search_result": {
|
|
"title": "Song One",
|
|
"album": "Album One",
|
|
"clean_title": "Song One",
|
|
"clean_album": "Album One",
|
|
"clean_artist": "Artist One",
|
|
},
|
|
}
|
|
)
|
|
|
|
album_info = detect_album_info_web(context)
|
|
|
|
assert album_info is not None
|
|
assert album_info["is_album"] is True
|
|
assert album_info["confidence"] == 0.5
|
|
assert album_info["album_name"] == "Album One"
|
|
assert album_info["track_number"] == 4
|
|
assert album_info["disc_number"] == 2
|
|
|
|
|
|
def test_get_import_source_reads_underscore_source_from_nested_dicts():
|
|
"""Netti93 multi-artist fix: many track payloads carry '_source' (the
|
|
discography/wishlist dicts) or 'source' only inside track_info (search
|
|
results). get_import_source must resolve all of them — previously only
|
|
the context-level keys worked, so direct downloads resolved '' and
|
|
source-specific metadata logic never ran."""
|
|
from core.imports.context import get_import_source
|
|
|
|
assert get_import_source({"track_info": {"source": "deezer"}}) == "deezer"
|
|
assert get_import_source({"track_info": {"_source": "deezer"}}) == "deezer"
|
|
assert get_import_source({"original_search_result": {"_source": "itunes"}}) == "itunes"
|
|
assert get_import_source({"_source": "tidal"}) == "tidal"
|
|
# context-level 'source' still wins over nested
|
|
assert get_import_source({"source": "spotify", "track_info": {"_source": "deezer"}}) == "spotify"
|
|
assert get_import_source({}) == ""
|