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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user