Files
lintunes/andtunes/TASKS.md
T
travandClaude Opus 5 efa07b05c6 v0.20.4: the song that wasn't there
trav couldn't play the first track of his playlist. Shuffle off gave him song
two; shuffle on gave him a random one. The player was right both times:
library.json named a song whose file was not on the device, and skipping an
unplayable file advances one slot. Tapping any song that *is* there plays
exactly that song, shuffle or not.

The file was missing because a gvfs-MTP mount can hold a phantom directory --
one it lists happily while the device has no such folder. Every write into it
fails EIO, and mkdir(exist_ok=True) sees the phantom and does nothing, so it
never heals; only remounting clears it. That folder was new because Round 52's
retag moved the file under a new artist.

So a copy that raises OSError now costs that song, not the sync, and the song
is left out of the m3u and library.json. Aborting cost 2,400 songs for one
folder; counting it present anyway put a song in the manifest that isn't on
the device, which is the bug you could hear. The names come back in the
summary and a dialog rather than vanishing.

Planning is a worker now. plan_andtunes_sync is pure and writes nothing, but
it walks every file on the device, and over MTP that is thousands of round
trips -- on the GUI thread it froze the window and GNOME offered to kill
LinTunes, which is how a sync got force-quit halfway through.

And the sync stopped going quiet at the end. "Album art 501/501" is emitted
before the last album, and then _write_index, _ship_buttons and
_prune_empty_dirs ran silently: 501 exists() stats over MTP, ~840 KB of
writes and a full tree walk, with the progress line frozen. They report now,
and _write_index reads Art/ in one listing instead of a stat per album.

andTunes 0.2.2: the wheel scrolls the other way in lists, and smoothly --
a detent adds to a pixel debt that a Choreographer callback pays off a
fraction per frame, so one detent eases to a stop and a fast spin blends into
one movement instead of teleporting a row at a time. Volume is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKXUgsBBwe3qaHEjeV8ubP
2026-09-14 13:25:15 -07:00

6.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: phases 1–3 done (2026-09-11, LinTunes rounds 50–51, andTunes 0.2.0); 0.2.1 (LinTunes round 54, 2026-09-14) fixed the grouping crash — Library.group() keyed albums case-folded but artists raw-case, so one artist spelled two ways on one album ("RJD2" / "Rjd2") took the whole library down with Couldn't read library.json. Both maps fold case now, and Artist has a key the way Album always did. 0.2.2 (round 55, 2026-09-14) reversed the wheel in lists and made it scroll smoothly (WheelScroll — a pixel debt paid off per frame, not a row per detent); volume on the other screens is unchanged. What's left is the Parked list, pulled in as trav finds he needs it. Built with python3 andtunes/build.py — plain Java, no Gradle, nothing downloaded (see README.md, Toolchain). 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, mapped by the stock Generic.kl (checked 2026-09-11 via dumpsys input) to KEYCODE_DPAD_UP / DPAD_DOWN — not volume. Side/PTT button sits on mtk-kpd with KEY_VOLUMEDOWN + KEY_POWER. Headset exposes KEY_PLAYPAUSE, so a MediaSession gets media keys for free.
  • USB: set to come up as mtp,adb whenever the screen is unlocked (adb shell svc usb setScreenUnlockedFunctions mtp, 2026-09-11), so the gvfs mount sync uses and the adb install uses are both there at once.

Phase 1 — plays a song (v0.1.0) ✔

  • java-21-openjdk-devel — installed, javac on PATH. (Java 25 is also present; build.py points at 21.)
  • Android SDK: platform 33 + build-tools 34.0.0 were already in ~/Android/Sdk. No Gradle, no Kotlin — build.py runs aapt2 → javac → R8 → zipalign → apksigner directly (trav, 2026-09-11: don't pull a GB over cell if it isn't needed; it isn't).
  • Project, package me.teafry.andtunes, minSdk 26 / targetSdk 33, framework only: Activity, ListView, MediaPlayer, MediaSession, Notification.MediaStyle, R8. The APK is 61 KiB.
  • MANAGE_EXTERNAL_STORAGE + a first-run screen that sends the user to the toggle (LinTunes' installer grants it over adb, so it rarely shows).
  • Library loader: android.util.JsonReader streaming parse off the main thread into a process singleton; artists/albums grouped in one pass; an unknown format gets a readable message; reloads when library.json changes (a re-sync) the next time the menu resumes.
  • 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, repeat. No back button.
  • Audio focus, ACTION_AUDIO_BECOMING_NOISY, MediaSession + notification.
  • End of queue pauses unless repeat is on. Repeat: off / all / one.
  • adb shell am start -W: 313–338 ms cold to the menu (551 ms on the very first launch after install, which includes dex verification).

Phase 2 — the rest of the screens ✔

  • Playlists, artists (→ "All songs" + albums; all songs has its own shuffle), albums, search (artists, albums and songs as you type).
  • Album art thumbs in album rows, full art on now playing.
  • The "Now playing" bar at the bottom of every list screen; the playing song is drawn inverted in any list it appears in.
  • Scroll wheel → list scrolling (one row per detent), volume on the menu and now-playing screens. Taken in dispatchKeyEvent, DPAD and VOLUME codes, down and up, so the search box can't swallow it.
  • Replaceable button images: Buttons/<name>.png beats the bundled drawable, for the six menu tiles plus ◀ ▶ ⏯. LinTunes ships the defaults into Buttons/ only where the file is missing.
  • Missing file (synced then deleted) skips with a toast, doesn't stop the queue.
  • Resume last queue + track + position on launch (paused).

Phase 3 — install from LinTunes, counts coming back

  • Connections → Install andTunes on Rabbit… (LinTunes 0.19.0): adb install -r + the two permission grants; says Install / Update / Reinstall from the version on the device; with no adb, copies the APK to Download/ over MTP for a manual tap. APK committed at lintunes/android/andTunes.apk (+ andTunes.json, its version).
  • Format gate (LinTunes 0.20.0): sync reuses export/web_support.conversion_for outright, because Android's MediaPlayer decodes the same set a browser does. Protected AAC is refused and reported; ALAC, AIFF and anything else unplayable becomes FLAC (never a lossy re-encode). Conversions are cached per track, so they're converted once and size-diffed after that.
  • Play counts back (andTunes 0.2.0): a natural finish counts a play, as it does in LinTunes. The app keeps plays/andtunes-<install id>.json in the per-machine-totals shape of storage/play_journal.py. Sync folds it into <data_dir>/plays/ with the per-track max before copying anything, so the R1 is one more machine in the Round 38 model and there are no new merge rules.
  • Retired Sync Playlist to Rabbit (Auxio) from the menu. (trav's R1 has no old Music/<Playlist>/ folders, so no cleanup was needed.)

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.