"""Round 63: groundwork for Cassette that isn't Cassette. * Every individual import stamps *date added = now*, even for a file that is years old. The iTunes XML migration is the exception: it recreates a library rather than adding to one, so it keeps the dates the XML carries. * A brand-new library (a fresh data dir, or a fresh iTunes import) starts with a "New Tracks" smart playlist: added in the last 3 months, newest first. Its persistent id is fixed, so two machines seeding the same unsynced share merge into one playlist rather than two. * Buttons get a very slight emboss, computed from the live palette. """ import os import plistlib import shutil from datetime import datetime, timedelta, timezone from PyQt6.QtGui import QColor from lintunes import theme from lintunes.importers.file_importer import import_paths from lintunes.library_manager import LibraryManager from lintunes.models import PlaylistType, Track from lintunes.models.library import Library from lintunes.seed_playlists import ( NEW_TRACKS_NAME, NEW_TRACKS_PID, new_tracks_playlist, seed_new_library, ) from lintunes.smart import evaluate from lintunes.storage.json_storage import load_library def _utc_now(): return datetime.now(timezone.utc).replace(tzinfo=None) # ---- date added ---- class TestDateAdded: def test_import_stamps_now_even_for_an_old_file(self, qapp, tmp_path, mp3_file): old = tmp_path / "old.mp3" shutil.copyfile(mp3_file, old) ancient = datetime(2004, 5, 1).timestamp() os.utime(old, (ancient, ancient)) manager = LibraryManager(Library(), tmp_path / "data") before = _utc_now() - timedelta(seconds=1) [track] = import_paths([old], tmp_path / "Music", manager) added = datetime.fromisoformat(track.date_added) assert before <= added <= _utc_now() + timedelta(seconds=1) def test_itunes_migration_keeps_its_dates(self, tmp_path): from lintunes.main import run_import xml = tmp_path / "Library.xml" with open(xml, "wb") as fh: plistlib.dump({ "Music Folder": "file:///Volumes/M/iTunes%20Media/", "Tracks": {"7": { "Track ID": 7, "Name": "Old Song", "Persistent ID": "P7", "Track Type": "File", "Date Added": datetime(2009, 3, 4, 5, 6, 7), "Location": "file:///Volumes/M/iTunes%20Media/Music/a.mp3", }}, "Playlists": [], }, fh) data = tmp_path / "data" run_import(xml, str(tmp_path / "media"), data) library = load_library(data) assert library.tracks[7].date_added.startswith("2009-03-04") # ---- New Tracks ---- class TestNewTracks: def test_shape(self): playlist = new_tracks_playlist() assert playlist.name == NEW_TRACKS_NAME assert playlist.persistent_id == NEW_TRACKS_PID assert playlist.playlist_type == PlaylistType.SMART [rule] = playlist.smart_criteria.root.children assert (rule.field, rule.operator, rule.value, rule.unit) == \ ("date_added", "in_last", 3, "months") assert playlist.settings.sort_column == "date_added" assert playlist.settings.sort_ascending is False def test_matches_only_recent_tracks(self): now = _utc_now() recent = Track(track_id=1, date_added=(now - timedelta(days=20)).isoformat()) stale = Track(track_id=2, date_added=(now - timedelta(days=200)).isoformat()) criteria = new_tracks_playlist().smart_criteria assert evaluate(criteria, [recent, stale]) == [1] def test_seed_is_idempotent(self): playlists = {} assert len(seed_new_library(playlists)) == 1 assert seed_new_library(playlists) == [] assert list(playlists) == [NEW_TRACKS_PID] def test_manager_seed_persists(self, qapp, tmp_path): manager = LibraryManager(Library(), tmp_path) manager.seed_new_library() manager.flush() reloaded = load_library(tmp_path) assert reloaded.playlists[NEW_TRACKS_PID].name == NEW_TRACKS_NAME assert reloaded.playlists[NEW_TRACKS_PID].is_smart def test_itunes_import_is_seeded(self, tmp_path): from lintunes.main import run_import xml = tmp_path / "Library.xml" with open(xml, "wb") as fh: plistlib.dump({"Music Folder": "file:///M/", "Tracks": {}, "Playlists": []}, fh) run_import(xml, str(tmp_path), tmp_path / "data") assert NEW_TRACKS_PID in load_library(tmp_path / "data").playlists def test_existing_library_is_not_seeded(self): """run_gui seeds only when library.json is absent; pin the predicate by source rather than launching a GUI.""" import inspect from lintunes import main source = inspect.getsource(main.run_gui) assert 'not (data_dir / "library.json").exists()' in source assert "manager.seed_new_library()" in source # ---- emboss ---- class TestEmboss: def test_gradient_keeps_the_color_at_its_middle(self): color = QColor("#808080") gradient = theme.emboss_gradient(color) assert "stop:0.5 #808080" in gradient assert color.lighter(theme.EMBOSS_LIGHTER).name() in gradient assert color.darker(theme.EMBOSS_DARKER).name() in gradient def test_pressed_flips(self): color = QColor("#808080") up = theme.emboss_gradient(color) down = theme.emboss_gradient(color, pressed=True) light = color.lighter(theme.EMBOSS_LIGHTER).name() assert up.index(light) < up.index("stop:1") assert down.index(light) > down.index("stop:1") def test_flat_buttons_opt_out(self): qss = theme.button_emboss_qss(QColor("#808080")) assert "QPushButton:!flat" in qss assert "QPushButton {" not in qss