Files
lintunes/TASKS.md
trav 8dc33fa65e 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>
2026-07-02 11:36:11 -04:00

110 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 24 — 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 57 — 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.