import plistlib import tempfile from datetime import datetime from pathlib import Path import pytest from lintunes.importers.itunes_importer import ( import_itunes_xml, make_location_remapper, ) MUSIC_FOLDER = "file:///Volumes/Lucia/iTunes/iTunes%20Media/" MUSIC_ROOT = Path("/run/media/trav/tummult/music/iTunes Media") def _make_itunes_xml(tracks: dict, playlists: list) -> Path: plist_data = { "Major Version": 1, "Minor Version": 1, "Application Version": "12.8.3.1", "Music Folder": MUSIC_FOLDER, "Tracks": tracks, "Playlists": playlists, } tmp = tempfile.NamedTemporaryFile(suffix=".xml", delete=False) plistlib.dump(plist_data, tmp, fmt=plistlib.FMT_XML) tmp.close() return Path(tmp.name) def _track(track_id, name, location_suffix, **extra): return { "Track ID": track_id, "Name": name, "Persistent ID": f"PID{track_id}", "Track Type": "File", "Location": MUSIC_FOLDER + location_suffix, **extra, } class TestLocationRemapper: def test_remaps_music_folder_prefix(self): remap = make_location_remapper(MUSIC_FOLDER, MUSIC_ROOT) assert remap("/Volumes/Lucia/iTunes/iTunes Media/Music/A/B/C.m4a") == \ "/run/media/trav/tummult/music/iTunes Media/Music/A/B/C.m4a" def test_leaves_outside_paths_alone(self): remap = make_location_remapper(MUSIC_FOLDER, MUSIC_ROOT) assert remap("/Volumes/Other/song.mp3") == "/Volumes/Other/song.mp3" def test_localhost_form(self): remap = make_location_remapper( "file://localhost/Users/test/Music/iTunes/iTunes%20Media/", Path("/home/user/Music")) assert remap("/Users/test/Music/iTunes/iTunes Media/Music/X/Y/Z.mp3") == \ "/home/user/Music/Music/X/Y/Z.mp3" class TestItunesImporter: def test_import_basic_track(self): tracks = { "100": _track( 100, "Test Song", "Music/Test%20Artist/Test%20Album/Test%20Song.mp3", Artist="Test Artist", Album="Test Album", Genre="Rock", **{"Total Time": 200000, "Date Added": datetime(2020, 1, 1, 0, 0, 0)}, ) } playlists = [ { "Name": "Library", "Master": True, "Playlist Persistent ID": "MASTER", "Playlist Items": [{"Track ID": 100}], } ] xml_path = _make_itunes_xml(tracks, playlists) library, report = import_itunes_xml(xml_path, MUSIC_ROOT, check_files=False) assert len(library.tracks) == 1 track = library.tracks[100] assert track.name == "Test Song" assert track.artist == "Test Artist" assert track.location == str(MUSIC_ROOT / "Music/Test Artist/Test Album/Test Song.mp3") # Master playlist is a system playlist — not imported assert len(library.playlists) == 0 assert report.skipped_system == 1 assert report.track_count == 1 xml_path.unlink() def test_import_playlists_skips_smart_and_system(self): tracks = { "1": _track(1, "Song A", "Music/A/A/A.mp3"), "2": _track(2, "Song B", "Music/B/B/B.mp3"), } playlists = [ {"Name": "Library", "Master": True, "Playlist Persistent ID": "MASTER", "Playlist Items": [{"Track ID": 1}, {"Track ID": 2}]}, {"Name": "Movies", "Distinguished Kind": 2, "Playlist Persistent ID": "MOVIES"}, {"Name": "Smarty", "Smart Info": b"x", "Smart Criteria": b"y", "Playlist Persistent ID": "SMART1", "Playlist Items": [{"Track ID": 1}]}, {"Name": "My Folder", "Folder": True, "Playlist Persistent ID": "FOLDER1"}, {"Name": "My Playlist", "Playlist Persistent ID": "PL1", "Parent Persistent ID": "FOLDER1", "Playlist Items": [{"Track ID": 2}, {"Track ID": 999}]}, ] xml_path = _make_itunes_xml(tracks, playlists) library, report = import_itunes_xml(xml_path, MUSIC_ROOT, check_files=False) assert set(library.playlists.keys()) == {"FOLDER1", "PL1"} # Track 999 doesn't exist — dropped from the playlist assert library.playlists["PL1"].track_ids == [2] assert library.playlists["PL1"].parent_persistent_id == "FOLDER1" assert report.skipped_smart == 1 assert report.skipped_system == 2 assert report.playlist_count == 1 assert report.folder_count == 1 xml_path.unlink() def test_orphaned_child_detached_when_parent_skipped(self): playlists = [ {"Name": "Inside Smart Folder", "Playlist Persistent ID": "PL1", "Parent Persistent ID": "GONE"}, ] xml_path = _make_itunes_xml({}, playlists) library, _report = import_itunes_xml(xml_path, MUSIC_ROOT, check_files=False) assert library.playlists["PL1"].parent_persistent_id == "" xml_path.unlink() def test_skips_stream_tracks(self): tracks = { "1": {"Track ID": 1, "Name": "Radio", "Track Type": "URL", "Persistent ID": "R1"}, "2": _track(2, "Real Song", "Music/A/A/A.mp3"), } xml_path = _make_itunes_xml(tracks, []) library, report = import_itunes_xml(xml_path, MUSIC_ROOT, check_files=False) assert set(library.tracks.keys()) == {2} assert report.skipped_tracks == 1 xml_path.unlink() def test_unmapped_location_reported(self): tracks = { "1": {"Track ID": 1, "Name": "Elsewhere", "Persistent ID": "E1", "Track Type": "File", "Location": "file:///Volumes/Other/song.mp3"}, } xml_path = _make_itunes_xml(tracks, []) library, report = import_itunes_xml(xml_path, MUSIC_ROOT, check_files=False) assert report.unmapped_locations == ["/Volumes/Other/song.mp3"] assert 1 in library.tracks # still imported, just flagged xml_path.unlink() def test_computed_ratings_dropped(self): tracks = { "1": _track(1, "Guessed", "Music/A/A/A.mp3", **{"Rating": 100, "Rating Computed": True, "Album Rating": 80, "Album Rating Computed": True}), "2": _track(2, "Explicit", "Music/B/B/B.mp3", **{"Rating": 80, "Album Rating": 60}), } xml_path = _make_itunes_xml(tracks, []) library, _report = import_itunes_xml(xml_path, MUSIC_ROOT, check_files=False) assert library.tracks[1].rating == 0 assert library.tracks[1].album_rating == 0 assert library.tracks[2].rating == 80 assert library.tracks[2].album_rating == 60 xml_path.unlink() def test_import_empty_library(self): xml_path = _make_itunes_xml({}, []) library, report = import_itunes_xml(xml_path, MUSIC_ROOT, check_files=False) assert len(library.tracks) == 0 assert len(library.playlists) == 0 assert report.track_count == 0 xml_path.unlink()