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
+26
View File
@@ -104,6 +104,32 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
`journal.base_fields(tid)`, never the `Track`'s effective count. Those two
places are the whole hazard; `tests/test_round38.py` pins both.
- **`lintunes/storage/listen_history.py`** — the yearly Wrapped's raw data
(Round 74). The journals keep *totals*, which can't say "top songs of
2026", minutes listened, month or time of day; this keeps **one line per
listen** in `<data_dir>/history/<machine id>/<YYYY-MM>.jsonl` — the
journals' owner-only-writer rule, append-only, a finished month never
changes. `t` is *local* time with its offset (time-of-day is the listener's
clock); `ms` is time actually **heard** — `Player._account_heard` adds
position steps of 0–1.5 s, so seeks and pauses don't count; `end` is
finish/skip/stop/quit; the artist/album snapshot outlives a delete or
retag. Under 5 s is noise and never written. Only while the synced pref
`wrapped.track_history` is on: `Player.listen_ended` → `MainWindow` (adds
`src` desktop/cast and `ctx`, which it latches at `track_changed` because
`_now_playing_context` moves before the old listen closes) →
`LibraryManager.record_listen`, flushed with the journal. The Rabbit writes
the same lines (`History.java`) only when sync put `"history": true` in its
`library.json`; `andtunes/plays.bring_back_history` copies a month home when
the device's copy is longer (two desktops can both do that, hence the
union merge in `conflict_resolver._merge_listen_history`).
`cassette/wrapped.py` puts this year's **trimmed** log
(`trim_for_sharing`: no id/ctx/src/shuf) in every friend's `out/` as
`wrapped-<year>.json` when `wrapped.share_with_friends` is also on —
checked hourly by `CassetteUi`, written daily in Nov–Dec and monthly
otherwise, never rewritten when the plays didn't change. No Wrapped page
yet: that's built after Spotify's 2026 one drops, plus a Last.fm backfill
for Jan–Sep (scrobbling is on; the R1 never scrobbled).
- **`lintunes/library_manager.py`** — `LibraryManager(QObject)` owns the
`Library`, is the single funnel for all mutations, and persists them
**debounced** (3 s) with per-area dirty tracking (a play-count bump rewrites