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
This commit is contained in:
2026-09-14 12:51:40 -07:00
co-authored by Claude Opus 5
parent 370ea498cc
commit def7635a87
12 changed files with 265 additions and 25 deletions
+42
View File
@@ -1,5 +1,47 @@
## Done
### 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
with `No library / Couldn't read library.json`. The sync was fine. Everything
it wrote was on the device and correct — 2,432 media files, 208 covers, a
manifest whose every field type-checks against the parser. The app was the bug,
and it took all 2,439 songs down over fifteen of them.
- [x] **The diagnosis**, read off the device: a `NullPointerException` on
`Library$Artist.tracks` inside `load` (R8 inlines `group()` into it).
`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.
Replaying the algorithm over the real manifest: 15 tracks, six artists
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 worked on the 11th
because the 12th's sync was the first to carry both spellings of one of
those albums.
- [x] **Both maps fold case now**, and the artist is fetched-or-made *before*
the album block and held in a local, so there is no lookup left in
`group()` that can return null. `Artist` gains a `key` (the folded name),
the way `Album` already had one, and `ListActivity` navigates by it —
`ALBUM_ROW` already passed `album.key`, so artists just stopped being the
exception. `playlistIds.get` in `load()` got the same treatment.
- [x] **The device diff folds case too.** `/sdcard` 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 collision rule
folds as well, so two songs whose paths differ only by case get the
`[track_id]` suffix instead of one silently overwriting the other.
`plan.stale` still carries the device's own spelling — that is what
`_delete_stale` unlinks by.
- [x] **The fixture grew the shape** (`scripts/make_dev_library.py`): one
artist spelled two ways on one shared album, next to the other naming
edge cases. `tests/test_round54.py` pins the desktop half (the Java
grouping has no harness — it was verified on the device: 2,439 songs
listed, and RJD2 one row of 12 songs where there had been a crash).
### Round 52 (2026-09-11) — one fingerprint, six artists (v0.20.1)
trav's "Dionne Farris - I Know" kept being identified as Jay-Z, with ID3 tags