v0.12.0: the merge report stops reporting things you didn't do

"unplayed", "missed and never skipped" and "top 100 in past year" showed up in
the merge window constantly, and nobody had touched their rules. A live smart
playlist's membership is derived, but it was being persisted — and
recompute_smart_playlist rewrites it (and bumped date_modified) every time a
play count moves. Both machines did that against the same file after every
song, so Syncthing conflicted on a list the next load throws away and rebuilds
anyway. Membership now stays in memory: save_playlist writes track_ids: [] for
a live smart playlist, and the recompute passes touch=False so it marks nothing
dirty and moves no timestamp. live_update=False and unsupported criteria are
unchanged — their track_ids are a snapshot, which is real content. Since
_mark_playlist also dirties the metadata, library_metadata.json stops being
rewritten every song too.

The rest was presentation. Every summary read at the same weight, so someone
resizing a column on the other machine popped and raised the same window as a
21-track reconciliation, described as "Library columns/settings taken from the
most recently edited copy." Summaries now carry a level — WARNING for a merge
that couldn't resolve cleanly or discarded a side, CHANGE for real content,
INFO for cosmetic or derived — and the dialog has a Show: selector that filters
to one level and above. It opens at the highest level in the batch, so nothing
routine steals focus and a blank window can't happen; a later warning always
pulls the view back up to it. The wording says what happened instead of how the
merge works: a metadata merge names the keys that actually differed, and
adopting the other machine's music folder grades CHANGE rather than INFO,
because that one is a setting somebody chose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9ZEFi4qNJ39FMiBtiAxy2
This commit is contained in:
2026-08-25 14:53:37 -04:00
co-authored by Claude Opus 5
parent a61cbaa369
commit 521db2f81d
9 changed files with 631 additions and 35 deletions
+17 -1
View File
@@ -64,7 +64,13 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
`Playlist.date_modified` — bumped *only* in
`LibraryManager._set_track_ids` — not by the file's mtime, which moves for
cosmetic reasons. `library_manager._reconcile_playlist` uses the same helper
for the live-reload path.
for the live-reload path. Since Round 42 every `ConflictSummary` carries a
**`level`** (`WARNING` / `CHANGE` / `INFO`): a merge that only reconciled
column widths, or a smart playlist whose rules are byte-identical on both
sides, is `INFO` and must never read as an edit the user made. The dialog
(`gui/conflict_dialog.py`) shows one level *and above* and opens at the
highest level in the batch, so a routine merge never steals focus but a
blank window is impossible either.
- **`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
@@ -233,6 +239,16 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
change; `track_table._on_section_resized` ignores that section, or resizing
the window would rewrite the open playlist's JSON (and hand Syncthing a
conflict) purely for a width `apply_settings` overrides on load anyway.
Round 42 is the same rule one level down: a **live smart playlist's membership
is never persisted** (`Playlist.has_derived_membership` gates it in
`json_storage.save_playlist`, which writes `track_ids: []` +
`derived_membership: true`) and `recompute_smart_playlist` passes
`touch=False` to `_set_track_ids` so it marks nothing dirty and moves no
timestamp. Membership is rebuilt from the criteria on every load
(`recompute_all_smart`), so storing it only bought a conflict per song — one
per finished track, on the same file, from both machines. The exceptions are
`live_update=False` and `unsupported` criteria: their `track_ids` *are* the
content (a snapshot), so they still persist and still count as edits.
- **Qt/Wayland gotchas (GNOME/Mutter):** `QDrag.setPixmap` / `setDragCursor` /
`QCursor.pos()` are unreliable during a drag — `gui/drag_ghost.py` paints its