Store track paths relative to the data dir for multi-machine sync
The library is going live as the canonical store, synced to a second machine via Syncthing. Track locations were stored absolute and only remapped at import, so on a second machine (where Syncthing mounts the folder at a different path) every location would break. - lintunes/paths.py: to_relative/to_absolute. Locations are stored relative to the data dir and resolved back on load, applied only at the json_storage boundary (save_tracks/load_library). Track.location stays absolute in memory, so the player, tagging, and art code are unchanged. The data dir and the music move together inside one synced tree, so paths resolve wherever it's mounted — no per-machine music_root config. Absolute paths in older library.json files still load (back-compat). - tests/test_round15.py: helper round-trips, back-compat, and a machine-2 scenario (save under root A, load the copied tree under root B). - TASKS.md/tasks-done.md: mark the data-dir move + real import done; log the benign exit-time Qt/FFmpeg teardown segfault. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,5 +1,62 @@
|
||||
## Done
|
||||
|
||||
### 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
|
||||
Syncthing. See `tests/test_round15.py` (10 tests).
|
||||
|
||||
- [x] **Portable track paths** (`lintunes/paths.py`) — track locations are stored
|
||||
**relative to the data dir** (`to_relative`) and resolved back to absolute on load
|
||||
(`to_absolute`), applied only at the `json_storage` boundary (`save_tracks` /
|
||||
`load_library`). `Track.location` stays absolute in memory, so the player, tagging,
|
||||
and art code are untouched. Because the data dir and the music live inside one
|
||||
synced tree and move together, the library now resolves on any machine no matter
|
||||
where Syncthing mounts the folder — no per-machine `music_root` config. Absolute
|
||||
paths in older `library.json` files still load (back-compat).
|
||||
- [x] **Final import + go live** — fresh import of the current `iTunes Library.xml`
|
||||
into the synced data dir `/run/media/trav/tummult/music/lintunes/` (inside the
|
||||
verified "music" Syncthing share), config saved via `--save-config`, preferences
|
||||
(UI tuning + Last.fm) carried over from the old `./data`, which is retained as a
|
||||
rollback backup. Git remote set to `git.autonomic.zone:2222/trav/lintunes` so the
|
||||
second machine can clone.
|
||||
|
||||
### Round 14 (2026-06-26) — Smart playlists (Phase 1)
|
||||
|
||||
Auto-populating, criteria-driven playlists with full iTunes 12 import. See
|
||||
`tests/test_round14.py` (27 tests, incl. real captured blobs in
|
||||
`tests/smart_blobs.json`).
|
||||
|
||||
- [x] **Criteria model + evaluator** (`lintunes/smart.py`) — recursive
|
||||
`SmartRule`/`SmartGroup`/`SmartLimit`/`SmartCriteria` (JSON round-tripping),
|
||||
a shared `FIELD_REGISTRY` (field → Track attr, type, operators) used by both
|
||||
evaluator and editor, and `evaluate()` (match all/any, nested groups,
|
||||
string/int/duration/rating/date/bool ops, relative "in the last N", and
|
||||
limit-by items/time/size with a "selected by" sort).
|
||||
- [x] **iTunes import** — the binary "Smart Info"/"Smart Criteria" blobs are
|
||||
decoded by a **vendored, MIT-licensed parser** (`lintunes/itunes_smart/`,
|
||||
from github.com/cvzi/itunes_smartplaylist) and converted to our model by
|
||||
`parse_itunes_smart()`. Nested rule groups **parse and evaluate** correctly;
|
||||
blobs we can't represent (MediaKind/iCloud/etc.) flag `unsupported` and keep
|
||||
the imported track snapshot. Validated against all 14 real smart playlists.
|
||||
`loved` is deliberately dropped per user preference.
|
||||
- [x] **Manager** (`library_manager.py`) — `create_smart_playlist` /
|
||||
`set_smart_criteria` (undoable) / `recompute_smart_playlist` /
|
||||
`recompute_all_smart`; field-scoped, coalesced live recompute hooked into the
|
||||
track-edit/play/skip/add funnels (honours each playlist's `live_update`); a
|
||||
no-op equality guard to avoid Syncthing churn; manual add/remove/reorder
|
||||
blocked on smart playlists. `main.py` recomputes on load;
|
||||
`conflict_resolver.py` takes newest criteria (no track_id union) for smart.
|
||||
- [x] **GUI** — `❧ ` glyph on smart rows in the sidebar (with a rename-strip
|
||||
delegate), distinct context menu (New/Edit Smart Playlist), read-only track
|
||||
table for smart playlists, and `SmartPlaylistEditorDialog`
|
||||
(`gui/smart_playlist_dialog.py`): per-field rule rows, match all/any, limits,
|
||||
live-updating. New Smart Playlist on File menu (Ctrl+Alt+N).
|
||||
|
||||
**Phase 2 TODO:** nested-group **editing** UI in the dialog (imported nested
|
||||
playlists are currently shown read-only with a banner; they still update/play).
|
||||
|
||||
### Round 8 (2026-06-19)
|
||||
|
||||
All four implemented in Round 8 (2026-06-19); see tests/test_round8.py.
|
||||
|
||||
- [x] the ability to right click on any track and within the right-click menu that comes up there's an item that says "show in playlist..." with a right arrow. Hover that and you get a list of all the playlists that that track appears in. Select a playlist and the app jumps to that playlist and highlights the first instance of that song in that playlist.
|
||||
|
||||
Reference in New Issue
Block a user