chore: add a tool for finding Music tracks whose files have gone #14

Open
lyrathorpe wants to merge 1 commits from tools/find-missing-tracks into main
Owner

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.

## 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.
lyrathorpe added 1 commit 2026-08-24 19:36:42 +01:00
chore: add a tool for finding Music tracks whose files have gone
Build and publish container / build (pull_request) Successful in 3m8s
658c70a3dc
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
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin tools/find-missing-tracks:tools/find-missing-tracks
git checkout tools/find-missing-tracks
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lyrathorpe/music-curator#14