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:
@@ -1,5 +1,35 @@
|
||||
## Done
|
||||
|
||||
### Round 39 (2026-08-20) — Playlist merges stop losing your track order (v0.9.1)
|
||||
|
||||
The playlist half of the Round 36 rework, and what actually scrambled
|
||||
`a nissa one`. It took three defects together: reorder it on machine A, open it
|
||||
on machine B and resize the window, and B's file is newer, so the merge takes
|
||||
B's order — the old one — wholesale.
|
||||
|
||||
- [x] **Anchor-based merge.** `merge_track_order` in `conflict_resolver` unions
|
||||
two orderings by re-inserting each side-only track after the nearest track
|
||||
both copies share, instead of appending it at the tail. `_merge_playlist`
|
||||
and `library_manager._reconcile_playlist` (the live-reload path) both use
|
||||
it. The union invariant is unchanged and property-tested over 300 random
|
||||
pairs: a merge never drops a track, duplicates and disjoint lists included.
|
||||
- [x] **`Playlist.date_modified`, bumped only in `_set_track_ids`** — the single
|
||||
funnel for add / remove / reorder / undo, and deliberately *not*
|
||||
`mark_playlist_settings_dirty`. `_playlist_newer` prefers it over the
|
||||
file's mtime, falling back to mtime for playlists written before the field
|
||||
existed. The smart-playlist branch uses the same test, which told the same
|
||||
lie.
|
||||
- [x] **A window resize no longer rewrites the playlist.** The last column is
|
||||
stretch-sized, so Qt re-fires `sectionResized` for it whenever the viewport
|
||||
width changes — a resize or a splitter drag rewrote the open playlist's
|
||||
JSON, moved its mtime, and handed Syncthing another conflict, all for a
|
||||
width `apply_settings` overrides on load. `_on_section_resized` skips that
|
||||
section; genuine drags on every other column still persist.
|
||||
|
||||
Replayed against the real playlists on a copy: on `a nissa one` (23 tracks) the
|
||||
reorder survives a newer-by-mtime opponent, and on `a nissa ideas` (26 tracks)
|
||||
the other machine's two mid-list inserts land at 5 and 12 rather than 26 and 27.
|
||||
|
||||
### 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
|
||||
|
||||
Reference in New Issue
Block a user