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:
@@ -265,7 +265,42 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
|
||||
copystat and never trust mtimes (diff by name+size). Sync owns exactly
|
||||
`Music/<Playlist Name>/` on the device (creates/overwrites/deletes there,
|
||||
plus an Auxio-importable `.m3u`); it never deletes outside that folder and
|
||||
only ever *reads* local library files.
|
||||
only ever *reads* local library files. Since Round 47 this is the *older*
|
||||
of two syncs — kept on the menu as "Sync Playlist to Rabbit (Auxio)" until
|
||||
the andTunes app can play a song — and its `sanitize_name` / `track_display`
|
||||
/ `track_filename` / `build_m3u` / `CHUNK` are imported by both
|
||||
`export/exporter.py` and `andtunes/`, so their signatures are load-bearing.
|
||||
|
||||
- **`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
|
||||
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
|
||||
`Music/andTunes/library.json` and the app parses one file instead of
|
||||
indexing a filesystem. `layout.py` is the on-device shape (`Media/<Artist>/
|
||||
<Album>/04 Song.mp3` — one copy per song however many playlists hold it —
|
||||
plus `Art/`, `Playlists/`, `library.json`); `manifest.py` is pure and
|
||||
Qt-free; `art.py` renders one 480 px JPEG **per album** (not per track) via
|
||||
QImage — safe off the GUI thread, unlike QPixmap — cached under
|
||||
`$XDG_CACHE_HOME/lintunes/andtunes-art` and invalidated by the audio file
|
||||
being newer, which is exactly what embedding new art does. `sync.py` is a
|
||||
third sibling of `plan_export`/`ExportWorker`: pure planner, then a daemon
|
||||
thread with KiB progress. Three rules that bite: **planning must not render
|
||||
art** (a mutagen open per album, and planning runs on the GUI thread — the
|
||||
worker does it, and those bytes are absent from the progress total); the
|
||||
index (m3us then `library.json`) is written **last** and after a cancel is
|
||||
rewritten to list only tracks whose files actually landed, so the app never
|
||||
opens a manifest with holes in it; and every delete goes through
|
||||
`layout.assert_inside`, which refuses anything not strictly inside the
|
||||
andTunes root. That guard is why the old `Music/<Playlist>/` folders are
|
||||
structurally unreachable rather than merely un-referenced, and `Buttons/`
|
||||
(the user's own menu artwork) and `plays/` (written by the app) are skipped
|
||||
by the device scan entirely — sync reads those, it doesn't own them.
|
||||
Which playlists go on the device lives in `preferences.json` under
|
||||
`device_sync` (so it rides the Syncthing share and both machines agree);
|
||||
keeping it off the `Playlist` keeps it out of the conflict-merge machinery,
|
||||
and **unticking is the only way a playlist comes off the device**.
|
||||
|
||||
- **`lintunes/export/`** — `File → Export Playlist…` (also on a playlist's
|
||||
right-click menu). A sibling of `device_sync`, reusing its filename helpers
|
||||
@@ -323,6 +358,14 @@ persistence) → GUI (Qt widgets that read the manager and connect to its signal
|
||||
|
||||
## Conventions & gotchas
|
||||
|
||||
- **The Rabbit R1's screen is not what the internet says.** Measured:
|
||||
**480 × 640 px**, physical density 320, **override density 160** — so an app
|
||||
sees a 480 × 640 *dp* canvas on a 2.88" panel (~278 real ppi). One dp is
|
||||
about half its usual physical size, so andTunes doubles every stock value
|
||||
(rows ≥ 88 dp, text 28–32 sp). Don't "fix" it by changing the device
|
||||
density; trav has it where he wants it. Android 13 / API 33, arm64-v8a.
|
||||
The scroll wheel emits KEY_UP/KEY_DOWN (`KEYCODE_DPAD_UP`/`DOWN`).
|
||||
|
||||
- **Tests are organized as `tests/test_roundN.py`** — each development round adds
|
||||
a new `test_roundN.py` alongside the topical files (`test_models.py`,
|
||||
`test_itunes_importer.py`, etc.). New feature work follows the same pattern.
|
||||
|
||||
Reference in New Issue
Block a user