Root cause (#700): the Soulseek album-bundle path downloads whole releases into a private staging dir, then per-track workers claim those files via the staging-match shortcut. When slskd files arrived without ID3 tags (common for FLAC rips), the staging cache fell back to the filename stem as the title — and stems shaped like "Artist - Album - 03 - Title" could not clear the 0.80 title- similarity threshold against the clean Spotify track name. Every track in the album went not_found, the batch ended "failed" in the Downloads UI with an empty queue, and the bundle-downloaded files just sat unused in staging. Fix: in _staging_title_variants, add a trailing-title variant by extracting the segments after a bare track-number block (e.g. "03") between " - " delimiters. Conservative — only fires when a clear digit segment is present, so real song titles with dashes like "Hold Me - Live" are left intact. Generated as an additional variant alongside the existing raw/compacted/feat-stripped/bonus-stripped forms, so behavior on already-matching files is unchanged. Downstream (#698): the album-bundle staging miss pushed every failed track to the wishlist labelled as a playlist track, and a couple of fallback paths in ensure_wishlist_track_format and the slskd-result reconstruction hardcoded album_type='single' / total_tracks=1 on the stored album dict. On wishlist requeue the path builder saw album_type='single' and routed the download through single_path, dumping the file in the Singles tree even though it belonged to an album. (Running Reorganize would fix it because the DB album linkage was still correct, but the file landed in the wrong place first.) Fixes: - new resolve_wishlist_source_type_for_batch() returns 'album' for is_album_download batches; wishlist_failed.py now calls it instead of hardcoding 'playlist' - build_wishlist_source_context() threads album_context / artist_context / is_album_download from the batch into the wishlist row so future requeue logic has authoritative routing data - the non-dict-album fallback in ensure_wishlist_track_format and the slskd-result reconstruction default album_type='album' (and total_tracks=0 = unknown) instead of lying with 'single'/1; the existing setdefault chain handles dict-shaped album data unchanged Tests: - 2 staging-match tests pin the new tail-extraction behavior against a realistic untagged slskd stem, plus a negative test that confirms a dash-in-title without a digit segment still does NOT extract a variant - 2 payload tests pin the album_type='album' default for both fallback paths - 4 processing tests pin resolve_wishlist_source_type_for_batch() and the album-context threading in build_wishlist_source_context() 3974 pass; no behavioural change on already-working flows.
161 lines
5.2 KiB
Python
161 lines
5.2 KiB
Python
from types import SimpleNamespace
|
|
|
|
from core.wishlist import payloads
|
|
|
|
|
|
def test_sanitize_track_data_for_processing_normalizes_artists_and_album():
|
|
track = {
|
|
"name": "Song",
|
|
"album": 123,
|
|
"artists": [{"name": "Artist One"}, "Artist Two", SimpleNamespace(name="Artist Three")],
|
|
}
|
|
|
|
out = payloads.sanitize_track_data_for_processing(track)
|
|
|
|
assert out["album"] == "123"
|
|
assert out["artists"] == ["Artist One", "Artist Two", "namespace(name='Artist Three')"]
|
|
|
|
|
|
def test_get_track_artist_name_prefers_artists_list_then_artist_field():
|
|
assert payloads.get_track_artist_name({"artists": [{"name": "Artist One"}]}) == "Artist One"
|
|
assert payloads.get_track_artist_name({"artist": "Solo Artist"}) == "Solo Artist"
|
|
assert payloads.get_track_artist_name({}) == "Unknown Artist"
|
|
|
|
|
|
def test_ensure_spotify_track_format_preserves_existing_shape():
|
|
track = {
|
|
"id": "sp-1",
|
|
"name": "Song",
|
|
"artists": [{"name": "Artist One"}],
|
|
"album": {"name": "Album", "album_type": "ep", "total_tracks": 4},
|
|
}
|
|
|
|
out = payloads.ensure_spotify_track_format(track)
|
|
|
|
assert out is track
|
|
|
|
|
|
def test_ensure_spotify_track_format_builds_webui_shape():
|
|
track = {
|
|
"name": "Song",
|
|
"artist": "Artist One",
|
|
"album": {"name": "Album One", "release_date": "2024-01-01"},
|
|
"duration_ms": 1234,
|
|
"track_number": 7,
|
|
"disc_number": 2,
|
|
"preview_url": "https://example.test/preview",
|
|
"external_urls": {"spotify": "https://open.spotify.com/track/1"},
|
|
"popularity": 42,
|
|
}
|
|
|
|
out = payloads.ensure_spotify_track_format(track)
|
|
|
|
assert out["name"] == "Song"
|
|
assert out["artists"] == [{"name": "Artist One"}]
|
|
assert out["album"]["name"] == "Album One"
|
|
assert out["album"]["album_type"] == "album"
|
|
assert out["album"]["total_tracks"] == 0
|
|
assert out["source"] == "webui_modal"
|
|
|
|
|
|
def test_ensure_wishlist_track_format_aliases_the_spotify_helper():
|
|
track = {
|
|
"name": "Song",
|
|
"artist": "Artist One",
|
|
"album": {"name": "Album One"},
|
|
}
|
|
|
|
out = payloads.ensure_wishlist_track_format(track)
|
|
|
|
assert out["name"] == "Song"
|
|
assert out["artists"] == [{"name": "Artist One"}]
|
|
assert out["album"]["name"] == "Album One"
|
|
|
|
|
|
def test_extract_spotify_track_from_modal_info_converts_trackresult_like_object():
|
|
track_info = {
|
|
"spotify_track": SimpleNamespace(
|
|
title="Song Two",
|
|
artist="Artist Two",
|
|
album="Album Two",
|
|
)
|
|
}
|
|
|
|
out = payloads.extract_spotify_track_from_modal_info(track_info)
|
|
|
|
assert out["source"] == "trackresult"
|
|
assert out["name"] == "Song Two"
|
|
assert out["artists"] == [{"name": "Artist Two"}]
|
|
assert out["album"]["name"] == "Album Two"
|
|
|
|
|
|
def test_extract_spotify_track_from_modal_info_reconstructs_from_slskd_result():
|
|
track_info = {
|
|
"slskd_result": SimpleNamespace(
|
|
title="Song Three",
|
|
artist="Artist Three",
|
|
album="Album Three",
|
|
)
|
|
}
|
|
|
|
out = payloads.extract_spotify_track_from_modal_info(track_info)
|
|
|
|
assert out["reconstructed"] is True
|
|
assert out["name"] == "Song Three"
|
|
assert out["artists"] == [{"name": "Artist Three"}]
|
|
assert out["album"]["name"] == "Album Three"
|
|
|
|
|
|
def test_ensure_wishlist_track_format_defaults_non_dict_album_to_album_type():
|
|
"""When ``album`` arrives as a non-dict (legacy/reconstruction path) we
|
|
must not stamp ``album_type='single'`` — that lies about the origin
|
|
and routes the wishlist requeue through the single_path template
|
|
instead of album_path, dumping album tracks into the Singles tree.
|
|
Default to 'album' / total_tracks=0 (unknown) so downstream code can
|
|
fall through to the real release-type detection logic."""
|
|
track = {
|
|
"name": "Song",
|
|
"artist": "Artist One",
|
|
"album": "Album From Legacy String",
|
|
}
|
|
|
|
out = payloads.ensure_wishlist_track_format(track)
|
|
|
|
assert out["album"]["name"] == "Album From Legacy String"
|
|
assert out["album"]["album_type"] == "album"
|
|
assert out["album"]["total_tracks"] == 0
|
|
|
|
|
|
def test_extract_spotify_track_from_modal_info_slskd_reconstruction_defaults_to_album():
|
|
"""Slskd-result reconstruction is a last-resort path; defaulting to
|
|
``album_type='single'`` corrupted the requeue routing for album
|
|
batches. Same fix as ensure_wishlist_track_format: default 'album'."""
|
|
track_info = {
|
|
"slskd_result": SimpleNamespace(
|
|
title="Song Three",
|
|
artist="Artist Three",
|
|
album="Album Three",
|
|
)
|
|
}
|
|
|
|
out = payloads.extract_spotify_track_from_modal_info(track_info)
|
|
|
|
assert out["album"]["album_type"] == "album"
|
|
assert out["album"]["total_tracks"] == 0
|
|
|
|
|
|
def test_extract_wishlist_track_from_modal_info_uses_track_data_key():
|
|
track_info = {
|
|
"track_data": {
|
|
"id": "track-1",
|
|
"name": "Song Four",
|
|
"artists": [{"name": "Artist Four"}],
|
|
"album": {"name": "Album Four"},
|
|
}
|
|
}
|
|
|
|
out = payloads.extract_wishlist_track_from_modal_info(track_info)
|
|
|
|
assert out["id"] == "track-1"
|
|
assert out["name"] == "Song Four"
|
|
assert out["artists"] == [{"name": "Artist Four"}]
|