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
+40
View File
@@ -1,5 +1,45 @@
## Done
### Round 38 (2026-08-20) — One merge window, and play counts that can't conflict (v0.9.0)
Fifteen "Synced changes merged" windows were stacked on the desktop. Two
independent bugs, and the `.resolved/` backups said which mattered: every one of
the last nine `library.json` merges was ~98% play counts (43-47 of ~45 changed
tracks) plus exactly one real edit — the same single rating each time. The same
~45 tracks disagreed in *every* merge and the count only crept down (48 → 44 over
a day), so `max()` had been quietly discarding plays for at least that long.
- [x] **One window, not one per merge.** `show_conflict_summary` built a fresh
`ConflictSummaryDialog` each time and only reassigned the attribute — the
old dialog stayed a live child of the window. Since
`check_for_external_changes` runs `resolve_conflicts` on every Syncthing
watch tick, that was one window per merge for the life of the process. The
dialog is now a session log: `add_event()` folds each merge in as its own
timestamped entry (newest first), the restore button names the merge it
would undo, and `finished` clears MainWindow's reference so a close lets
the next merge open a fresh one.
- [x] **Per-machine play journals.** `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 all journals. Only the owner
writes its journal, so play data can't conflict; 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, so the version-skew window is safe too. On the real 21k
library: three plays wrote **83 bytes** and left `library.json` untouched,
where before each one rewrote 15 MB.
- [x] **The two hazards, pinned by tests.** `save_tracks` must write the base and
`PlayJournal.load` must be handed a base-valued library — the second is why
`reload_from_disk` folds the journals onto the `disk` copy *before*
reconciling, or the max() against the effective in-memory count would win
by exactly this machine's journal.
- [x] Journal conflicts merge by highest total rather than newest-wins. They
shouldn't be possible; if a filesystem oddity makes one, falling through to
`_keep_newer` would discard a machine's whole history.
Deliberately not done: lossless merges still open the window rather than
demoting to a status-bar line. With journals landing, a merge stops being
routine — the ones left are real edits, and worth seeing.
### Round 37 (2026-08-19) — Export a playlist: a folder, or a whole website (v0.8.0)
`File → Export Playlist…`, and the same item on a playlist's right-click menu.