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. Claude-Session: https://claude.ai/code/session_01SKXUgsBBwe3qaHEjeV8ubP
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
"""LinTunes — iTunes-style music library manager and player for Linux."""
|
||||
|
||||
__version__ = "0.20.2"
|
||||
__version__ = "0.20.3"
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
{"version_name": "0.2.0", "version_code": 2}
|
||||
{"version_name": "0.2.1", "version_code": 3}
|
||||
|
||||
@@ -192,9 +192,11 @@ def plan_andtunes_sync(playlists, tracks_by_id, root: Path, device=None,
|
||||
# Same collision rule as the per-playlist sync: every member of a
|
||||
# colliding group gets the [track_id] suffix, so a name never depends on
|
||||
# which playlist happened to be walked first.
|
||||
# Folded, because the device's filesystem is case-insensitive: two songs
|
||||
# whose paths differ only in case are one file there, not two.
|
||||
by_rel: dict[str, list[MediaItem]] = {}
|
||||
for item in items.values():
|
||||
by_rel.setdefault(item.rel, []).append(item)
|
||||
by_rel.setdefault(item.rel.lower(), []).append(item)
|
||||
for group in by_rel.values():
|
||||
if len(group) > 1:
|
||||
for item in group:
|
||||
@@ -231,19 +233,27 @@ def plan_andtunes_sync(playlists, tracks_by_id, root: Path, device=None,
|
||||
expected |= {art_relpath(key) for key in plan.albums}
|
||||
expected.add(MANIFEST_NAME)
|
||||
|
||||
# Compared case-folded: the device filesystem is case-insensitive, so a
|
||||
# song filed under "Toro y Moi" here and "Toro Y Moi" there is the same
|
||||
# file. Exact-string matching saw it as stale *and* missing and re-copied
|
||||
# it over MTP every single sync.
|
||||
on_device_folded = {rel.lower(): size for rel, size in on_device.items()}
|
||||
expected_folded = {rel.lower() for rel in expected}
|
||||
|
||||
for item in plan.items:
|
||||
if item.convert_to:
|
||||
# Its FLAC size is unknown until the worker converts it.
|
||||
item.device_size = on_device.get(item.rel)
|
||||
item.device_size = on_device_folded.get(item.rel.lower())
|
||||
plan.copies.append(item)
|
||||
plan.bytes_to_copy += item.size
|
||||
elif on_device.get(item.rel) == item.size:
|
||||
elif on_device_folded.get(item.rel.lower()) == item.size:
|
||||
plan.kept += 1
|
||||
else:
|
||||
plan.copies.append(item)
|
||||
plan.bytes_to_copy += item.size
|
||||
# plan.stale keeps the device's own spelling — _delete_stale unlinks by it.
|
||||
for rel, size in sorted(on_device.items()):
|
||||
if rel not in expected:
|
||||
if rel.lower() not in expected_folded:
|
||||
plan.stale.append(rel)
|
||||
plan.bytes_freed += size
|
||||
return plan
|
||||
|
||||
Reference in New Issue
Block a user