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:
@@ -1,5 +1,53 @@
|
||||
## Done
|
||||
|
||||
### Round 33 (2026-08-14) — Delete songs from the library (v0.6.0)
|
||||
|
||||
An iTunes 12 habit LinTunes had no answer for: getting rid of a song you don't
|
||||
want to keep. `LibraryManager` had `add_track` but no inverse, and nothing in the
|
||||
app had ever deleted a local music file. Deletion goes to the desktop trash
|
||||
rather than `unlink`, so it's recoverable both by Ctrl+Z and from the file
|
||||
manager afterwards.
|
||||
|
||||
Two constraints shaped it. The trash is **per-filesystem** — the music sits on a
|
||||
mounted volume, so the right destination is `<topdir>/.Trash-<uid>`, and using
|
||||
the home trash would mean a cross-device copy plus a recorded original path
|
||||
"Restore" can't reach. And playlist cleanup has to be **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.
|
||||
|
||||
- [x] `lintunes/trash.py` — freedesktop Trash spec 1.0 by hand, no new
|
||||
dependency (a new pip dep would need a manual `pip install -e .` on the
|
||||
other machine). Picks the trash by the file's own mount, writes the
|
||||
percent-encoded `.trashinfo` with `O_EXCL` first to claim the name, then
|
||||
renames the file in; a failed rename cleans up the info file. `TrashError`
|
||||
leaves the file untouched.
|
||||
- [x] `LibraryManager.delete_tracks(ids, delete_files=)` — the single funnel.
|
||||
Removes from `library.tracks` and every non-smart playlist, prunes the
|
||||
dirs it emptied, invalidates the exported artwork, and pushes one undo
|
||||
Command covering the whole selection. A file that can't be trashed
|
||||
**keeps its track** rather than leaving an orphaned entry. New
|
||||
`tracks_removed` / `tracks_restored` signals.
|
||||
- [x] Undo restores the file from the trash *and* the track's original position
|
||||
in each playlist; redo re-trashes (re-recording paths, since a redo can
|
||||
land on a different collision-suffixed name). If the trash was emptied
|
||||
behind us, the library entry is restored anyway — a visible broken track
|
||||
beats a silent second loss.
|
||||
- [x] `Player.drop_tracks()` — stops if the deleted track is playing, otherwise
|
||||
keeps the current track's place in the shortened queue. Previously a dead
|
||||
id in the queue made `_load_current` stop everything with an error dialog
|
||||
once the walk reached it.
|
||||
- [x] Context menu gains "Remove from Library" and "Remove from Library and
|
||||
Delete File", in **both** the library and playlist views. No keyboard
|
||||
shortcut — `Del` still means "remove from playlist". Confirmation uses the
|
||||
existing destructive-dialog idiom (DestructiveRole button, Cancel as
|
||||
default) and names the playlists affected.
|
||||
- [x] `tests/test_round33.py` (26 tests) covers the spec details (volume vs home
|
||||
trash, percent-encoding, collision suffixes, no orphaned info file on
|
||||
failure), the manager (playlist cleanup, pruning, partial failure, undo of
|
||||
position, emptied-trash undo) and `drop_tracks`. Verified for real against
|
||||
`/run/media/trav/tummult/.Trash-1000` with a scratch file.
|
||||
|
||||
### Round 32 (2026-08-14) — Custom start times survive a track change (v0.5.2)
|
||||
|
||||
trav reported "Pretty Girls is a Motherfucker" starting at 0:00 despite its
|
||||
|
||||
Reference in New Issue
Block a user