Live multi-machine sync: watch files, reconcile, alert on conflicts
Both machines now reflect each other's changes within a couple seconds, and
genuine Syncthing conflicts auto-merge with a backup and an alert.
- sync_watcher.py: QFileSystemWatcher (debounced, re-arms after atomic renames)
emits a single `changed`; the manager decides if it was external.
- library_manager: `reload_from_disk()` re-reads and reconciles disk into memory
(max play/skip counts, newest edit wins, playlist membership unioned), keeping
local unsaved edits and object identity so open views stay valid, then refreshes
the UI without touching the player. `flush()` records each file's (mtime, size)
so our own writes are never mistaken for an external change. New signals
library_reloaded / conflict_resolved.
- conflict_resolver: back up BOTH sides into .resolved/<ts>/{original,incoming}/
before merging, return list[ConflictSummary], add restore_backup(); runs at
startup and live.
- gui/conflict_dialog.py: modeless summary with Open/Restore backup; main_window
shows it from a status-bar notice and preserves scroll+selection on reload.
Tests: tests/test_round16.py (9) + full suite green (225).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,5 +1,33 @@
|
||||
## Done
|
||||
|
||||
### Round 16 (2026-07-02) — Live multi-machine sync + conflict alerts
|
||||
|
||||
Both machines now reflect each other's changes within a couple seconds, with
|
||||
genuine conflicts auto-merged (backed up first) and surfaced in an alert. See
|
||||
`tests/test_round16.py` (9 tests) + the known limitations below.
|
||||
|
||||
- [x] **Live external-change reload** — new `lintunes/sync_watcher.py`
|
||||
(`QFileSystemWatcher`, 600 ms debounce, re-arms after atomic-rename replaces).
|
||||
`LibraryManager.reload_from_disk()` re-reads and **reconciles** disk into memory
|
||||
(max play/skip counts, newest edit wins, playlist membership unioned), keeping
|
||||
local unsaved edits and object identity so open views stay valid; then refreshes
|
||||
the UI without touching the player. **Self-write suppression:** `flush()` records
|
||||
each file's `(mtime, size)` in `_own_sigs`; the watcher ignores changes matching
|
||||
our own writes so a save never triggers a reload.
|
||||
- [x] **Auto-merge + backups + summaries** — `storage/conflict_resolver.py` now
|
||||
backs up *both* sides into `<data>/.resolved/<timestamp>/{original,incoming}/`
|
||||
before merging, returns `list[ConflictSummary]`, and adds `restore_backup()`.
|
||||
Conflicts are resolved at startup **and live** (via the watcher).
|
||||
- [x] **Alert UI** — `gui/conflict_dialog.py` `ConflictSummaryDialog` (scrollable
|
||||
summary, Open backup folder, Restore pre-merge backup), shown modeless from a
|
||||
status-bar notice so a merge never interrupts playback. Wired in `main_window`
|
||||
(`_on_library_reloaded` preserves scroll+selection; `_on_conflict_resolved`).
|
||||
|
||||
**Known limitations (v2):** a *simultaneous* conflicting playlist edit unions
|
||||
membership, so a deletion made on one side while the other edits the same file can
|
||||
be resurrected (clean, non-simultaneous deletes reload fine) — proper fix is
|
||||
per-playlist tombstones. No per-change accept/refuse yet (whole-file restore only).
|
||||
|
||||
### Round 15 (2026-07-01) — Go live: portable multi-machine paths + final import
|
||||
|
||||
Prepared LinTunes to become the canonical library, synced to a second machine via
|
||||
|
||||
Reference in New Issue
Block a user