# LinTunes — task board Legend: `[ ]` todo · `[~]` in progress · `[x]` done. When a round closes, move its finished items to `tasks-done.md`. ## New backlog (added 2026-07-02, next round) - [ ] we might need an equalizer at some point but I'm not sure how it should show up - [ ] I need to be able to drag a track or tracks from the gnome file browser into a playlist or just into the Library. It should copy the files into the 'itunes music' folder where we store all songs. It should store them where it makes sense for them to go 'artist -> album'. If there is no artist it should go in a standard unknown artist folder. The tracks should show up where they are dragged to in the playlist (if they're being dragged into a playlist) _(Note: window/table drops + copy-to-`Artist/Album/` + unknown-artist fallback already exist — `file_importer.organized_destination`, `MainWindow.dropEvent`, `playlist_view.files_dropped`. Remaining gap to verify/build: inserting at the exact drop **position** in the playlist rather than appending.)_ - [ ] if we rename an artist or album we should move the files. Basically we should keep the music folder organized in the same way the library itself is organized inside lintunes. If we add an artist the track moves from the unknown folder to the artist folder (make a folder if we need to) _(Touches the "never move the user's music files" rule in CLAUDE.md — this would deliberately relax it for metadata renames; needs care around Syncthing + the other machine.)_ - [ ] one of the sliders in the gray adjustments in preferences should be for the color of the equalizer visualizer. This will set the color of the gray when it's in gray mode. We still toggle between color, gray and off when clicking. Also what mode it's in (gray/color/off) should persist between launches of the app. ## 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: silent resume after pause — REDIAGNOSED per trav 2026-07-02: not Bluetooth-specific; happens on the Debian 13 machine whenever he walks away, resumes, and gets no sound despite the visualizer moving (decoding runs, the idle-suspended audio sink comes back dead; a slight rewind fixed it). Two-part fix in `player.py`: (1) `_apply_volume()` re-applied on resume / `setDevice` / `BufferedMedia`; (2) resume after a pause ≥30 s (`RESUME_NUDGE_THRESHOLD_S`) does a **seek-in-place** first — the automated version of the manual rewind, without losing the playback position. - [ ] **verify Task F on the Debian 13 machine**: play, pause, walk away ≥1 min, resume — must be audible without manually 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`). Follow-up 2026-07-02: "Use for All N Songs in Album" button applies the art to every library track on that album, not just the selection. - [ ] 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 ` (read-only re-check) 2. `python3 scripts/recover_artwork.py --data-dir --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.