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
4.7 KiB
4.7 KiB
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 onmtk-kpdwith 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 — frameworkActivity,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_AUDIOgrants the audio files but notlibrary.json, which isn't media — that's the whole reason.- Library loader: parse
library.jsonoff the main thread into a singleton that survives between activities. Refuse an unknownformatwith 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.ListActivityin 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>.pngfrom the device folder when present, else the bundled drawable. LinTunes ships defaults intoButtons/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")(theffmpeg_available()runtime-tool pattern — never a pip dep), thenadb install -r; fall back to copying the APK over MTP for a manual tap. APK committed atlintunes/android/andTunes.apkwithpackage_data, read viaimportlib.resourceslikeexport/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>.jsonin the per-machine-totals shape ofstorage/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 oldMusic/<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.