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>
This commit is contained in:
184
tests/test_itunes_importer.py
Normal file
184
tests/test_itunes_importer.py
Normal file
@ -0,0 +1,184 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user