# LinTunes — task board Legend: `[ ]` todo · `[~]` in progress · `[x]` done. When a round closes, move its finished items to `tasks-done.md`. ## Round 21 — transport buttons fill their bubble (v0.1.2) Plan reference: `~/.claude/plans/tap-targets-on-the-snazzy-minsky.md`. Tests in `tests/test_round21.py`. Fix-only round → patch bump **0.1.2**. - [x] Prev/play/next (and shuffle) tap targets now tile their rounded box: `_box(split=True)` in `gui/transport.py` gives each button an equal, full-height share of the bubble with the glyph centered; minimum button widths keep the bubbles at their old footprint (no bigger). ## Round 20 — Ctrl+I save hang on the Debian machine (v0.1.1) Plan reference: `~/.claude/plans/i-m-having-some-trouble-eager-gosling.md`. Tests in `tests/test_round20.py`. Fix-only round → patch bump **0.1.1**. Background: on the Debian machine, OK in Get Info froze the GUI thread past mutter's ~5 s check-alive → "Force Quit / Wait" dialog. Happens on single-track edits. Whole save path is synchronous on the GUI thread. - [x] Perf instrumentation: new `lintunes/perf.py` (`timed()` context manager, INFO on the `lintunes.perf` logger → stderr/journal). Times tag saves, artwork saves, file moves, edit_track(s)_fields, browser rebuild, smart recompute, and the debounced JSON flush. - [x] `tagging.write_tags` now parses + saves the file ONCE per edit: grouping/compilation/bpm ride the same save via registered Easy keys (GRP1/TCMP/TBPM on EasyID3, cpil on EasyMP4) instead of `_write_extra_tags` doing a second full parse+save. - [x] `LibraryView` coalesces browser rebuilds through a 0 ms single-shot timer: an N-track Get Info edit rebuilds the genre/artist/album cascade once, not N times (was O(edited × library)). - [ ] **Diagnose on the Debian machine**: self-update, reproduce a Ctrl+I edit, read `lintunes.perf` timings (terminal run or `journalctl --user`) to see which stage eats the ~5 s — likely the audio-file rewrite or the 15 MB library.json flush. Then decide on moving that stage off the GUI thread (deliberately deferred). ## Round 19 — version display, git self-update, BPM fix Plan reference: `~/.claude/plans/some-fixes-features-for-lintues-merry-journal.md`. Tests land in `tests/test_round19.py`. First versioned release: **0.1.0**. - [x] Version number bottom-left in the status bar: `__version__` in `lintunes/__init__.py` is the single source of truth (setup.py regex-reads it); shown by the new `gui/version_button.py`; tooltip carries the git short hash so two machines on the same version but different commits are distinguishable. - [x] Git self-update: new `lintunes/updater.py` fetches upstream ~10 s after launch and every 4 h (daemon threads, lastfm.py pattern). Commits behind → a `*` on the version button; click → confirm dialog → `git pull --ff-only` → clean quit (flush + player shutdown) → `os.execv` relaunch on the new code (positional file args stripped so they don't re-import). Pull failures (offline / conflicting local edits) surface in the status bar and leave the running app untouched. Not a git checkout / no upstream → button is just a static label. Limitation: a round that adds a pip dependency still needs a manual `pip install -e .` per machine. Pushing `master` is now effectively "releasing" to the other machines. - [x] Status-bar bug found while verifying: the totals label was added with `addPermanentWidget(…, stretch=1)`, which squeezed the transient message area to zero width — every `showMessage` (scrobbles, tag-write errors, import status, sync notices) has been invisible since the label landed in Round 5. Both readouts are now non-permanent widgets: a transient message temporarily replaces them, then they return. - [x] BPM tap button: whole-session averaging — removed the 8-tap rolling window in `tap_tempo.py` (trav's "no rhythm" suspicion was the window, not him). All taps since the session started are averaged; a >2.5 s gap still begins a new session. Verified live under Xvfb: 8 fast + 8 slow taps read the blended overall average, not just the recent 8. - [ ] **verify by eye in the running app**: version reads bottom-left; after the next `git push`, machine 2 shows the `*` within ~10 s of launch and click-to-update pulls + restarts; tap out a real song's BPM and sanity-check the number. ## Round 18 — the 2026-07-02 backlog batch Plan reference: `~/.claude/plans/can-you-knock-out-synthetic-horizon.md`. Tests land in `tests/test_round18.py`. (The equalizer backlog item moved to Parked / deferred — see there.) - [x] Drag tracks from the GNOME file browser into a playlist/Library: the copy-to-`Artist/Album/` + unknown-artist machinery already existed; the gap was that `playlist_view._on_files_dropped` discarded the drop row. Position now threads through `files_dropped` → `MainWindow.import_files` → `import_paths` → `add_tracks_to_playlist(position)`, so dropped files land at the drop line (a file already in the library dedups to its existing track and still inserts in place). - [x] Renaming artist/album/album_artist moves the file so the music folder stays organized like the library (`LibraryManager._maybe_move_file` under `organize_root()` = `/Music`): folders created as needed, Unknown Artist fallback, collision " 1" suffixes, empty dirs pruned (never the root), undo/redo moves the file back/forward, a failed move keeps the edit + old path and reports via `file_move_failed` in the status bar. Files outside the organize root are never moved. Cross-machine: `location` now merges newest-`date_modified`-wins in `conflict_resolver`, covering both sync-conflict files and live `reload_from_disk`. CLAUDE.md's never-move rule updated accordingly. - [x] Visualizer: new "Visualizer (gray mode)" slider in the Preferences gray adjustments sets the dim-mode bar color (untouched = the old derived look); the color/gray/off click cycle is unchanged and the mode now persists across launches (`visualizer_mode` pref). - [ ] **verify by eye in the running app**: drop files from Nautilus into the middle of a manual-sort playlist (land at the drop line, Ctrl+Z removes); Get Info an Unknown Artist track → set artist → watch the file move in Nautilus (Ctrl+Z moves it back; try once while a track is playing); bulk album rename + one Ctrl+Z restores all; drag the visualizer slider while playing in gray mode; cycle modes + relaunch. (A scripted offscreen run of all of the above passed 2026-07-02.) ## Round 17 — 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 - [ ] Equalizer ("maybe just some bass/mid/treble sliders in preferences" — trav 2026-07-02). Deferred from Round 18: QMediaPlayer has no audio-effects hooks, so even a 3-band EQ means either a custom decode→filter→output pipeline (replaces playback; risky for seek/formats) or leaning on the system (PipeWire filter-chain / EasyEffects). Needs a design decision before building. - [ ] 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.