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:
@@ -52,12 +52,27 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
|
||||
hex `persistent_id` (folder containment via `parent_persistent_id`).
|
||||
|
||||
- **`lintunes/storage/json_storage.py`** — the library is **multiple files** in
|
||||
the data dir: `library.json` (all tracks), `library_metadata.json`, and one
|
||||
`playlists/<persistent_id>.json` per playlist. Writes are atomic (`*.json.tmp`
|
||||
the data dir: `library.json` (all tracks), `library_metadata.json`, one
|
||||
`playlists/<persistent_id>.json` per playlist, and one
|
||||
`plays/<machine-id>.json` per machine. Writes are atomic (`*.json.tmp`
|
||||
→ rename). **`storage/conflict_resolver.py`** merges Syncthing
|
||||
`*.sync-conflict-*` files on startup: play counts take the max, edited fields
|
||||
take the newest, playlist membership takes the union.
|
||||
|
||||
- **`lintunes/storage/play_journal.py`** — why play counts can't conflict. Since
|
||||
Round 38 `library.json` holds only a **base** count and each machine owns
|
||||
`plays/<machine-id>.json` with *its own* per-track totals; effective count =
|
||||
base + the sum of every journal. Only the owner ever writes its journal, so
|
||||
two machines never touch the same file — and finishing a track no longer
|
||||
rewrites 15 MB, which is what handed Syncthing a conflict once per song.
|
||||
Totals, not an append log, so there is no compaction step to double-count in.
|
||||
`PlayJournal.load()` must be handed a library whose tracks still carry **base**
|
||||
counts (the fresh load at startup, the `disk` copy inside `reload_from_disk`) —
|
||||
folding an already-folded library promotes the effective count to base.
|
||||
Mirror-image rule in `json_storage.save_tracks()`: it writes
|
||||
`journal.base_fields(tid)`, never the `Track`'s effective count. Those two
|
||||
places are the whole hazard; `tests/test_round38.py` pins both.
|
||||
|
||||
- **`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
|
||||
|
||||
Reference in New Issue
Block a user