v0.35.0: a play history for the yearly Wrapped

The play journals keep running totals per track, which is enough for "most
played ever" and nothing a Wrapped asks: top songs of this year, minutes
listened, which month, what time of day, what got skipped. Those all come
from one timestamped line per listen, so LinTunes now keeps that, while
Preferences -> General -> "Keep a play history for my yearly Wrapped" is on.

history/<machine>/<YYYY-MM>.jsonl, one writer per folder like the journals:
local start time with its offset, ms actually heard (Player adds position
steps under 1.5 s, so seeks and pauses don't count), how it ended, where it
played and from what, and a name/artist/album snapshot that outlives a
delete. Under 5 s is noise and never written.

andTunes 0.3.0 writes the same lines on the Rabbit when sync says the
desktop wants them, and sync copies each month home when the device's copy
is longer. "Share this year's play history with friends" puts a trimmed
copy (no ids, playlists or devices) in each friend's cassette folder, daily
in November and December and monthly otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 16:47:40 -07:00
co-authored by Claude Opus 5.5
parent b3d6c01774
commit ad1d152948
26 changed files with 1148 additions and 15 deletions
+35
View File
@@ -259,6 +259,7 @@ def build(out: Path, seed: int = 47):
shutil.rmtree(data_dir)
json_storage.save_library(library, data_dir)
_write_plays(data_dir, ids)
_write_history(data_dir, library)
_write_prefs(data_dir, library)
shutil.rmtree(covers, ignore_errors=True)
@@ -371,6 +372,40 @@ def _write_plays(data_dir: Path, ids: list):
json.dumps(entries, indent=2), encoding="utf-8")
def _write_history(data_dir: Path, library: Library):
"""A Wrapped-shaped listen history (storage/listen_history.py): a desktop
across three months and a Rabbit across one, with a torn last line on the
Rabbit's — what a sync caught mid-append looks like."""
from datetime import datetime, timedelta, timezone
from lintunes.storage.listen_history import entry_for
tracks = [t for t in library.tracks.values() if t.total_time]
zone = timezone(timedelta(hours=-7))
for machine, src, months, count in (("devbox", "desktop", (8, 9, 10), 40),
("andtunes-dev0r1", "r1", (10,), 15)):
folder = data_dir / "history" / machine
folder.mkdir(parents=True, exist_ok=True)
for month in months:
lines = []
when = datetime(2026, month, 1, 8, 0, tzinfo=zone)
for _ in range(count):
track = random.choice(tracks)
finished = random.random() < 0.7
heard = (track.total_time if finished
else random.randint(6000, max(6001, track.total_time)))
entry = entry_for(track, started=when, heard_ms=heard,
how="finish" if finished else "skip", src=src,
ctx="" if src == "r1" else "library",
track_id=track.track_id)
if entry: # clips under 5 s heard are noise, as in the app
lines.append(json.dumps(entry, ensure_ascii=False,
separators=(",", ":")))
when += timedelta(hours=random.randint(1, 30))
text = "\n".join(lines) + "\n"
if src == "r1":
text += '{"t":"2026-10-3'
(folder / f"2026-{month:02d}.jsonl").write_text(text, encoding="utf-8")
def _write_prefs(data_dir: Path, library: Library):
(data_dir / "preferences.json").write_text(json.dumps({
"ui_scale": "medium",