"""Round 15: portable, machine-independent track paths. Track locations are stored relative to the data dir so a Syncthing-synced library resolves correctly on a second machine that mounts the shared folder at a different absolute path. See lintunes/paths.py and the storage boundary in lintunes/storage/json_storage.py. """ import json import os import shutil from lintunes.paths import to_relative, to_absolute from lintunes.storage.json_storage import save_library, load_library from lintunes.models import Library, Track # --------------------------------------------------------------------------- # # to_relative / to_absolute # --------------------------------------------------------------------------- # class TestPathHelpers: def test_round_trip(self): data_dir = "/run/media/trav/tummult/music/lintunes" loc = "/run/media/trav/tummult/music/iTunes Media/Music/A/Song.m4a" rel = to_relative(loc, data_dir) assert rel == "../iTunes Media/Music/A/Song.m4a" assert to_absolute(rel, data_dir) == loc def test_resolves_under_a_different_root(self): rel = "../iTunes Media/Music/A/Song.m4a" # Same relative path, a different mount point -> a valid local path there. assert to_absolute(rel, "/home/trav/synced/music/lintunes") == ( "/home/trav/synced/music/iTunes Media/Music/A/Song.m4a") def test_empty_passes_through(self): assert to_relative("", "/data") == "" assert to_absolute("", "/data") == "" def test_absolute_stored_path_is_left_alone(self): # Back-compat: libraries written before portable paths kept absolute # locations; those must load unchanged. loc = "/run/media/trav/tummult/music/iTunes Media/x.mp3" assert to_absolute(loc, "/some/other/lintunes") == loc def test_already_relative_location_not_re_relativized(self): assert to_relative("../iTunes Media/x.mp3", "/data/lintunes") == ( "../iTunes Media/x.mp3") # --------------------------------------------------------------------------- # # Storage boundary: JSON is portable, in-memory paths are absolute # --------------------------------------------------------------------------- # def _library(*locations): lib = Library() for i, loc in enumerate(locations, start=1): lib.tracks[i] = Track(track_id=i, name=f"t{i}", location=loc) return lib class TestPortableStorage: def test_saved_json_is_relative(self, tmp_path): data_dir = tmp_path / "music" / "lintunes" loc = str(tmp_path / "music" / "iTunes Media" / "A" / "Song.m4a") save_library(_library(loc), data_dir) raw = json.loads((data_dir / "library.json").read_text()) stored = raw["1"]["location"] assert stored == os.path.join("..", "iTunes Media", "A", "Song.m4a") assert str(tmp_path) not in stored # no machine-specific prefix leaked def test_load_resolves_absolute(self, tmp_path): data_dir = tmp_path / "music" / "lintunes" loc = str(tmp_path / "music" / "iTunes Media" / "A" / "Song.m4a") save_library(_library(loc), data_dir) lib = load_library(data_dir) assert lib.tracks[1].location == loc def test_second_machine_different_mount(self, tmp_path): # Machine 1 writes the library under rootA/music/... root_a = tmp_path / "rootA" data_a = root_a / "music" / "lintunes" loc_a = str(root_a / "music" / "iTunes Media" / "A" / "Song.m4a") save_library(_library(loc_a), data_a) # Syncthing replicates the whole "music" folder to a different path. root_b = tmp_path / "rootB" (root_b).mkdir() shutil.copytree(root_a / "music", root_b / "music") lib_b = load_library(root_b / "music" / "lintunes") assert lib_b.tracks[1].location == str( root_b / "music" / "iTunes Media" / "A" / "Song.m4a") def test_empty_location_round_trips(self, tmp_path): data_dir = tmp_path / "music" / "lintunes" save_library(_library(""), data_dir) lib = load_library(data_dir) assert lib.tracks[1].location == "" def test_absolute_json_still_loads(self, tmp_path): # A pre-portability library.json (absolute paths) must still load. data_dir = tmp_path / "music" / "lintunes" data_dir.mkdir(parents=True) loc = "/elsewhere/iTunes Media/x.mp3" (data_dir / "library.json").write_text(json.dumps( {"1": {"track_id": 1, "name": "t1", "location": loc}})) lib = load_library(data_dir) assert lib.tracks[1].location == loc