moved tasks to projects board
This commit is contained in:
@@ -8,8 +8,9 @@ LinTunes is an iTunes-replacement music library manager and player for Linux,
|
||||
built with PyQt6 / Qt Multimedia. It imports an iTunes 12 library, stores the
|
||||
library as plain JSON (syncable via Syncthing), and reproduces the iTunes UI
|
||||
(left sidebar + right playlist view, column browser, customizable per-playlist
|
||||
columns). `spec.md` is the original design brief; `TASKS.md` is the live backlog
|
||||
and `tasks-done.md` records completed work.
|
||||
columns). `spec.md` is the original design brief. Tasks live on the kanban board at
|
||||
`~/Documents/projtracker/projects/lintunes/` (use the `tasks` skill); the old
|
||||
`TASKS.md`/`tasks-done.md` are archived there — don't recreate them.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -336,7 +337,7 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
|
||||
|
||||
- **`lintunes/andtunes/`** — the desktop half of andTunes, a music player for
|
||||
the Rabbit R1 (Round 47; the app itself lives in `andtunes/` at the repo
|
||||
root, board in `andtunes/TASKS.md`). The premise: **the app never scans
|
||||
root, R1 measurements in `andtunes/DEVICE.md`, tasks tagged `andtunes` on the board). The premise: **the app never scans
|
||||
anything.** Auxio re-reads Android's MediaStore on every launch, which is
|
||||
the "your songs will show up here" hang — but LinTunes already knows the
|
||||
artist, album and year of every file it just copied, so it writes them into
|
||||
|
||||
@@ -1,582 +0,0 @@
|
||||
# LinTunes — task board
|
||||
|
||||
Legend: `[ ]` todo · `[~]` in progress · `[x]` done.
|
||||
When a round closes, move its finished items to `tasks-done.md`.
|
||||
|
||||
|
||||
|
||||
# tasks
|
||||
|
||||
- [ ] archive the done tasks in here to another file, this is crufty....
|
||||
|
||||
## Round 49 (2026-09-10) — Import from URL: done, see tasks-done.md
|
||||
|
||||
File ▸ Import from URL… (the `song` helper in-process), plus the album art
|
||||
opening on one click, art-shaped and screen-tall. Left on the table:
|
||||
|
||||
- [ ] **Machine 2 needs yt-dlp.** A runtime CLI tool, not a pip dep:
|
||||
`pip install --user yt-dlp` (and ffmpeg, which it already has for
|
||||
Qt). The menu says so if it's missing.
|
||||
- [ ] **Importing the same link twice makes a second copy.** The importer
|
||||
dedups by file location, and a fresh download has a fresh path. The
|
||||
yt-dlp id (the `[jNQXAC9IVRw]` in the filename) is a stable key to
|
||||
check against before downloading.
|
||||
- [ ] **"Stop Identifying (N left)" doesn't count songs still downloading.**
|
||||
The queue only knows about songs that have landed.
|
||||
- [ ] **The art window's title-bar allowance is a fixed 48 px.** Wayland
|
||||
doesn't report frame size before the window is mapped. If GNOME's
|
||||
title bar is taller at some scale, the bottom of the cover can clip by
|
||||
a few pixels.
|
||||
|
||||
## Round 48 (2026-09-10) — A library to break: done, see tasks-done.md
|
||||
|
||||
`scripts/make_dev_library.py` + the andTunes resume docs. Left on the table:
|
||||
|
||||
- [ ] **The fixture has no iTunes XML.** `test_itunes_importer.py` still leans
|
||||
on the gitignored `itunes-test-library/`. A generator flag that emits a
|
||||
matching `iTunes Library.xml` would make the import path developable
|
||||
without trav's own export too.
|
||||
- [ ] **No fixture for a Syncthing conflict.** The merge rules are the
|
||||
subtlest thing in the codebase and the generator could plant a
|
||||
`*.sync-conflict-*` pair on demand — two copies with different
|
||||
`date_modified`, one stamped and one not — instead of every conflict
|
||||
test hand-rolling one.
|
||||
|
||||
## Round 47 (2026-09-06) — LinTunes writes the device's library: done, see tasks-done.md
|
||||
|
||||
The desktop half of andTunes: a de-duplicated `Music/andTunes/` tree, a
|
||||
`library.json` the R1 app reads instead of scanning, per-album cover export,
|
||||
and a ticked-playlist selection that makes unticking the way to remove one.
|
||||
The app itself is rounds 48–50 — its board is `andtunes/TASKS.md`. Left on
|
||||
the table:
|
||||
|
||||
- [ ] **Ship default button images.** The spec wants the six menu tiles to be
|
||||
replaceable artwork "stored in an easy spot". The device side is
|
||||
designed (`Buttons/<name>.png`, app falls back to a bundled drawable)
|
||||
but LinTunes doesn't write the defaults yet — and when it does it must
|
||||
only fill in files that are *missing*, never overwrite trav's own.
|
||||
- [ ] **A format gate for what Android can't decode.** Sync copies every
|
||||
suffix verbatim today. AIFF and protected AAC will simply fail to play
|
||||
on the device. Wants the `export/web_support.py` shape: deny-by-default
|
||||
on the suffix with the iTunes `kind` breaking the `.m4a` tie, and
|
||||
FLAC-only conversion so it can never cost a bit.
|
||||
- [ ] **Clean up the old `Music/<Playlist>/` folders.** They're outside the
|
||||
andTunes root and the containment guard makes them unreachable on
|
||||
purpose, so this has to be a separate, explicitly confirmed action —
|
||||
and only once andTunes has replaced Auxio.
|
||||
- [ ] **Art export is the slow part of a sync.** One mutagen open + JPEG
|
||||
encode per album, on the worker thread, uncounted in the progress
|
||||
bar's byte total (the bar sits full while the label says "Album art").
|
||||
Fine for a few hundred albums; if trav syncs a thousand it wants a
|
||||
second progress phase with its own range.
|
||||
|
||||
## Round 46 (2026-09-02) — The file already told you: done, see tasks-done.md
|
||||
|
||||
Filename-derived proposals when no database knows the song, plus using the
|
||||
file's own tags/name/duration to rank real lookup results. Left on the table:
|
||||
|
||||
- [ ] **Submit fingerprints back to AcoustID.** The Zuuso case is unfixable by
|
||||
lookup because nobody ever submitted it — and LinTunes is holding a good
|
||||
fingerprint plus (after an identify) confirmed tags. `/v2/submit` takes
|
||||
exactly that. It would make the library better for everyone and fix
|
||||
trav's own second machine. Needs a user API key (not just the app key)
|
||||
and must be explicitly opt-in — never submit tags the user hasn't
|
||||
confirmed.
|
||||
- [ ] **An LLM pass for the genuinely ambiguous ones.** Raised by trav
|
||||
2026-09-02. Worth being precise about where it helps: *not* the Zuuso
|
||||
case (no data exists to reason over) and *not* the Alhambra year (1961
|
||||
isn't in the response either). It helps where there are many plausible
|
||||
candidates and the signal is semantic — an odd filename convention, or
|
||||
deciding whether a number is a performance year or a release year. Would
|
||||
be the app's first LLM dependency; keep it behind the same
|
||||
"nothing is written until accepted" gate.
|
||||
- [ ] **A "clean up this filename" action independent of identify.** Half of
|
||||
what round 46 built is useful with no network at all: 474 files in
|
||||
Unknown Artist/Unknown Album could have artist/title/track filled from
|
||||
their names in one pass. Wants the worker+progress split, not a dialog
|
||||
per track.
|
||||
|
||||
## Round 45 (2026-09-02) — Ask the song what it is: done, see tasks-done.md
|
||||
|
||||
Right-click → Identify Track…: Chromaprint fingerprint + AcoustID lookup, a
|
||||
proposal dialog, oldest-year-wins. Left on the table:
|
||||
|
||||
- [ ] **Batch identify without a dialog per track.** The queue reviews one
|
||||
track at a time, which is right for a handful and tedious for a hundred.
|
||||
A "fill only what's empty, above 0.9 confidence, no questions asked" pass
|
||||
would want the `plan_export()`-then-worker split from `export/exporter.py`
|
||||
(progress on the status bar, a cancel point) rather than the current
|
||||
one-at-a-time `_identify_next` chain, plus a summary at the end naming
|
||||
what it changed. Worth doing only if trav actually has a big pile of
|
||||
badly-tagged files.
|
||||
- [ ] **Fetch the cover in the same pass.** An identification hands back
|
||||
MusicBrainz release IDs, and the Cover Art Archive serves art by release
|
||||
ID — so a confirmed match could offer artwork without the separate
|
||||
artist/album text search that `art_search.py` does today. Needs
|
||||
`meta=releaseids` and a second endpoint; keep the deny-by-default
|
||||
"nothing is written until accepted" shape.
|
||||
- [ ] **Genre is still untouched.** AcoustID/MusicBrainz return tags, but
|
||||
they're folksonomy noise (dozens of overlapping user tags per recording)
|
||||
and iTunes-style genre is a single word. Deliberately skipped; revisit
|
||||
only with a mapping trav trusts.
|
||||
- [ ] **Tracks under ~3 seconds can't be fingerprinted** — Chromaprint returns
|
||||
"Empty fingerprint" and the status bar says so. Fine for music, would
|
||||
matter if the library ever holds sound effects.
|
||||
|
||||
## Round 40 (2026-08-22) — welcoming the newcomers: done, see tasks-done.md
|
||||
|
||||
No startup font modal, a first-run music-folder question, and a portable
|
||||
`music_folder`. Left on the table:
|
||||
|
||||
- [ ] **Offer to move the files when the music folder changes.** Today changing
|
||||
it warns that existing songs stay put and only new additions go to the new
|
||||
folder — honest, but someone genuinely relocating a library has to move it
|
||||
by hand. Constraints already worked out, so this needn't be re-derived:
|
||||
`_move_file` uses `Path.rename`, which is **same-filesystem only**, so a
|
||||
move to another drive needs a chunked copy + unlink (never `shutil.move`
|
||||
in one shot — no progress, no cancel point); every moved track's
|
||||
`location` must be repointed through `LibraryManager`, and its
|
||||
`date_modified` bumped or `_merge_track_fields` won't carry the new path
|
||||
across a sync; files **outside** the organize root must not be touched;
|
||||
and it wants the `plan_export()`-then-worker split from
|
||||
`export/exporter.py` so it can show progress and be cancelled. Not
|
||||
undoable — an `UndoStack` command is a synchronous closure, and a 21k-file
|
||||
move can't run on the GUI thread. Tabled 2026-08-22: trav has no plans to
|
||||
move his own library, so this was not worth the round.
|
||||
|
||||
## Round 37 (2026-08-19) — Export Playlist: done, see tasks-done.md
|
||||
|
||||
Folder + web-mix export, audio.js/jQuery dropped for a dependency-free
|
||||
player, lossless-only conversion.
|
||||
|
||||
## Round 38 (2026-08-20) — merge windows + play journals: done, see tasks-done.md
|
||||
## Round 39 (2026-08-20) — playlist order + honest date_modified: done, see tasks-done.md
|
||||
|
||||
## Round 36 — the merge rework (closed out by Rounds 38 and 39)
|
||||
|
||||
The three symptoms in Round 35 shared a root cause; that round fixed the
|
||||
performance half. Round 38 took the play-count and dialog items, Round 39 the
|
||||
two playlist ones. Only the `.stignore` note below is left.
|
||||
|
||||
- [x] **Playlist merges lose position.** Done in Round 39 — `merge_track_order`
|
||||
anchors each side-only track to the nearest track both copies share.
|
||||
Replayed against the real `a nissa one`: the reorder survives and a
|
||||
mid-list insert lands at index 5, not 26.
|
||||
- [x] **mtime is a lie for playlists.** Done in Round 39 — `Playlist.date_modified`
|
||||
is bumped only in `_set_track_ids`, and `_on_section_resized` now ignores
|
||||
the stretched last column, so resizing the window doesn't rewrite the
|
||||
playlist file at all.
|
||||
- [x] **`max()` play counts discard concurrent plays.** Done as described —
|
||||
per-machine `plays/<machine-id>.json` journals, machine id in the config
|
||||
dir. Three plays on the real library now write 83 bytes instead of 15 MB,
|
||||
and `library.json` isn't touched at all.
|
||||
- [x] **Quiet the merge dialog.** One dialog per session: each merge is folded
|
||||
in as a timestamped entry (newest first) instead of opening another
|
||||
window. Fifteen had stacked up. Kept the window for every merge rather
|
||||
than demoting lossless ones to the status bar — with journals landing,
|
||||
a merge stops being routine and is worth seeing.
|
||||
- [ ] Consider a `.stignore` for `.resolved` so merge backups stop syncing
|
||||
(Round 35 bounded the folder to 10 snapshots, but it still replicates).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# old
|
||||
|
||||
## Round 28 — cancelable device sync (v0.3.0)
|
||||
|
||||
- [x] "✕" button left of the sync progress group; expands to "cancel transfer"
|
||||
on hover; confirm dialog (Cancel Transfer / Keep Copying) before cancelling.
|
||||
- [x] `DeviceSyncWorker.cancel()`: stops at the next chunk boundary, removes
|
||||
the in-flight partial file, emits `cancelled` with copied/total counts.
|
||||
- [x] Cancelled syncs always leave a coherent device folder: the m3u is
|
||||
rewritten to list only tracks actually present (fixes the pre-existing gap
|
||||
where stale files deleted before a cancel could leave dangling m3u refs).
|
||||
- [x] Recoverability verified: cancel-then-resync produces byte-identical
|
||||
results to an uninterrupted sync (test + live run against the real Rabbit).
|
||||
|
||||
Tests in `tests/test_round28.py`. Feature round → minor bump **0.3.0**.
|
||||
|
||||
## Round 27 — device-sync guards (v0.2.2)
|
||||
|
||||
- [x] Quit warning while a transfer is copying (closeEvent; covers X button,
|
||||
Ctrl+Q, MPRIS Quit, and the self-update restart, which used to bypass
|
||||
closeEvent entirely).
|
||||
- [x] Machine can't sleep or shutdown/restart mid-transfer: second
|
||||
SleepInhibitor (suspend+logout flags, its own reason text for GNOME's dialog).
|
||||
- [x] Found & fixed: the GNOME Inhibit D-Bus call sent signed ints against a
|
||||
(susu) signature, so the *playback* sleep inhibitor had silently never
|
||||
worked — `_uint` marshalling repairs both.
|
||||
- [x] Investigated Syncthing changes landing mid-copy: safe by design (the
|
||||
SyncPlan is a click-time snapshot; the worker never reads live library
|
||||
state). Hardened the one gap: a source file relocated under the queue is
|
||||
skipped + dropped from the m3u + reported, instead of aborting the sync.
|
||||
|
||||
Tests in `tests/test_round27.py`. Fix round → patch bump **0.2.2**.
|
||||
|
||||
## Round 26 — Device menu: sync playlist to Rabbit R1 (v0.2.0)
|
||||
|
||||
- [x] Device menu (left of Track) with "Sync Playlist to Rabbit"; grayed out on
|
||||
the Library view, while syncing, or when no Rabbit is plugged in.
|
||||
- [x] `device_sync.py`: MTP/gvfs detection (the Rabbit mounts via MTP, not mass
|
||||
storage), name+size incremental diff, stale-file deletion scoped to the
|
||||
playlist's own folder, Auxio-importable `.m3u`, chunked-copy worker thread.
|
||||
- [x] Up-front free-space check with a needed-vs-available alert.
|
||||
- [x] Right-justified sync progress bar in the status bar (version left,
|
||||
totals middle, sync right).
|
||||
|
||||
Tests in `tests/test_round26.py`. Feature round → minor bump **0.2.0**.
|
||||
Follow-up polish (**0.2.1**): sync status split into a plain "Copying to
|
||||
Rabbit R1 · X / Y" label + textless bar (overlay text didn't fit), and the
|
||||
Device menu moved to the right of Track per trav.
|
||||
|
||||
## Round 25 — folder highlight while dragging a playlist (v0.1.6)
|
||||
|
||||
Tests in `tests/test_round25.py`. Fix round → patch bump **0.1.6**.
|
||||
|
||||
Dragging a playlist over a folder gave no indication the drop would land
|
||||
inside it. `PlaylistTree` now highlights the hovered folder mid-drag with the
|
||||
same treatment track drops give playlist rows, via a shared
|
||||
`_highlight_drop_target(pos, kind)` helper ("folder" for internal moves,
|
||||
"playlist" for track drops). A folder in the dragged row's own branch is
|
||||
never highlighted — `move_playlist` rejects that drop as a cycle, so lighting
|
||||
it up would lie. The Round 24 autoscroll tick's highlight refresh became
|
||||
kind-aware (`_drag_target_kind`) so a folder highlight survives edge
|
||||
auto-scrolling instead of being cleared by the track-only check.
|
||||
|
||||
- [x] `lintunes/gui/sidebar.py` — `_highlight_drop_target` /
|
||||
`_is_own_branch` helpers; `_drag_target_kind` tracked through
|
||||
`dragMoveEvent`, reset in `_stop_autoscroll`.
|
||||
|
||||
## Round 24 — sidebar drag auto-scroll (v0.1.5)
|
||||
|
||||
Tests in `tests/test_round24.py`. Fix round → patch bump **0.1.5**.
|
||||
|
||||
Dragging a playlist near the top/bottom edge of the sidebar tree scrolled
|
||||
nothing: `PlaylistTree.dragMoveEvent` fully overrides the base implementation,
|
||||
so Qt's built-in autoscroll (in `QAbstractItemView.dragMoveEvent`) never ran —
|
||||
the same root cause Round 13 fixed in the track table. Ported that machinery
|
||||
(`autoscroll_direction` shared from `track_table`, 40 ms timer, one unit per
|
||||
tick) into `PlaylistTree`. Playlist moves keep the scroll live at the edge
|
||||
even over non-folder rows (the tree always accepts an internal move
|
||||
somewhere); track drags gate on hovering a real playlist row, matching the
|
||||
drop-target highlight, and the highlight re-tracks the row under a held-still
|
||||
cursor as it scrolls.
|
||||
|
||||
- [x] `lintunes/gui/sidebar.py` — `_update_autoscroll` / `_autoscroll_tick` /
|
||||
`_stop_autoscroll` wired into `dragMoveEvent`, `dragLeaveEvent`,
|
||||
`dropEvent`.
|
||||
|
||||
## Round 23 — playback-control provenance log (v0.1.4)
|
||||
|
||||
Tests in `tests/test_round23.py`. Diagnostic round → patch bump **0.1.4**.
|
||||
|
||||
Background: LinTunes has resumed playback by itself for ~4s (then paused)
|
||||
four times while trav was away/asleep (Jul 5, 7, 8?, 10). Fingerprint each
|
||||
time: current track resumed from its paused position, paused ~4s later.
|
||||
Suspected phantom media-key events (the Audioengine 2+ USB DAC registers a
|
||||
HID *keyboard*), but the pathway is unproven — hence instrumentation.
|
||||
|
||||
- [x] New `lintunes/eventlog.py` — `log_control(source, action, detail)`
|
||||
appends timestamped one-liners to
|
||||
`~/.cache/lintunes/control-events.log` (1 MB rotate, never raises)
|
||||
and mirrors to the `lintunes.control` logger.
|
||||
- [x] Provenance hooks: every MPRIS Player method (`mpris.py`; PyQt6 lacks
|
||||
QDBusContext so no sender — run dbus-monitor alongside when the
|
||||
sender matters), the media-key `eventFilter` in `main_window.py`
|
||||
(logs the source input device where the compositor exposes it), and
|
||||
`Player.toggle_play`/`pause` (state + position).
|
||||
- [x] `tests/conftest.py`: autouse fixture isolates the log path so tests
|
||||
never write the real ~/.cache file.
|
||||
|
||||
## Round 22 — start time honored on same-source replay (v0.1.3)
|
||||
|
||||
Plan reference: `~/.claude/plans/could-you-look-into-generic-balloon.md`.
|
||||
Tests in `tests/test_round22.py`. Fix-only round → patch bump **0.1.3**.
|
||||
|
||||
Background: editing a track's start time saved fine but replaying it still
|
||||
started at 0:00 whenever that track was already the loaded source — Qt's
|
||||
`QMediaPlayer.setSource()` no-ops on an unchanged URL, so the LoadedMedia
|
||||
status change that consumes the armed `_pending_start_ms` never re-fired.
|
||||
(A forced clear+reload was tried first and races the FFmpeg backend: the
|
||||
seek lands, then the pipeline restart snaps position back to 0.)
|
||||
|
||||
- [x] `Player._load_current`: when the source URL is unchanged, skip
|
||||
`setSource` entirely — the media is already loaded — and `stop()` +
|
||||
seek straight to the armed start time. Verified live (offscreen GUI
|
||||
harness): replay-while-playing, replay-after-EndOfMedia, and
|
||||
cleared-start-time replay all land where they should.
|
||||
- [x] `Player.previous()`: the ">3 s in restarts the track" path now seeks
|
||||
to the track's custom start time instead of 0:00.
|
||||
|
||||
## 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_folder>/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 <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
|
||||
|
||||
- [ ] 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.
|
||||
**See the backend note below — a GStreamer sink would make this a
|
||||
drop-in `equalizer-3bands` element instead of a redesign.**
|
||||
|
||||
- [ ] Swap the local sink from Qt Multimedia to GStreamer (`GstSink`).
|
||||
Assessed 2026-08-15 while fixing the Round 34 audio leak. Not urgent —
|
||||
the parking brake handles the leak — but the case is real and it should
|
||||
be the plan whenever the equalizer or gapless comes up, since those are
|
||||
what make it pay for itself.
|
||||
|
||||
Why: Qt Multimedia's FFmpeg backend has cost us five workarounds now, all
|
||||
of them in `LocalSink` — the stale-`LoadedMedia` URL tagging, the
|
||||
`setSource` no-op dance, the resume nudge, the shutdown-ordering segfault
|
||||
guard, and now the parking brake. GStreamer is what every other Linux
|
||||
music player uses (Rhythmbox, Lollypop, Amberol), corks properly on
|
||||
pause, and unlocks three parked/impossible features: a 3-band EQ
|
||||
(`equalizer-3bands`), true gapless (`playbin3` `about-to-finish`), and
|
||||
ReplayGain (`rgvolume`).
|
||||
|
||||
Cost: `LocalSink` is ~180 lines and `PlaybackSink` is already a real
|
||||
seam (proven by `CastSink`), so the sink itself is bounded. The bigger
|
||||
cost is the tests — 10+ files stub Qt Multimedia *by name* via
|
||||
`patch.multiple(player_module, QMediaPlayer=…, QAudioOutput=…,
|
||||
QAudioBufferOutput=…, QMediaDevices=…)`. Also: new system dep
|
||||
(`python3-gi` + `gstreamer1.0-*` via apt here, `python3-gobject` +
|
||||
`gstreamer1-plugins-*` via dnf on the Fedora machine — the self-updater
|
||||
only does `git pull`, so both machines need it installed by hand before
|
||||
the push lands), GLib bus pumped from a `QTimer` rather than a GLib main
|
||||
loop, and the visualizer's PCM tee rebuilt on `appsink` instead of
|
||||
`QAudioBufferOutput`.
|
||||
|
||||
Do it incrementally: write `GstSink` alongside `LocalSink`, put it behind
|
||||
a preference, run it for a week, delete the Qt one when it's trusted.
|
||||
Already available here: GStreamer 1.26.2 + Python bindings + libav,
|
||||
pipewire and good/base plugin sets.
|
||||
- [ ] 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.
|
||||
@@ -0,0 +1,28 @@
|
||||
# andTunes — device facts
|
||||
|
||||
Measured facts about the Rabbit R1 that the app is built around.
|
||||
(Moved out of the old andtunes/TASKS.md board on 2026-09-24.)
|
||||
|
||||
|
||||
- Panel **480 × 640 px**, physical density 320, **override density 160** — so
|
||||
the app sees a 480 × 640 **dp** canvas on a 2.88" screen (~278 real ppi).
|
||||
- **One dp is about half its usual physical size here.** Double every stock
|
||||
value: body text 28–32 sp, list rows ≥ 88 dp, menu tiles ~213 dp. A 48 dp
|
||||
row would be 4.4 mm tall. Do not "fix" this by changing the device density —
|
||||
trav likes it where it is.
|
||||
- Android 13 / API 33, `gsi_r1-userdebug`, arm64-v8a, 65 GB free.
|
||||
- **The wheel can only ever give us detents.** `getevent -pl` (2026-09-14):
|
||||
`och1970_holl_key` advertises `KEY (0001): KEY_UP KEY_DOWN` and *nothing
|
||||
else* — no `EV_REL`/`REL_WHEEL`, no `EV_ABS`, no resolution. The `och1970`
|
||||
is a Hall latch, so the driver quantises the magnet into clicks and hands
|
||||
userspace one key press each. There is no sub-detent position to read
|
||||
without a kernel driver change, so every bit of nuance has to be derived
|
||||
from the *timing* between detents (see `WheelScroll`).
|
||||
- Scroll wheel = input device `och1970_holl_key` → **KEY_UP / KEY_DOWN**,
|
||||
mapped by the stock `Generic.kl` (checked 2026-09-11 via `dumpsys input`)
|
||||
to **`KEYCODE_DPAD_UP` / `DPAD_DOWN`** — not volume. Side/PTT button sits
|
||||
on `mtk-kpd` with KEY_VOLUMEDOWN + KEY_POWER. Headset exposes
|
||||
KEY_PLAYPAUSE, so a MediaSession gets media keys for free.
|
||||
- USB: set to come up as **`mtp,adb`** whenever the screen is unlocked
|
||||
(`adb shell svc usb setScreenUnlockedFunctions mtp`, 2026-09-11), so the
|
||||
gvfs mount sync uses and the adb install uses are both there at once.
|
||||
+2
-2
@@ -209,6 +209,6 @@ push onto any Android device or emulator with `adb push`. That tree includes
|
||||
the awkward cases on purpose — an emoji album name, a 208-character title, two
|
||||
tracks that differ only by id, a track with no artist.
|
||||
|
||||
## Board
|
||||
## Device facts
|
||||
|
||||
`TASKS.md` in this directory.
|
||||
`DEVICE.md` in this directory: screen density, wheel keycodes, USB modes.
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
# andTunes — task board
|
||||
|
||||
A music player for the Rabbit R1 that never scans anything: LinTunes writes
|
||||
`Music/andTunes/library.json` during sync and the app just reads it. See
|
||||
`../andTunes spec.txt` for the original brief.
|
||||
|
||||
Legend: `[ ]` todo · `[~]` in progress · `[x]` done.
|
||||
|
||||
**Status: phases 1–3 done (2026-09-11, LinTunes rounds 50–51, andTunes
|
||||
0.2.0); 0.2.1 (LinTunes round 54, 2026-09-14) fixed the grouping crash —
|
||||
`Library.group()` keyed albums case-folded but artists raw-case, so one artist
|
||||
spelled two ways on one album ("RJD2" / "Rjd2") took the whole library down
|
||||
with `Couldn't read library.json`. Both maps fold case now, and `Artist` has a
|
||||
`key` the way `Album` always did. 0.2.2 (round 55, 2026-09-14) reversed the
|
||||
wheel in lists and made it scroll smoothly (`WheelScroll` — a pixel debt paid
|
||||
off per frame, not a row per detent); volume on the other screens is
|
||||
unchanged.** What's left is the *Parked* list, pulled in
|
||||
as trav finds he needs it. Built with
|
||||
`python3 andtunes/build.py` — plain Java, no Gradle, nothing downloaded (see
|
||||
`README.md`, *Toolchain*). Phases are andTunes' own; which LinTunes round
|
||||
each lands in is decided when it starts.
|
||||
|
||||
## Device facts (measured 2026-09-06, `adb shell wm size` etc.)
|
||||
|
||||
- Panel **480 × 640 px**, physical density 320, **override density 160** — so
|
||||
the app sees a 480 × 640 **dp** canvas on a 2.88" screen (~278 real ppi).
|
||||
- **One dp is about half its usual physical size here.** Double every stock
|
||||
value: body text 28–32 sp, list rows ≥ 88 dp, menu tiles ~213 dp. A 48 dp
|
||||
row would be 4.4 mm tall. Do not "fix" this by changing the device density —
|
||||
trav likes it where it is.
|
||||
- Android 13 / API 33, `gsi_r1-userdebug`, arm64-v8a, 65 GB free.
|
||||
- **The wheel can only ever give us detents.** `getevent -pl` (2026-09-14):
|
||||
`och1970_holl_key` advertises `KEY (0001): KEY_UP KEY_DOWN` and *nothing
|
||||
else* — no `EV_REL`/`REL_WHEEL`, no `EV_ABS`, no resolution. The `och1970`
|
||||
is a Hall latch, so the driver quantises the magnet into clicks and hands
|
||||
userspace one key press each. There is no sub-detent position to read
|
||||
without a kernel driver change, so every bit of nuance has to be derived
|
||||
from the *timing* between detents (see `WheelScroll`).
|
||||
- Scroll wheel = input device `och1970_holl_key` → **KEY_UP / KEY_DOWN**,
|
||||
mapped by the stock `Generic.kl` (checked 2026-09-11 via `dumpsys input`)
|
||||
to **`KEYCODE_DPAD_UP` / `DPAD_DOWN`** — not volume. Side/PTT button sits
|
||||
on `mtk-kpd` with KEY_VOLUMEDOWN + KEY_POWER. Headset exposes
|
||||
KEY_PLAYPAUSE, so a MediaSession gets media keys for free.
|
||||
- USB: set to come up as **`mtp,adb`** whenever the screen is unlocked
|
||||
(`adb shell svc usb setScreenUnlockedFunctions mtp`, 2026-09-11), so the
|
||||
gvfs mount sync uses and the adb install uses are both there at once.
|
||||
|
||||
## Phase 1 — plays a song (v0.1.0) ✔
|
||||
|
||||
- [x] `java-21-openjdk-devel` — installed, `javac` on PATH. (Java 25 is also
|
||||
present; `build.py` points at 21.)
|
||||
- [x] Android SDK: platform 33 + build-tools 34.0.0 were already in
|
||||
`~/Android/Sdk`. **No Gradle, no Kotlin** — `build.py` runs aapt2 →
|
||||
javac → R8 → zipalign → apksigner directly (trav, 2026-09-11: don't
|
||||
pull a GB over cell if it isn't needed; it isn't).
|
||||
- [x] Project, package `me.teafry.andtunes`, minSdk 26 / targetSdk 33,
|
||||
framework only: `Activity`, `ListView`, `MediaPlayer`, `MediaSession`,
|
||||
`Notification.MediaStyle`, R8. The APK is 61 KiB.
|
||||
- [x] `MANAGE_EXTERNAL_STORAGE` + a first-run screen that sends the user to
|
||||
the toggle (LinTunes' installer grants it over adb, so it rarely shows).
|
||||
- [x] Library loader: `android.util.JsonReader` streaming parse off the main
|
||||
thread into a process singleton; artists/albums grouped in one pass;
|
||||
an unknown `format` gets a readable message; reloads when
|
||||
`library.json` changes (a re-sync) the next time the menu resumes.
|
||||
- [x] `MenuActivity`: six tiles, 2 × 3, **loads nothing**. The sixth reads
|
||||
"Now Playing" or "Shuffle All" depending on the service.
|
||||
- [x] `ListActivity` in songs mode + "Shuffle all" header row.
|
||||
- [x] `PlaybackService` (foreground) + `NowPlayingActivity`: white
|
||||
background, square art, slim scrubber, ◀ ⏯ ▶, shuffle, repeat.
|
||||
**No back button.**
|
||||
- [x] Audio focus, ACTION_AUDIO_BECOMING_NOISY, MediaSession + notification.
|
||||
- [x] End of queue **pauses** unless repeat is on. Repeat: off / all / one.
|
||||
- [x] `adb shell am start -W`: **313–338 ms** cold to the menu (551 ms on the
|
||||
very first launch after install, which includes dex verification).
|
||||
|
||||
## Phase 2 — the rest of the screens ✔
|
||||
|
||||
- [x] Playlists, artists (→ "All songs" + albums; all songs has its own
|
||||
shuffle), albums, search (artists, albums and songs as you type).
|
||||
- [x] Album art thumbs in album rows, full art on now playing.
|
||||
- [x] The "Now playing" bar at the bottom of every list screen; the playing
|
||||
song is drawn inverted in any list it appears in.
|
||||
- [x] Scroll wheel → list scrolling (one row per detent), volume on the menu
|
||||
and now-playing screens. Taken in `dispatchKeyEvent`, DPAD *and*
|
||||
VOLUME codes, down *and* up, so the search box can't swallow it.
|
||||
- [x] Replaceable button images: `Buttons/<name>.png` beats the bundled
|
||||
drawable, for the six menu tiles plus ◀ ▶ ⏯. LinTunes ships the
|
||||
defaults into `Buttons/` only where the file is missing.
|
||||
- [x] Missing file (synced then deleted) skips with a toast, doesn't stop
|
||||
the queue.
|
||||
- [x] Resume last queue + track + position on launch (paused).
|
||||
|
||||
## Phase 3 — install from LinTunes, counts coming back
|
||||
|
||||
- [x] `Connections → Install andTunes on Rabbit…` (LinTunes 0.19.0):
|
||||
`adb install -r` + the two permission grants; says Install / Update /
|
||||
Reinstall from the version on the device; with no adb, copies the APK
|
||||
to `Download/` over MTP for a manual tap. APK committed at
|
||||
`lintunes/android/andTunes.apk` (+ `andTunes.json`, its version).
|
||||
- [x] Format gate (LinTunes 0.20.0): sync reuses
|
||||
`export/web_support.conversion_for` outright, because Android's
|
||||
MediaPlayer decodes the same set a browser does. Protected AAC is
|
||||
refused and reported; ALAC, AIFF and anything else unplayable becomes
|
||||
**FLAC** (never a lossy re-encode). Conversions are cached per track, so
|
||||
they're converted once and size-diffed after that.
|
||||
- [x] Play counts back (andTunes 0.2.0): a natural finish counts a play, as
|
||||
it does in LinTunes. The app keeps `plays/andtunes-<install id>.json` in
|
||||
the per-machine-totals shape of `storage/play_journal.py`. Sync folds it
|
||||
into `<data_dir>/plays/` with the per-track max *before* copying
|
||||
anything, so the R1 is one more machine in the Round 38 model and there
|
||||
are no new merge rules.
|
||||
- [x] Retired `Sync Playlist to Rabbit (Auxio)` from the menu. (trav's R1
|
||||
has no old `Music/<Playlist>/` folders, so no cleanup was needed.)
|
||||
|
||||
## Parked
|
||||
|
||||
- Gapless, crossfade, EQ, ratings, folder browsing, sleep timer, Android Auto.
|
||||
- Deleting the old per-playlist folders automatically. They're outside the
|
||||
andTunes root and the containment guard makes them unreachable on purpose;
|
||||
removing them should be an explicit, separately-confirmed action.
|
||||
@@ -0,0 +1,91 @@
|
||||
social/napster layer to lintunes
|
||||
|
||||
what is the user journey??? hehe
|
||||
|
||||
want to subscribe to playlists
|
||||
don't want any central servers I run
|
||||
syncthing perhaps, per person
|
||||
each person has a folder they share
|
||||
and you sync it down
|
||||
and you can copy it to your own machine
|
||||
and it's lowkey a gossip protocol bc if you're on the same network with someone else who follows that friend you can give them the tracks
|
||||
how to limit total size?
|
||||
|
||||
OH what if you sync a COPY of that person's library file
|
||||
that knows what tracks they have
|
||||
and you can peruse it
|
||||
and mark certain ones as wanted
|
||||
so maybe you have a want list
|
||||
|
||||
each person's share folder is read only to you
|
||||
|
||||
|
||||
|
||||
|
||||
2026-09-28
|
||||
what are necessary features?
|
||||
what features does syncthing afford?
|
||||
let's do the venn diagram of those
|
||||
then do the ones that it afford but aren't necessary
|
||||
|
||||
|
||||
|
||||
|
||||
follow someone by pasting a link?
|
||||
do it under Connections menu
|
||||
it would have "add friend library" or "share library with a friend"
|
||||
add friend library gives you a pop up with a text field where you can paste in the syncthing link that's read-only to you
|
||||
you also create a folder that's read-only for them and it outputs that link for you to give the friend
|
||||
they can paste this link into the same "add friend library"
|
||||
there should also be a menu item that says "sync settings"
|
||||
all these menu items can be grouped together under Connections between a couple horizonal lines.
|
||||
|
||||
sync settings can have a list on the left of 'general settings', and then in the list each friend you're synced with
|
||||
this should look and behave like the settings window in vlc with the list on the left and settings that pop up on the right.
|
||||
for each one it'll give you status of Syncthing generally and each friend connection. Whether you've made contact with the other machine.
|
||||
The way the friend sync should work is that each user chooses what of their library they want to sync. This is in sync settings for each friend.
|
||||
by default it should have checked 'share whole library
|
||||
if that button is unchecked then a browser (like the browser in the app, genre, artist, album) shows up as well as 2 buttons, 'select all' and 'select none'
|
||||
we need to make sure there's a cancel button on this window in case people press one of these buttons by accident, thus clearing their selectionn.
|
||||
each artist, album and song has a little checkmark next to it
|
||||
whatever has a checkmark, these artists, albums are made available to the other user
|
||||
in the folder that we share with them, that's read-only to them, contains shrunken library database copies. If someone has selected to share all artists and all playlists, it's basically just a copy of the local library.
|
||||
if it's pared down on either the playlists or artits, etc then those are removed from the copy of the library in the share folder
|
||||
we only need to write the file when we hit 'ok' in the settings menu.
|
||||
we can set what we share with each friend in each friend settings
|
||||
OR
|
||||
you can set a setting in the general sync settings that you want to share the same thing to all your friends and then you pick in the general settings.
|
||||
|
||||
share all playlists is default unchecked with none of the playlists selected
|
||||
thus defaulting to a user opting in to sharing all their playlists
|
||||
in general settings there's a setting for what your name shows up as to other users.
|
||||
|
||||
in the main window we now need to cut out a small square of the Library button to put a little down-arrow triangle library selector button
|
||||
hierarchically this button should appear as a smaller part of the library button, resting right next to it and not being sloped on the corners where they meet.
|
||||
clicking that button causes a tiny popup window to show below it where you can select your friends libraries by name
|
||||
when switching in to their library it now shows all of their library and playlists as if you're using their lintunes
|
||||
but the files aren't all there
|
||||
there is now another column that shows up by default all the way to the left of the other columns
|
||||
this is a download request button
|
||||
each artist, album and song will have a little embossed button with a cassette tape icon on it
|
||||
if you press it it goes gray (as does the icon next to any sub-items, ie, clicking the cassette for an artist will transitively click the cassette for all albums and tracks of that artist)
|
||||
if you hover over a grayed-out pressed cassete button it should go medium-gray indicating it can be pressed to swap back from selected to un-selected
|
||||
they're esentially all checkboxes.
|
||||
|
||||
If any new artists/albums/tracks are selected or unselected, 'cancel' and 'save' buttons will appear in the bottom menu to go back to your own library and update your requests file
|
||||
this requests file is read by the other lintunes and if it sees a song that has been 'cassetted', ie, selected by the friend lintunes, then the subsequent file is saved to the out-box that is read-only synced back to the friend.
|
||||
the other friend's lintunes tracks that folder and when files are synced in there that we have 'cassetted' then we copy them into our library as if the 'add to library' menu item had been selected
|
||||
ie, sort into the right folder. Import the song.
|
||||
we then now update the list of tracks we have 'cassetted' to a specific friend because we no longer cassette it, we have it now.
|
||||
|
||||
when viewing a friend's library you can say 'see only tracks I don't have' or 'see all tracks' in the bottom menu.
|
||||
|
||||
I imagine this will require some delta-library folders or ways to keep track of state. We want to be resilient if sync goes wonky.
|
||||
also we want it to be fast so we want to limit disk writes and reads. It should be fast because it's all local. We let syncthing handle everything that goes over the network.
|
||||
|
||||
I now also want to by default on new installs to add a smart playlist called 'new tracks' which shows stuff that has been added to your library in the past 3 months
|
||||
this can be deleted just like any other smart playlist
|
||||
|
||||
|
||||
|
||||
additionally I'd like to add some emboss to all of the buttons. Just a real slight emboss, soft.
|
||||
-1315
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user