v0.17.0: the desktop already knows what the songs are
The first half of andTunes, a music player for the Rabbit R1. Auxio re-reads Android's MediaStore on every launch, which is the "your songs will show up here" hang — so the answer isn't a faster scanner, it's not scanning at all. LinTunes already knows the artist, album and year of every file it just copied, so it writes them into Music/andTunes/library.json and the app reads one file instead of indexing a filesystem. New lintunes/andtunes/: layout.py (a de-duplicated Media/<Artist>/<Album>/ tree, so a song in three playlists is stored once), manifest.py (pure, Qt-free), art.py (one 480 px cover per album via QImage, cached and invalidated by the audio file's mtime — which is exactly what embedding new art moves), and sync.py, a third sibling of plan_export/ExportWorker. Three rules the code depends on: planning never renders art (a mutagen open per album, and planning runs on the GUI thread); the index is written last and after a cancel lists only tracks whose files actually landed; and every delete goes through layout.assert_inside, which is why the old Music/<Playlist>/ folders are structurally unreachable rather than merely un-referenced. Connections gains "Sync to Rabbit" and "Rabbit Sync Settings…" — the ticked set is the device's contents, so unticking is how a playlist comes off, which nothing could do before. The selection rides preferences.json so both machines agree. The old per-playlist sync stays, renamed "(Auxio)": removing it now would leave the device full of files and nothing able to open them until the app exists. The app itself is rounds 48-50; its board is andtunes/TASKS.md, including the measured screen facts (480x640 px at density override 160 — one dp is half its usual physical size on a 2.88" panel). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wrTys5U1fLb4pBD2LKWzV
This commit is contained in:
@@ -1,5 +1,78 @@
|
||||
## Done
|
||||
|
||||
### Round 47 (2026-09-06) — LinTunes writes the device's library (v0.17.0)
|
||||
|
||||
The first half of andTunes, a music player for the Rabbit R1 that never scans
|
||||
anything. Auxio re-reads Android's MediaStore on every launch, which is what
|
||||
leaves the Rabbit sitting on "your songs will show up here" — so the answer
|
||||
isn't a faster scanner, it's not scanning: LinTunes already knows the artist,
|
||||
album and year of every file it just copied, so it writes them down and the
|
||||
app reads one JSON file. This round builds the desktop side; the app itself is
|
||||
rounds 48–50 (`andtunes/TASKS.md`).
|
||||
|
||||
- [x] **A de-duplicated tree instead of a folder per playlist.**
|
||||
`lintunes/andtunes/layout.py` lays out `Music/andTunes/` — `Media/
|
||||
<Artist>/<Album>/04 Song.mp3` (the iTunes shape `filename_tags` already
|
||||
reads back, with a `2-04` prefix once a release has more than one disc),
|
||||
`Art/<key>.jpg`, `Playlists/<Name>.m3u`, `library.json`. A song in three
|
||||
playlists is stored once, which the Artists/Albums/Songs screens need
|
||||
anyway and which saves the space three copies cost.
|
||||
- [x] **`library.json`, the reason the app opens fast.**
|
||||
`andtunes/manifest.py` is pure — no Qt, no device, no I/O. Rows are
|
||||
sparse the way `Track.to_dict` is sparse, because that file is what the
|
||||
R1 parses before it can draw anything. Artists and albums are *not*
|
||||
shipped: grouping a flat list on device beats parsing three redundant
|
||||
ones. A playlist's ids are filtered to tracks that actually landed, so
|
||||
the app never resolves a dangling reference.
|
||||
- [x] **One cover per album, not per song.** `andtunes/art.py` reads the
|
||||
embedded artwork with mutagen, scales to 480 px (the panel's width) and
|
||||
re-encodes as JPEG through `QImage`/`QBuffer` — no new dependency, and
|
||||
QImage is safe off the GUI thread. Keyed on album artist + album, so a
|
||||
twelve-track record costs one mutagen open and one file. Cached under
|
||||
`$XDG_CACHE_HOME/lintunes/andtunes-art`, invalidated by the audio file
|
||||
being newer — which is exactly right, since embedding new art rewrites
|
||||
the file and moves its mtime.
|
||||
- [x] **A sibling worker, not a flag on the old one.**
|
||||
`andtunes/sync.py` follows the `plan_export`/`ExportWorker` shape
|
||||
exactly: pure planner, then a daemon thread emitting KiB progress, with
|
||||
the same MTP caveats (no copystat, diff by name+size). It is separate
|
||||
because it nests directories, has an album-art pass, and writes several
|
||||
trailing files instead of one m3u. Planning deliberately does *not*
|
||||
render art — that's a mutagen open per album and planning runs on the
|
||||
GUI thread.
|
||||
- [x] **The index is written last, and always describes what's really there.**
|
||||
Cancel mid-sync and the manifest and m3us are rewritten listing only
|
||||
tracks whose files landed, so andTunes opens a smaller working library
|
||||
rather than a broken one. Re-syncing completes it.
|
||||
- [x] **A containment guard, because this is the first code that deletes a
|
||||
directory on the Rabbit.** Every delete goes through
|
||||
`layout.assert_inside`, which refuses anything that isn't strictly
|
||||
inside the andTunes root — so the old `Music/<Playlist>/` folders are
|
||||
structurally unreachable, not merely un-referenced. `Buttons/` (the
|
||||
user's own menu artwork) and `plays/` (written by the app) are skipped
|
||||
by the scan entirely: sync reads those, it doesn't own them. Emptied
|
||||
folders are pruned bottom-up, asking the filesystem rather than
|
||||
`os.walk`'s stale listing.
|
||||
- [x] **Unticking a playlist is how it comes off the device.** New
|
||||
`Connections → Sync to Rabbit` (everything ticked, no longer caring
|
||||
which view is open) and `Rabbit Sync Settings…`
|
||||
(`gui/device_sync_dialog.py` — a checkbox list with a filter box,
|
||||
because 487 playlists is a normal library here). The selection lives in
|
||||
`preferences.json` under `device_sync`, so it rides the Syncthing share
|
||||
and both machines agree on what the Rabbit is carrying; keeping it off
|
||||
the `Playlist` keeps it out of the conflict-merge machinery.
|
||||
- [x] **The old per-playlist sync stays, renamed "(Auxio)".** Removing it in
|
||||
the same round that adds the new tree would leave the device full of
|
||||
files and nothing able to open them until round 49. It goes when
|
||||
andTunes can actually play a song.
|
||||
|
||||
Not done, deliberately: the Android app (rounds 48–50), shipping default
|
||||
button images, the format gate for files Android can't decode, and reading
|
||||
play counts back off the device. Verified against the real 21,531-track
|
||||
library into a scratch device root — a re-sync copies nothing, and unticking
|
||||
a playlist removes its media, its m3u, its now-unused cover and every emptied
|
||||
folder.
|
||||
|
||||
### Round 46 (2026-09-02) — The file already told you (v0.16.0)
|
||||
|
||||
Round 45 shipped and trav broke it in two places within the hour, both real.
|
||||
|
||||
Reference in New Issue
Block a user