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
This commit is contained in:
2026-09-14 13:25:15 -07:00
co-authored by Claude Opus 5
parent def7635a87
commit efa07b05c6
12 changed files with 457 additions and 36 deletions
+37
View File
@@ -1,5 +1,42 @@
## Done
### Round 55 (2026-09-14) — the song that wasn't there (v0.20.4, andTunes 0.2.2)
trav couldn't play the first track of his playlist: shuffle off gave him song
2, 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. Verified on the Rabbit: tapping any song
that *is* there plays exactly that song, shuffle or not.
- [x] **The phantom directory.** `Media/Dionne Farris/` existed as far as gvfs
was concerned — it would list the album inside it — and did not exist at
all per `adb`. Every write into it failed `EIO`, and
`mkdir(exist_ok=True)` saw the phantom and did nothing, so it could never
heal while the mount lived. Freshly made folders worked fine, so it was
stale cache, not a broken mount. Only remounting clears it. That folder
was new because Round 52's retag moved the file under a new artist.
- [x] **A refused copy 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 what trav was hearing. The names come back in
the summary and a dialog, so it is never silent.
- [x] **Planning is a worker** (`AndTunesPlanWorker`). It 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. `disk_usage` rides along, being one more MTP call.
- [x] **The tail of the sync says what it's doing.** "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.
- [x] **The wheel scrolls smoothly, and the other way** (`WheelScroll`). 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. Direction
flipped in lists only; volume on the other screens is unchanged.
### Round 54 (2026-09-14) — one artist spelled two ways (v0.20.3, andTunes 0.2.1)
trav synced to the Rabbit, the sync reported success, and andTunes answered