Files
lintunes/andtunes/TASKS.md
T
travandClaude Opus 5 bcba621f3f v0.17.1: a library to break
Round 47 was verified by loading trav's real 21,531-track library. Nothing was
written and nothing was at risk, but it's the wrong habit and he said so. The
alternative is better for development anyway: reproducible, in-repo, and full
of the awkward cases on purpose rather than by luck.

scripts/make_dev_library.py builds a complete synthetic library — data dir and
music tree — from ffmpeg sine waves in six containers, with art on some albums
and not others, every playlist type (regular, folder, live/non-live/
unsupported/limited+nested smart, system, empty, duplicates), a play journal
and a tombstone. ~4 MB, gitignored; the generator is the artifact worth
keeping, not the sine waves.

The edge cases are the point. A collision pair identical in artist/album/title/
number, slashes and colons in every name, a track with no artist, a multi-disc
release, a compilation whose album_artist differs, a dangling location, a
208-character title, and non-ASCII plus an emoji all the way out to the m3u
filename. CLAUDE.md now carries the rule: never develop against the real
library, and when a feature needs a shape the fixture lacks, add it here.

Using it found two bugs in it — a relative --out made every location resolve
against the wrong root, and four-minute uncompressed clips made it 62 MB.

andTunes is paused on a weak connection (the Android SDK is a ~1-1.5 GB
one-time download; after that --offline builds need nothing). andtunes/
README.md now carries everything needed to resume cold: measured device facts,
the dp trap, exact toolchain commands, locked design decisions, and the
library.json contract. JDK 21 is installed and ticked off.

Also answers the scroll wheel question: it reads as volume because the ROM's
key layout maps the wheel's KEY_UP/KEY_DOWN to KEYCODE_VOLUME_UP/DOWN, but a
focused activity sees key events first — so andTunes can claim it by consuming
both those and DPAD_UP/DOWN, in onKeyDown and onKeyUp. No system file touched,
no other app affected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wrTys5U1fLb4pBD2LKWzV
2026-09-10 00:49:26 -04:00

5.6 KiB
Raw Blame History

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: paused 2026-09-10 on a weak connection — the Android SDK is a ~1–1.5 GB download. Everything needed to resume cold is in README.md: device facts, toolchain commands, locked design decisions, and the exact shape of the library.json LinTunes already writes. 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.
  • 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.

Phase 1 — plays a song (v0.1.0)

  • java-21-openjdk-devel — installed, javac on PATH. (Java 25 is also present; AGP won't run on it, so point the build at 21.)
  • Android SDK — the one blocker. commandline-tools + platform 33 + build-tools into ~/Android/Sdk, then a first Gradle build to warm the wrapper and AGP caches. ~1–1.5 GB all told; after that ./gradlew --offline assembleDebug needs no connection. Exact commands in README.md.
  • 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.

Phase 2 — 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. Consume KEYCODE_VOLUME_UP/DOWN and DPAD_UP/DOWN in both onKeyDown and onKeyUp — the ROM's key layout decides which arrives, and claiming them in a focused activity affects no other app. First check /system/usr/keylayout/ on the device; see README.md.
  • 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.

Phase 3 — 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.