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
+6 -2
View File
@@ -7,8 +7,12 @@ A music player for the Rabbit R1 that never scans anything: LinTunes writes
Legend: `[ ]` todo · `[~]` in progress · `[x]` done.
**Status: phases 1–3 done (2026-09-11, LinTunes rounds 50–51, andTunes
0.2.0).** What's left is the *Parked* list, pulled in as trav finds he needs
it. Built with
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.** 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.
@@ -51,7 +51,7 @@ final class Library {
}
static final class Artist {
String name, sortKey;
String key, name, sortKey;
final ArrayList<Album> albums = new ArrayList<>();
final ArrayList<Track> tracks = new ArrayList<>(); // alphabetical
}
@@ -168,7 +168,9 @@ final class Library {
}
checkFormat(format);
for (Playlist p : lib.playlists) {
for (Long id : playlistIds.get(p.id)) {
List<Long> members = playlistIds.get(p.id);
if (members == null) continue; // two playlists sharing an id
for (Long id : members) {
Track t = lib.byId.get(id);
if (t != null) p.tracks.add(t);
}
@@ -257,32 +259,38 @@ final class Library {
String who = !t.albumArtist.isEmpty() ? t.albumArtist
: !t.artist.isEmpty() ? t.artist : UNKNOWN_ARTIST;
String albumName = t.album.isEmpty() ? UNKNOWN_ALBUM : t.album;
String key = who.toLowerCase(Locale.ROOT) + ""
// Both maps fold case, or "RJD2" and "Rjd2" share one album while
// owning two artist entries — and the second track of that album
// skips the block that would have made the artist. Fetched and
// held here, so no lookup below can come back null.
String whoKey = who.toLowerCase(Locale.ROOT);
Artist artist = artistsByName.get(whoKey);
if (artist == null) {
artist = new Artist();
artist.key = whoKey;
artist.name = who;
artist.sortKey = sortKey(who);
artistsByName.put(whoKey, artist);
artists.add(artist);
}
String key = whoKey + ""
+ albumName.toLowerCase(Locale.ROOT);
Album album = albumsByKey.get(key);
if (album == null) {
album = new Album();
album.key = key;
album.name = albumName;
album.artist = who;
album.artist = artist.name;
album.sortKey = sortKey(albumName);
albumsByKey.put(key, album);
albums.add(album);
Artist artist = artistsByName.get(who);
if (artist == null) {
artist = new Artist();
artist.name = who;
artist.sortKey = sortKey(who);
artistsByName.put(who, artist);
artists.add(artist);
}
artist.albums.add(album);
}
album.tracks.add(t);
if (album.art.isEmpty() && !t.art.isEmpty()) album.art = t.art;
if (t.year > 0 && (album.year == 0 || t.year < album.year)) album.year = t.year;
t.albumRef = album;
artistsByName.get(who).tracks.add(t);
artist.tracks.add(t);
}
for (Album a : albums) {
Collections.sort(a.tracks, (x, y) -> x.disc != y.disc ? x.disc - y.disc
@@ -301,10 +301,10 @@ public class ListActivity extends BaseActivity {
open(PLAYLIST, ((Library.Playlist) row.ref).id);
break;
case ARTIST_ROW:
open(ARTIST, ((Library.Artist) row.ref).name);
open(ARTIST, ((Library.Artist) row.ref).key);
break;
case ALL_SONGS:
open(ARTIST_SONGS, ((Library.Artist) row.ref).name);
open(ARTIST_SONGS, ((Library.Artist) row.ref).key);
break;
case ALBUM_ROW:
open(ALBUM, ((Library.Album) row.ref).key);
+2 -2
View File
@@ -26,8 +26,8 @@ import sys
import zipfile
from pathlib import Path
VERSION_NAME = "0.2.0"
VERSION_CODE = 2
VERSION_NAME = "0.2.1"
VERSION_CODE = 3
PACKAGE = "me.teafry.andtunes"
MIN_SDK = 26