Round 17: task batch + cruft sweep (ratings, art download, MPRIS/exit/BT fixes)
Features:
- Rating hover dots: hovering a rating cell shows 5 clickable slots
(RatingDelegate); click slot k sets k stars, clicking the current count
clears. Undoable, library-only (ratings never rewrite music files).
- Search bar moved into the Library header strip, right of the Library
button, so the tracklist top aligns with the playlist tree.
- Right-click "Download Album Art…": iTunes Search API (no key), off-thread
fetch, preview/confirm dialog with Next Result, embeds via write_artwork
+ size refresh, invalidates the MPRIS art cache.
Fixes:
- MPRIS media keys: PropertiesChanged sent invalidated_properties as "av"
instead of "as", so gsd-media-keys dropped it and never MRU-bumped
lintunes (why the play/pause key kept waking stale players). Now an
explicit QDBusArgument string array; loopback-verified sa{sv}as.
- Exit segfault: ordered Player.shutdown() (stop, clear source, detach
buffer/audio outputs) from closeEvent/aboutToQuit; scripted run exits 0.
- BT zero-volume after pause/resume: volume re-applied on resume, device
swap, and BufferedMedia (needs verify on the affected machine).
Cruft sweep:
- Tag writes filtered to EDITABLE_FIELDS; failures logged + surfaced in
the status bar (was a swallowed print).
- O(n²) import fixed (location index + cached max track id); O(1)
refresh/reveal via TrackTableModel row index.
- lastfm: scrobble-queue thread lock, login prefs write marshalled to the
GUI thread, JSON/raise_for_status order fixed.
- Shared read_json/write_json; TableSettingsMixin dedupes view settings.
TASKS.md rewritten as a resumable board; tests in tests/test_round17.py
(251 total pass).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
171
TASKS.md
171
TASKS.md
@ -1,84 +1,109 @@
|
||||
# LinTunes — master task list
|
||||
# LinTunes — task board
|
||||
|
||||
Legend: `[ ]` todo · `[~]` in progress · `[x]` done.
|
||||
When a round closes, move its finished items to `tasks-done.md`.
|
||||
|
||||
## Open / future
|
||||
## Round 17 (current) — TASKS.md batch + cruft cleanup
|
||||
|
||||
- [ ] when hovering the cursor over a song's rating field in the tracklist view it should light up 5 dots. Whichever song is currently hovered over there should show 5 dots and if I click on one it rates it that many stars. And if it already has stars it should only show dots where there aren't stars (so if it's rated 4 stars then there's a dot on the right of the 4 stars). I can click on already rated songs to set the rating.
|
||||
Plan reference: `~/.claude/plans/can-you-take-a-drifting-sonnet.md`.
|
||||
Tests land in `tests/test_round17.py`.
|
||||
|
||||
- [ ] move search to the right of library and the browse comes up to be in line with the top of the playlist list
|
||||
### Phase 1 — cruft & correctness quick wins
|
||||
- [x] 1a. Tag-write filter: `_write_track_tags` only writes fields in
|
||||
`tagging.EDITABLE_FIELDS`; rating/size edits become library-JSON-only
|
||||
and no longer rewrite music files (`library_manager.py`)
|
||||
- [x] 1b. Replace `print()` with logging; new `tag_write_failed` signal
|
||||
surfaced in the status bar (`library_manager.py`, `file_importer.py`,
|
||||
`main_window.py`, `main.py`)
|
||||
- [x] 1c. O(n²) import fix: `{location: track}` index per `import_paths` +
|
||||
cached `_max_track_id` for O(1) `new_track_id` (`file_importer.py`,
|
||||
`library_manager.py`)
|
||||
- [x] 1d. `TrackTableModel._row_by_id` index → O(1) `refresh_track` /
|
||||
`reveal_track` (`track_table.py`)
|
||||
- [x] 1e. `threading.Lock` around the scrobble-queue load/mutate/save
|
||||
(`lastfm.py`)
|
||||
- [x] 1f. Last.fm login: prefs write marshalled to the GUI thread via an
|
||||
internal signal (`lastfm.py`)
|
||||
- [x] 1g. Shared `read_json`/`write_json` in `json_storage.py`; drop the
|
||||
duplicates in `conflict_resolver.py`
|
||||
- [x] 1h. `lastfm._call`: try JSON first, fall back to `raise_for_status()`
|
||||
on non-JSON bodies
|
||||
|
||||
- [~] we need to be able to make smart playlists. They should also be properly imported from itunes. We need all the fields that itunes 12 is able to work with when creating a smart playlist. This is a complicated feature! We also need to be able to edit the criteria of a smart playlist once it is created. Smart playlists should have a little Rotated Floral Heart Bullet (❧) to the left of their title in the playlist list on the left. _(Round 14 — Phase 1 done: full iTunes import (incl. nested groups, which parse + evaluate) and a flat-rule editor. **Phase 2 TODO:** nested-group editing UI in the dialog — until then, imported nested playlists are shown read-only. See tasks-done.md.)_
|
||||
### Phases 2–4 — player & desktop integration
|
||||
- [x] Task F: Bluetooth zero-volume after pause/resume — factored
|
||||
`_apply_volume()`; re-applied on resume, after `setDevice`, and on
|
||||
`BufferedMedia` (`player.py`)
|
||||
- [ ] **verify Task F on the Bluetooth machine**: pause ≥30 s on BT
|
||||
headphones, resume, must be audible without seeking
|
||||
- [x] Task E: exit segfault — idempotent `Player.shutdown()`
|
||||
(stop → clear source → detach buffer/audio outputs → disconnect
|
||||
QMediaDevices), called from `closeEvent` + `aboutToQuit`.
|
||||
Verified 2026-07-02: scripted run (import → play to end → close)
|
||||
exits 0, no segfault.
|
||||
- [x] Task C: MPRIS play/pause commandeering — root cause found & fixed:
|
||||
`_notify` sent `PropertiesChanged` with `invalidated_properties`
|
||||
marshalled as `av` instead of `as`, so gsd-media-keys dropped it and
|
||||
never bumped LinTunes in its media-key MRU. Now an explicit empty
|
||||
string-array via `QDBusArgument` (`mpris.py`). Verified 2026-07-02 by
|
||||
D-Bus loopback: old code's signature was `sa{sv}av`, new is `sa{sv}as`
|
||||
(test_round17).
|
||||
- [ ] **verify Task C in practice**: play in LinTunes with a stale YouTube
|
||||
tab around; the media key should control only LinTunes. Fallbacks if
|
||||
GNOME still misroutes (documented, not built):
|
||||
`org.gnome.SettingsDaemon.MediaKeys.GrabMediaPlayerKeys`,
|
||||
bus-name re-registration on play.
|
||||
|
||||
- [ ] the app should be a little more agressive about comandeering the play/pause button from other media playing on the system. If I was playing a youtube video and that was associated with play/pause, but it's been hours since I played it, and I started playing music in lintunes, I would expect the play/pause button to pause the lintunes music when I hit it— not also start playing a youtube video, you know what I mean?
|
||||
### Phases 5–7 — features
|
||||
- [x] Task B: search bar moved into the Library header strip
|
||||
(`_top_strip`), directly right of the sidebar Library button, so the
|
||||
browse/tracklist top aligns with the playlist tree (`library_view.py`)
|
||||
- [x] Task A: rating hover dots — hovering a rating cell shows 5 slots
|
||||
(★ where rated, • where not); click slot k sets k stars; clicking the
|
||||
current rating clears it. `RatingDelegate` + `rating_edited` signal,
|
||||
wired to `manager.edit_track_fields` in both views (`track_table.py`,
|
||||
`library_view.py`, `playlist_view.py`). Rating is library-only (no
|
||||
music-file rewrite), undoable with Ctrl+Z.
|
||||
- [x] Task D: right-click "Download Album Art…" — iTunes Search API
|
||||
(no key; upscale artworkUrl100 → 600x600), off-thread fetch,
|
||||
confirmation dialog with preview + Next Result, embed via
|
||||
`tagging.write_artwork` + size refresh, invalidate MPRIS art cache
|
||||
(new `art_search.py`, new `gui/album_art_dialog.py`,
|
||||
`track_table.py`, `main_window.py`, `mpris.py`)
|
||||
- [ ] verify Tasks A/B/D by eye in the running app (hover/click ratings,
|
||||
search-bar alignment, art download on a real album)
|
||||
|
||||
- [ ] ability to right-click a song and download album art. Is there a repo we can search to get it? Wikipedia? It should ask for confirmation the art is correct.
|
||||
### Phase 8
|
||||
- [x] `TableSettingsMixin`: dedupe sort/columns/width persistence between
|
||||
LibraryView and PlaylistView (new `gui/table_settings.py`)
|
||||
|
||||
- [ ] Minor: LinTunes segfaults during Qt-Multimedia/FFmpeg pipeline teardown on
|
||||
app **exit** (fires after the GUI is gone — cosmetic, no data risk since writes
|
||||
are atomic). Tidy the shutdown so the media objects are released cleanly.
|
||||
## Standalone maintenance (manual, not code work)
|
||||
|
||||
- [ ] Live-sync v2 (Round 16 shipped v1): per-playlist **tombstones** so a
|
||||
deletion made on one machine while the other edits the same playlist isn't
|
||||
resurrected by the union merge; optional **per-change accept/refuse** review
|
||||
(v1 offers whole-file restore only). Also consider a lighter per-file reload for
|
||||
very large libraries (v1 does a full reload + smart recompute on each external
|
||||
change).
|
||||
- [ ] Artwork recovery against the LIVE data dir (embeds the 790
|
||||
album-propagated covers; gets art-bearing coverage to 100%):
|
||||
1. `python3 scripts/audit_artwork.py --data-dir <data>` (read-only re-check)
|
||||
2. `python3 scripts/recover_artwork.py --data-dir <data> --dry-run` → review
|
||||
3. re-run with `--write`; `audit_artwork.py` again to confirm
|
||||
Note: `lintunes/itc.py` is a live dependency of `recover_artwork.py`
|
||||
(tests in `test_round9.py`) — do not delete as "unused".
|
||||
|
||||
## Parked / deferred
|
||||
|
||||
- [ ] (`tagging.read_embedded_artwork`); it is NOT stored in library.json.
|
||||
|
||||
<!-- Context added by Claude (Opus 4.8), 2026-06-19 — the "migrate 100% of my
|
||||
artwork" question is now ANSWERED & TOOLED, only the grid-view feature remains:
|
||||
- lintunes shows art from embedded tags only; the importer never touched art.
|
||||
- Audited the live library vs iTunes' XML "Artwork Count" + the on-disk
|
||||
`Album Artwork/` cache (scripts/audit_artwork.py, read-only). Result:
|
||||
13,571 tracks already have embedded art; 790 more are recoverable by
|
||||
copying an album-mate's embedded art; 0 cache-only and 0 hard residue;
|
||||
7,015 genuinely have no art; 6 files missing on disk.
|
||||
=> after recovery, 100% of art-bearing tracks (14,361/14,361) are covered.
|
||||
- iTunes' `.itc` cache holds NO art that we can't already recover: `Cache/` is
|
||||
keyed by track persistent_id but only mirrors already-embedded art (0 extra);
|
||||
`Download/Store` art uses an opaque id, but every track it would cover already
|
||||
has art via embed or album-propagation, so residue is 0. No `.itl` parsing or
|
||||
online refetch needed.
|
||||
- TO FINISH MIGRATION (before/after the live import): run
|
||||
python3 scripts/recover_artwork.py --data-dir <data> --write
|
||||
(defaults to --dry-run; embeds the 790 album-propagated covers into the files
|
||||
via tagging.write_artwork). scripts/audit_artwork.py re-checks coverage.
|
||||
- `.itc` decoder lives in lintunes/itc.py (tests/test_round9.py). -->
|
||||
|
||||
- [ ] Make the data dir location a **preference** (currently only settable via
|
||||
`--data-dir` / `--save-config`).
|
||||
|
||||
Artwork migration still to run against the live library (before relying on it):
|
||||
1. `python3 scripts/audit_artwork.py --data-dir <data> --xml "<iTunes XML>"`
|
||||
(read-only) to re-confirm the counts on the freshly-imported library.
|
||||
2. `python3 scripts/recover_artwork.py --data-dir <data> --dry-run`, review,
|
||||
then re-run with `--write` to embed the covers into the files.
|
||||
|
||||
|
||||
|
||||
## done
|
||||
|
||||
- [x] confirm last fm works
|
||||
|
||||
- [x] do do a volume slider, this can some space between the visualizer and the timeline.
|
||||
|
||||
- [x] I'd like in the preferences to have a colors adjustment for each area setting: background, round-rec of buttons, the (currently white) behind the track/artist/album text, the color of that text itself, and the color of the gray of the stripes in the tracklisting. Each of those should have a slider from black to white that appears immediately so the user can tweak it to their liking.
|
||||
|
||||
- [x] visualizer should have more discrete mode that's a light gray on light gray
|
||||
|
||||
- [x] the ability to not just copy/paste a song but also to ctrl-x cut a song from one space and ctrl-v paste it somewhere else. AND when pasting a song(s) it should paste above the currently selected track. That's where it pastes to, not to the end of the playlist.
|
||||
- [x] if the user is dragging a track around and they drag above the top of the track list, the track list should scroll up. Same if they hover the track below the track list at the bottom it scrolls down. The track list should scroll in the direction the user is hovering the track. This helps the user move a track to a place in a playlist that isn't currently seen.
|
||||
|
||||
- [x] **Move data dir** to `/run/media/trav/tummult/music/lintunes/` — done
|
||||
2026-07-01 (fresh import written there, config saved with `--save-config`).
|
||||
Track paths are now stored **relative to the data dir** (`lintunes/paths.py`,
|
||||
applied at the `json_storage` boundary) so the Syncthing-shared library is
|
||||
portable to the second machine regardless of its mount point. See tasks-done.md
|
||||
Round 15.
|
||||
|
||||
- [x] Run the real import on trav's library — done 2026-07-01: 21,382 tracks,
|
||||
462 playlists + 21 folders, 18 smart playlists (3 kept as snapshot), 5 missing
|
||||
files, 1,324 case/unicode path fixes. Imported to the synced data dir; old
|
||||
`./data` kept as a rollback backup until machine 2 is confirmed.
|
||||
- [ ] Smart playlists Phase 2: nested-group editing UI in the criteria
|
||||
dialog (import + evaluation of nested groups already works; imported
|
||||
nested playlists are read-only until then). Big change, on hold.
|
||||
- [ ] Live-sync v2: per-playlist tombstones (so a delete on one machine
|
||||
isn't resurrected by the union merge during a simultaneous edit) +
|
||||
optional per-change accept/refuse review. Low priority — trav doesn't
|
||||
edit on both machines at once; v1 handles sequential use.
|
||||
- [ ] Data-dir location as an in-app preference (currently only
|
||||
`--data-dir` / `--save-config`).
|
||||
- [ ] Album-art grid view: a browsable grid of album covers for the
|
||||
library (the artwork-coverage investigation that motivated it is
|
||||
answered & tooled — see Standalone maintenance above).
|
||||
- [ ] Off-thread artwork reads: `SidebarArt.set_track` and the Get Info
|
||||
dialog read embedded art synchronously on the GUI thread (fine for
|
||||
local files; worth revisiting for large FLACs).
|
||||
- [ ] Progress UI (or background thread) for multi-selection tag writes in
|
||||
`edit_tracks_fields` — a big batch currently blocks the UI.
|
||||
- [ ] More aggressive media-key commandeering beyond the Task C fix, if the
|
||||
real-machine verification shows GNOME still routing keys elsewhere.
|
||||
|
||||
Reference in New Issue
Block a user