v0.9.1: playlist merges stop losing your track order
What scrambled `a nissa one` took three defects at once. Reorder it on machine A; open it on machine B and resize the window; B's file is now newer, so the merge takes B's order — the old one — wholesale. _merge_playlist was a 2-way union with no common ancestor: one side's order wholesale, the other side's extras appended at the tail, so a track inserted in the middle on one machine arrived at the bottom on the other. merge_track_order re-inserts each side-only track after the nearest track both copies share instead. _reconcile_playlist (the live-reload path) uses the same helper. The union invariant is unchanged and property-tested over 300 random pairs — a merge never drops a track. Whose order wins is now Playlist.date_modified, bumped only in _set_track_ids (add / remove / reorder / undo) and deliberately not by mark_playlist_settings_dirty, with a file-mtime fallback for playlists written before the field existed. The file's mtime was a lie: the last column is stretch-sized, so Qt re-fires sectionResized whenever the viewport width changes, and a window resize or splitter drag rewrote the open playlist's JSON — moving its mtime and handing Syncthing another conflict — for a width apply_settings overrides on load anyway. _on_section_resized now skips that section; genuine drags on every other column still persist. Replayed on a copy of the real playlists: `a nissa one` keeps its reorder against a newer-by-mtime opponent, and `a nissa ideas` takes the other machine's two mid-list inserts at 5 and 12 rather than 26 and 27. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8Mzze7shr5pZoEgKQU5NW
This commit is contained in:
@@ -57,7 +57,14 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
|
||||
`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.
|
||||
take the newest, playlist membership takes the union. Since Round 39 that
|
||||
union is **anchor-based** (`merge_track_order`): a track only one copy has is
|
||||
re-inserted after the nearest track both share, not appended at the tail, so
|
||||
a middle insert stays in the middle. Whose order wins is decided by
|
||||
`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.
|
||||
|
||||
- **`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
|
||||
@@ -187,6 +194,12 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
|
||||
- **Keep `Player` and `track_table` manager-free where they already are** —
|
||||
cross-cutting data is injected via callbacks/signals (e.g. `track_table` takes
|
||||
a `playlists_for_track` callback rather than importing the manager).
|
||||
- **Cosmetic table settings must not look like edits.** The last column is
|
||||
stretch-sized, so Qt re-fires `sectionResized` for it on every viewport width
|
||||
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.
|
||||
|
||||
- **Qt/Wayland gotchas (GNOME/Mutter):** `QDrag.setPixmap` / `setDragCursor` /
|
||||
`QCursor.pos()` are unreliable during a drag — `gui/drag_ghost.py` paints its
|
||||
own child-widget overlay instead. `QAudioOutput` must not be constructed before
|
||||
|
||||
Reference in New Issue
Block a user