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