720f5c10a2f5a056a29b94abc3f809dcb9ce7c94
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f091a545f5 |
v0.20.5: the wheel learns to coast
getevent settled what the wheel can and can't tell us: och1970_holl_key advertises KEY_UP and KEY_DOWN and nothing else -- no REL_WHEEL, no resolution, because the och1970 is a Hall latch the driver quantises into clicks. There is no sub-detent position to read without a kernel driver change, so all the nuance has to come from *when* the detents arrive. So a detent is now an impulse into a velocity rather than a jump. The list coasts and eases out, and an impulse is worth up to 8x more when detents come 30 ms apart than when they come 220 ms apart -- squared, so the gain stays out of the way while you're hunting for one row. Measured on the device: one unhurried detent moves about half a row, twelve fast ones move forty-odd. Turning back the other way kills the coast first, so a correction bites instead of fighting the glide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKXUgsBBwe3qaHEjeV8ubP |
||
|
|
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 |
||
|
|
def7635a87 |
v0.20.3: one artist spelled two ways
The sync was fine. Everything it put on the Rabbit was there and correct -- 2,432 media files, 208 covers, a manifest whose every field type-checks against the parser -- and andTunes still answered "No library / Couldn't read library.json". Fifteen tracks took all 2,439 songs down with them. Library.group() keyed albumsByKey case-folded and artistsByName raw-case, and only ever built the Artist inside `if (album == null)`. So the second track of an album whose artist is spelled differently found the album already there, skipped the block that would have made the artist, and dereferenced the null that came back. Six artists in trav's library are spelled two ways: RJD2/Rjd2, Toro Y Moi/Toro y Moi, FatBoy Slim/Fatboy Slim, LOVING/Loving, Land Of The Loops/Land of the Loops, Salami Rose Joe Louis/salami rose joe louis. It had worked until the 12th because that sync was the first to carry both spellings of one of those albums. Both maps fold case now, and the artist is fetched-or-made before the album block and held, so no lookup left in group() can come back null. Artist gains a key the way Album always had one, and ListActivity navigates by it -- ALBUM_ROW already passed album.key, so artists just stopped being the exception. The device filesystem is case-insensitive, so those two spellings are one folder there. plan_andtunes_sync compared exact strings and saw every such file as stale *and* missing, deleting and re-copying it over MTP on every sync forever; the diff and the collision rule both fold now. plan.stale still carries the device's own spelling, since that is what _delete_stale unlinks by. Verified on the Rabbit: 2,439 songs listed, RJD2 one row of 12 songs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKXUgsBBwe3qaHEjeV8ubP |
||
|
|
81b7f6767c |
v0.20.0: the Rabbit's plays come home
andTunes phase 3. The R1 is now one more machine in the Round 38 play-count model, and nothing lands on it that Android can't play. Play counts: andTunes 0.2.0 counts a play on a natural finish (LinTunes' rule) and keeps Music/andTunes/plays/andtunes-<install id>.json in exactly the per-machine-totals journal shape, written beside the old one and renamed over it. Each sync first folds it into <data_dir>/plays/ with a new play_journal.merge_totals, the per-track max: the file now has two homes and both desktops may bring it back, so the max converges and an older copy can never pull a count down. Nothing is written when nothing moved, and PlayJournal.load needed no change. Format gate: the planner reuses export/web_support.conversion_for outright, since Android's MediaPlayer decodes the browser's set. FairPlay is refused and reported; ALAC, AIFF and oddities land as FLAC, converted once into a per-track cache and size-diffed after that. With no ffmpeg, the export's "sync without them?" question. A file ffmpeg can't read costs that song, not the sync. "Sync Playlist to Rabbit (Auxio)" is retired from the menu; device_sync's helpers stay because export and andTunes import them. The dev fixture grew a real ALAC track, a Protected AAC .m4p and an "Odd Formats" playlist. Verified on the R1 with it: AIFF and ALAC arrived as FLAC and played, the FairPlay track was refused, four plays came back on the next sync and each track's effective count rose by exactly one, and a third sync copied nothing. Found along the way: USB re-enumeration can wedge gvfsd-mtp, after which anything touching the mount (find_device, the GUI tests) hangs in an uninterruptible wait. CLAUDE.md now has the recovery. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ZCVBTJRFJ2XfMshu2gtUv |
||
|
|
9483d8951b |
v0.19.0: andTunes plays music on the Rabbit
The Android half of andTunes had been paused on a 1 GB toolchain download (Gradle wrapper, Android Gradle Plugin, Kotlin). None of that was needed: platform 33 and build-tools 34 were already in ~/Android/Sdk, and an app that links no libraries needs five SDK steps, not a build system. andtunes/build.py runs aapt2 -> javac -> R8 -> zipalign -> apksigner and produces a 61 KiB APK. andTunes 0.1.0 is framework-only Java: a six-tile menu that loads nothing, a streaming library.json parse grouped into artists and albums in one pass, one ListActivity for every list (songs and artist songs open with Shuffle all, albums carry art thumbs, search as you type), a Now playing bar under every list, and a white Now Playing screen with square art, a slim scrubber, prev/play/next, shuffle and repeat, and no back button. A foreground PlaybackService owns the queue: audio focus, becoming-noisy, MediaSession and a MediaStyle notification, end of queue pauses, missing files skip, and the last queue and position come back on launch. Black on white, and every size doubled for the R1's override density. Cold start to the menu: 313-338 ms. The scroll wheel turned out to be DPAD_UP/DOWN (stock Generic.kl), not volume. The app takes both in dispatchKeyEvent: lists scroll a row per detent, and the menu and Now Playing get volume. LinTunes side: Connections > Install andTunes on Rabbit... installs the committed APK over adb and grants all-files access, or copies it to Download/ over MTP when there's no adb. Sync fills Buttons/ with default artwork only where a file is missing, so trav's own art is never overwritten. Verified against the dev library only, synced to the R1 and driven with adb input + screencap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSsYtRn4WZx6xj4GEdVPt9 |
||
|
|
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 |
||
|
|
cc1ca5ef78 |
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 |