soulsync/tests/playlists/test_stable_source_track_id.py
BoulderBadgeDad 6e622d30f1 #901: give file-import playlist tracks a stable id so manual matches stick
A Find & Add on a file-import (CSV/M3U/TXT) playlist track was silently dropped and
the track re-appeared as 'extra' (radoslav-orlov). Root cause: unlike Spotify/YouTube
(native ids), file-import + iTunes-only tracks arrive with an EMPTY source_track_id —
and the whole manual-match system keys on it. _persist_find_and_add_match is a no-op
on an empty id, and find_manual_library_match_by_source_track_id returns None for one,
so the match can be neither recorded nor looked up. That's the youtube-vs-file
difference the reporter noticed.

Fix: stable_source_track_id() derives a DETERMINISTIC 'file:<hash>' id from the track
identity (artist|title|album, normalized) when there's no native id; mirror_playlist
assigns it so the SAME song gets the SAME id across re-imports/discovery — exactly
what the match lookup needs. Native ids are used verbatim; bonus: discovery extra_data
now survives a re-import for these tracks too.

Tests: helper (native passthrough, deterministic + case/field-insensitive, distinct
per song, empty-on-no-title, file: prefix); mirror_playlist integration (file tracks
get stable distinct ids, stable across re-import, native ids untouched). 319 playlist/
sync/discovery/mirrored tests green.
2026-06-22 08:53:24 -07:00

45 lines
2.1 KiB
Python

"""#901: a manual match (Find & Add) on a file-import playlist track was silently
dropped and the track re-appeared as "extra". Root cause: file-import / iTunes-only
tracks arrive with an EMPTY source_track_id, and the whole manual-match system keys
on it — an empty key can't be persisted (no-op) or looked up. Fix: derive a stable
deterministic id from the track's identity so matches stick like they do for
Spotify/YouTube (which carry native ids).
"""
from __future__ import annotations
from core.playlists.source_refs import stable_source_track_id
def test_native_id_is_used_verbatim():
assert stable_source_track_id({"source_track_id": "2fdfsGuqb6SBX5ocoBWHUd"}) == "2fdfsGuqb6SBX5ocoBWHUd"
# explicit existing wins over the dict
assert stable_source_track_id({"source_track_id": "x"}, existing="y") == "y"
def test_file_track_gets_a_deterministic_prefixed_id():
t = {"track_name": "Slow Ride", "artist_name": "Foghat", "album_name": "Fool for the City"}
a = stable_source_track_id(t)
assert a.startswith("file:") and len(a) == len("file:") + 16
# SAME song → SAME id across calls/re-imports (what the match lookup needs)
assert stable_source_track_id(dict(t)) == a
def test_identity_is_case_and_field_insensitive_but_distinguishes_songs():
base = {"track_name": "Slow Ride", "artist_name": "Foghat", "album_name": "Fool for the City"}
same = {"name": "slow ride", "artist": "FOGHAT", "album": "fool for the city"} # alt field names + case
assert stable_source_track_id(base) == stable_source_track_id(same)
# a different song gets a different id
assert stable_source_track_id(base) != stable_source_track_id(
{"track_name": "I Just Want to Make Love to You", "artist_name": "Foghat"})
def test_empty_id_when_no_title():
assert stable_source_track_id({"artist_name": "Foghat"}) == ""
assert stable_source_track_id({}) == ""
def test_never_collides_with_a_real_upstream_id():
# the file: prefix keeps synthetic ids out of the spotify/youtube id space
fid = stable_source_track_id({"track_name": "x", "artist_name": "y"})
assert ":" in fid and not fid.replace("file:", "").startswith("file")