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>
110 lines
6.0 KiB
Markdown
110 lines
6.0 KiB
Markdown
# LinTunes — task board
|
||
|
||
Legend: `[ ]` todo · `[~]` in progress · `[x]` done.
|
||
When a round closes, move its finished items to `tasks-done.md`.
|
||
|
||
## Round 17 (current) — TASKS.md batch + cruft cleanup
|
||
|
||
Plan reference: `~/.claude/plans/can-you-take-a-drifting-sonnet.md`.
|
||
Tests land in `tests/test_round17.py`.
|
||
|
||
### 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
|
||
|
||
### 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.
|
||
|
||
### 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)
|
||
|
||
### Phase 8
|
||
- [x] `TableSettingsMixin`: dedupe sort/columns/width persistence between
|
||
LibraryView and PlaylistView (new `gui/table_settings.py`)
|
||
|
||
## Standalone maintenance (manual, not code work)
|
||
|
||
- [ ] 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
|
||
|
||
- [ ] 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.
|