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
87 lines
4.7 KiB
Markdown
87 lines
4.7 KiB
Markdown
# 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.
|
||
|
||
## 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.
|
||
- Scroll wheel = input device `och1970_holl_key` → **KEY_UP / KEY_DOWN**
|
||
(`KEYCODE_DPAD_UP` / `DPAD_DOWN`). Side/PTT button sits on `mtk-kpd` with
|
||
KEY_VOLUMEDOWN + KEY_POWER. Headset exposes KEY_PLAYPAUSE, so a MediaSession
|
||
gets media keys for free.
|
||
|
||
## Round 48 — plays a song (v0.1.0)
|
||
|
||
- [ ] Toolchain: `sudo dnf install java-21-openjdk-devel`, Android
|
||
commandline-tools + platform 33 + build-tools into `~/Android/Sdk`.
|
||
(Java 25 is installed but AGP won't run on it.) Gradle via the wrapper.
|
||
- [ ] Gradle project, package `me.teafry.andtunes`, minSdk 26 / targetSdk 33.
|
||
**No Compose, no androidx, no Media3** — framework `Activity`,
|
||
`ListView`, `MediaPlayer`, `MediaSession`, `Notification.MediaStyle`,
|
||
R8 minify. Cold start is the whole feature; there is nothing to
|
||
initialize if nothing is linked in.
|
||
- [ ] `MANAGE_EXTERNAL_STORAGE` + a first-run screen that sends the user to
|
||
the toggle. `READ_MEDIA_AUDIO` grants the audio files but **not**
|
||
`library.json`, which isn't media — that's the whole reason.
|
||
- [ ] Library loader: parse `library.json` off the main thread into a
|
||
singleton that survives between activities. Refuse an unknown `format`
|
||
with a readable message rather than half-reading it.
|
||
- [ ] `MenuActivity`: six tiles, 2 × 3, **loads nothing**. The sixth reads
|
||
"Now Playing" or "Shuffle All" depending on the service.
|
||
- [ ] `ListActivity` in songs mode + "Shuffle all" header row.
|
||
- [ ] `PlaybackService` (foreground) + `NowPlayingActivity`: white
|
||
background, square art, slim scrubber, ◀ ⏯ ▶, shuffle. **No back
|
||
button** — Android's own is always on screen.
|
||
- [ ] Audio focus, ACTION_AUDIO_BECOMING_NOISY, MediaSession + notification.
|
||
- [ ] End of queue **pauses** unless repeat is on. Repeat: off / all / one.
|
||
- [ ] Verify with `adb shell am start -W` — target: menu visible < 400 ms.
|
||
|
||
## Round 49 — the rest of the screens
|
||
|
||
- [ ] Playlists, artists (→ albums + "All songs" + shuffle for that artist),
|
||
albums, search.
|
||
- [ ] Album art thumbs in album rows, full art on now playing.
|
||
- [ ] The "Now playing" bar at the bottom of every list screen.
|
||
- [ ] Scroll wheel → list scrolling, and volume on the now-playing screen.
|
||
- [ ] Replaceable button images: load `Buttons/<name>.png` from the device
|
||
folder when present, else the bundled drawable. LinTunes ships defaults
|
||
into `Buttons/` only when the file is missing, so the user's own
|
||
artwork is never overwritten.
|
||
- [ ] Missing file (synced then deleted) skips with a toast, doesn't crash
|
||
the queue.
|
||
- [ ] Resume last track + position on launch.
|
||
|
||
## Round 50 — install from LinTunes, counts coming back
|
||
|
||
- [ ] `Connections → Install andTunes on Rabbit…`: `shutil.which("adb")`
|
||
(the `ffmpeg_available()` runtime-tool pattern — never a pip dep), then
|
||
`adb install -r`; fall back to copying the APK over MTP for a manual
|
||
tap. APK committed at `lintunes/android/andTunes.apk` with
|
||
`package_data`, read via `importlib.resources` like `export/templates`.
|
||
- [ ] Format gate shaped like `export/web_support.py`: deny-by-default on
|
||
suffix, AIFF and protected AAC refused, anything else unplayable
|
||
converted to **FLAC** (never a lossy re-encode).
|
||
- [ ] Play counts back: the app writes `plays/andtunes-<installid>.json` in
|
||
the per-machine-totals shape of `storage/play_journal.py`; sync copies
|
||
it back and folds it in. The R1 becomes one more machine in the Round 38
|
||
model — no new merge rules.
|
||
- [ ] Retire `Sync Playlist to Rabbit (Auxio)` once andTunes covers it, and
|
||
offer a one-off cleanup of the old `Music/<Playlist>/` folders.
|
||
|
||
## 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.
|