Lidarr renames an artist or album folder, music-mirror prunes the old path and
encodes the new one, and every Apple Music entry pointing at the old path dies.
Music has no view of those, and the ! only appears once a track is touched.
This will keep happening, so the finder is worth keeping.
Why the XML and not AppleScript
A broken track makes AppleScript's locationraise rather than return missing value, so a bulk query dies on the first one:
...«class cFlT» id 115500 of «class cLiP» id 62... (-1728)
A per-track try loop is correct but costs one Apple event apiece. Music's own File > Library > Export Library gives the whole thing as a plist, which plistlib reads in 1.2s at 50,000 tracks — measured, not guessed.
Why a walk and not exists()
Measured on a synthetic library of her shape, 50,000 tracks in 6,250 album
folders:
Approach
Filesystem operations
path.exists() per track
50,000 stat calls
one os.walk + set lookup
6,951 directory reads
Locally that is only 2.7× wall clock. Over SMB each stat is a network round
trip, while a directory read returns every name in it at once — which is where
the minutes were going.
Two comparisons that had to be loosened
Both would otherwise report most of the library as missing:
Unicode. macOS stores filenames decomposed; the share composes them. Mötley Crüe is two different byte strings depending on which side wrote it.
Normalised to NFC on both sides.
Case. The share is very likely case-insensitive. A file is not missing
because someone capitalised it differently.
It also now separates "missing its file" from "has no file at all" — streaming
entries were being counted as local.
Testing
121 tests, green locally and in docker build --target test. The new ones
cover a deleted file, a decomposed filename, a case difference, a streaming
entry, root derivation and its override, the unmounted-share warning, a healthy
library staying quiet, location decoding, and that the walk really is one read
per directory rather than one per file.
The test stage now copies tools/, since the suite covers it and the runtime
image deliberately does not carry it. No change to what ships.
## Why
Lidarr renames an artist or album folder, music-mirror prunes the old path and
encodes the new one, and every Apple Music entry pointing at the old path dies.
Music has no view of those, and the `!` only appears once a track is touched.
This will keep happening, so the finder is worth keeping.
## Why the XML and not AppleScript
A broken track makes AppleScript's `location` **raise** rather than return
`missing value`, so a bulk query dies on the first one:
...«class cFlT» id 115500 of «class cLiP» id 62... (-1728)
A per-track `try` loop is correct but costs one Apple event apiece. Music's own
`File > Library > Export Library` gives the whole thing as a plist, which
`plistlib` reads in 1.2s at 50,000 tracks — measured, not guessed.
## Why a walk and not `exists()`
Measured on a synthetic library of her shape, 50,000 tracks in 6,250 album
folders:
| Approach | Filesystem operations |
| ---------------------- | ----------------------- |
| `path.exists()` per track | 50,000 stat calls |
| one `os.walk` + set lookup | 6,951 directory reads |
Locally that is only 2.7× wall clock. Over SMB each stat is a network round
trip, while a directory read returns every name in it at once — which is where
the minutes were going.
## Two comparisons that had to be loosened
Both would otherwise report most of the library as missing:
- **Unicode.** macOS stores filenames decomposed; the share composes them.
`Mötley Crüe` is two different byte strings depending on which side wrote it.
Normalised to NFC on both sides.
- **Case.** The share is very likely case-insensitive. A file is not missing
because someone capitalised it differently.
It also now separates "missing its file" from "has no file at all" — streaming
entries were being counted as local.
## Testing
121 tests, green locally and in `docker build --target test`. The new ones
cover a deleted file, a decomposed filename, a case difference, a streaming
entry, root derivation and its override, the unmounted-share warning, a healthy
library staying quiet, location decoding, and that the walk really is one read
per directory rather than one per file.
The test stage now copies `tools/`, since the suite covers it and the runtime
image deliberately does not carry it. No change to what ships.
Lidarr renames an artist or album folder, music-mirror prunes the old path and
encodes the new one, and every entry in Apple Music pointing at the old path
dies. Music offers no view of those, and the exclamation mark only appears once
a track is touched.
Reads the XML from Music's own Export Library rather than asking Music itself.
A broken track makes AppleScript's `location` raise instead of returning a
value, so a bulk query dies on the first one with error -1728 and a per-track
loop costs an Apple event apiece.
Checked against a single directory walk rather than a test per file. On a
fifty-thousand-track library that is around seven thousand directory reads
instead of fifty thousand stat calls, and over SMB each of those stats is a
network round trip -- which is the difference between seconds and minutes.
Two comparisons have to be loosened or most of the library reads as missing.
macOS stores filenames decomposed while the share composes them, so the umlauts
in Motley Crue are two different byte strings depending on which side wrote the
name; both are normalised to NFC. And the share is very likely
case-insensitive, so a file is not missing because someone capitalised it
differently.
The test stage now copies tools/ as well, since the suite covers this and the
runtime image deliberately does not carry it.
All checks were successful
Build and publish container / build (pull_request) Successful in 3m8s
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Why
Lidarr renames an artist or album folder, music-mirror prunes the old path and
encodes the new one, and every Apple Music entry pointing at the old path dies.
Music has no view of those, and the
!only appears once a track is touched.This will keep happening, so the finder is worth keeping.
Why the XML and not AppleScript
A broken track makes AppleScript's
locationraise rather than returnmissing value, so a bulk query dies on the first one:A per-track
tryloop is correct but costs one Apple event apiece. Music's ownFile > Library > Export Librarygives the whole thing as a plist, whichplistlibreads in 1.2s at 50,000 tracks — measured, not guessed.Why a walk and not
exists()Measured on a synthetic library of her shape, 50,000 tracks in 6,250 album
folders:
path.exists()per trackos.walk+ set lookupLocally that is only 2.7× wall clock. Over SMB each stat is a network round
trip, while a directory read returns every name in it at once — which is where
the minutes were going.
Two comparisons that had to be loosened
Both would otherwise report most of the library as missing:
Mötley Crüeis two different byte strings depending on which side wrote it.Normalised to NFC on both sides.
because someone capitalised it differently.
It also now separates "missing its file" from "has no file at all" — streaming
entries were being counted as local.
Testing
121 tests, green locally and in
docker build --target test. The new onescover a deleted file, a decomposed filename, a case difference, a streaming
entry, root derivation and its override, the unmounted-share warning, a healthy
library staying quiet, location decoding, and that the walk really is one read
per directory rather than one per file.
The test stage now copies
tools/, since the suite covers it and the runtimeimage deliberately does not carry it. No change to what ships.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.