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:
2026-08-14 11:21:50 -04:00
co-authored by Claude Opus 5
parent 01996d5fe2
commit 6fad0247ee
10 changed files with 878 additions and 5 deletions
+15 -2
View File
@@ -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