Files
lintunes/tests/test_itunes_importer.py
Trav f6d35fa594 Import existing LinTunes project
Snapshot of the existing codebase before working through the TASKS.md
backlog. Real library data (data/) and the iTunes import fixture
(itunes-test-library/) are gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:12:01 -04:00

185 lines
7.1 KiB
Python

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()