v0.9.0: play counts that can't conflict

The merge windows kept coming because finishing a track rewrote all 15 MB of
library.json. Both machines did that, so Syncthing saw two edits to one big
file between syncs and produced a conflict file roughly once per song — and
the merge then took max() of the two counts, discarding whichever side had
played less. The .resolved/ backups showed the last nine library.json merges
were ~98% play counts plus exactly one real edit, with the same ~45 tracks
disagreeing every time and the count only creeping down over a day.

library.json now holds only a base count. Each machine owns
plays/<machine-id>.json with its own per-track totals, and the effective count
is base + the sum of every journal. Only the owner writes its journal, so play
data can't conflict; the journal stores totals rather than an append log, so
there's no compaction step to double-count in; and a machine still on older
code keeps bumping its own base, which stays additive with our journal.

Two places carry the whole hazard, and both are pinned by tests: save_tracks
writes journal.base_fields(), never the Track's effective count, and
PlayJournal.load must be given a base-valued library — which is why
reload_from_disk folds the journals onto the disk copy before reconciling.

On the real 21k library: three plays write 83 bytes and leave library.json
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8Mzze7shr5pZoEgKQU5NW
This commit is contained in:
2026-08-20 20:42:07 -04:00
co-authored by Claude Opus 5
parent 5df0af0bae
commit 2d961c0c9e
10 changed files with 556 additions and 18 deletions
+9 -9
View File
@@ -14,10 +14,13 @@ When a round closes, move its finished items to `tasks-done.md`.
Folder + web-mix export, audio.js/jQuery dropped for a dependency-free
player, lossless-only conversion. Round 36 below is still open.
## Round 36 — the merge rework (planned, Round 35 covered the speed half)
## Round 38 (2026-08-20) — merge windows + play journals: done, see tasks-done.md
## Round 36 — the merge rework (the playlist half is still open)
The three symptoms in Round 35 shared a root cause; that round fixed the
performance half. This is the correctness half.
performance half. Round 38 took the play-count and dialog items; the two
playlist items below are what's left.
- [ ] **Playlist merges lose position.** `_merge_playlist` is a 2-way union with
no common ancestor: it takes one side's order wholesale and *appends* the
@@ -31,13 +34,10 @@ performance half. This is the correctness half.
So "most recently edited" often means "most recently resized". Give
`Playlist` a `date_modified` bumped only in `_set_track_ids`, and merge on
that.
- [ ] **`max()` play counts discard concurrent plays.** Base 100, one machine
plays 5 (105), the other plays 3 (103) → merge keeps 105 and those 3 are
gone. Per-machine `plays/<machine-id>.json` journal (machine id kept
outside the synced dir): only its owner ever writes it, so play data can
never conflict, effective count = base + sum of journals, and `library.json`
stops being rewritten every 3 s during playback — which is what generates
the conflicts in the first place.
- [x] **`max()` play counts discard concurrent plays.** Done as described —
per-machine `plays/<machine-id>.json` journals, machine id in the config
dir. Three plays on the real library now write 83 bytes instead of 15 MB,
and `library.json` isn't touched at all.
- [x] **Quiet the merge dialog.** One dialog per session: each merge is folded
in as a timestamped entry (newest first) instead of opening another
window. Fifteen had stacked up. Kept the window for every merge rather