v0.25.0: New Tracks, a soft emboss, date added pinned

Groundwork for Cassette (friend library sharing) that stands on its own:

- A brand-new library (fresh data dir, or a fresh iTunes import) starts
  with a "New Tracks" smart playlist: added in the last 3 months, newest
  first. Fixed persistent id, so two machines seeding one unsynced share
  merge into a single playlist.
- Every non-flat button, the sidebar Library button and the transport
  boxes get a very slight top-lit gradient computed from the live palette,
  with the chosen color kept exactly at its midpoint.
- Tests pin that individual imports stamp date added = now even for an
  old file, and that the iTunes migration keeps the XML's dates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 20:56:28 -07:00
co-authored by Claude Opus 5.5
parent da9186420b
commit 6d65ca4050
9 changed files with 275 additions and 11 deletions
+147
View File
@@ -0,0 +1,147 @@
"""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