v0.6.0: delete songs from the library
Right-click a track (or a multi-selection) for "Remove from Library" or "Remove from Library and Delete File". The second moves the file to the desktop trash rather than unlinking it, so it stays recoverable by Ctrl+Z in-app and by "Restore" from the file manager afterwards. The trash is per-filesystem: music lives on a mounted volume, so the file belongs in <topdir>/.Trash-<uid> with a topdir-relative, percent-encoded Path. Using ~/.local/share/Trash would be a cross-device copy recording an original path "Restore" can't reach. lintunes/trash.py implements the freedesktop spec directly rather than adding a dependency that would need a manual reinstall on the other machine. Playlist cleanup is synchronous with the removal: playlist edits address tracks by row index into track_ids while the view skips ids missing from the library, so a dangling id would desync the two and make a later "Remove from Playlist" hit the wrong track. A file that can't be trashed keeps its track — better a song to delete again than a library entry orphaned from a file still on disk. Player gains drop_tracks() so deleting the playing track stops cleanly instead of erroring out when the queue walk later reaches a dead id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,16 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
|
||||
so the desktop's media keys / now-playing popup control playback. Spacebar and
|
||||
arrow keys are handled locally via `MainWindow.eventFilter`.
|
||||
|
||||
- **`lintunes/trash.py`** — freedesktop.org Trash spec 1.0, hand-rolled (no new
|
||||
dep). The trash is **per-filesystem**: music usually lives on a mounted volume,
|
||||
so the file belongs in `<topdir>/.Trash-<uid>` with a *topdir-relative*,
|
||||
percent-encoded `Path=`, not in `~/.local/share/Trash` (which would be a
|
||||
cross-device copy the file manager can't "Restore"). The `.trashinfo` is
|
||||
created with `O_EXCL` **first** to claim the name atomically, then the file is
|
||||
renamed in; a failed rename unlinks the info file so there's never a
|
||||
half-trashed pair. Raises `TrashError` without touching the file, so a caller
|
||||
can treat failure as "not deleted". Only `LibraryManager` calls it.
|
||||
|
||||
- **`lintunes/device_sync.py`** — one-way playlist sync to the Rabbit R1 (Device
|
||||
menu). The Rabbit mounts via **MTP/gvfs** (a FUSE path under
|
||||
`/run/user/<uid>/gvfs`), not mass storage — so plain file I/O, but never
|
||||
@@ -152,8 +162,11 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
|
||||
inside `LibraryManager.organize_root()` (`<music_folder>/Music`) to keep the
|
||||
tree organized iTunes-style (`_maybe_move_file`; undoable; files outside the
|
||||
root are never moved; the new path syncs cross-machine via the `location`
|
||||
newest-wins merge in `conflict_resolver`). Nothing else may move or rewrite
|
||||
music files. The library JSON is the source of truth for everything else.
|
||||
newest-wins merge in `conflict_resolver`). Since Round 33 the *only* other
|
||||
path is an explicit user delete (`LibraryManager.delete_tracks`), which moves
|
||||
the file to the desktop trash via `trash.py` — never `unlink`, so it stays
|
||||
recoverable. Nothing else may move, rewrite or remove music files. The library
|
||||
JSON is the source of truth for everything else.
|
||||
- **Versioning & self-update:** `__version__` in `lintunes/__init__.py` is
|
||||
the single source of truth (`setup.py` regex-reads it, never imports the
|
||||
package). Claude bumps minor for feature rounds and patch for fix-only
|
||||
|
||||
Reference in New Issue
Block a user