chore: add a tool for finding Music tracks whose files have gone
Build and publish container / build (pull_request) Successful in 3m8s
Build and publish container / build (pull_request) Successful in 3m8s
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.
This commit is contained in:
@@ -377,6 +377,43 @@ stubbed. No network, no credentials, no rate limit. On a Nix machine:
|
||||
nix shell nixpkgs#python3Packages.pytest -c pytest
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
Host-side scripts under `tools/`. Not part of the container image; run them
|
||||
wherever they are needed.
|
||||
|
||||
### `find_missing_tracks.py`
|
||||
|
||||
Reports tracks in an Apple Music library whose files are no longer on disk —
|
||||
which happens whenever Lidarr renames an artist or album folder and
|
||||
music-mirror prunes the old path.
|
||||
|
||||
```sh
|
||||
# Music: File > Library > Export Library... then, with the share mounted:
|
||||
python3 tools/find_missing_tracks.py Library.xml --root /Volumes/music-mp3
|
||||
```
|
||||
|
||||
Reading the exported XML rather than asking Music itself is deliberate. A
|
||||
broken track makes AppleScript's `location` raise instead of returning a value,
|
||||
so a bulk query dies on the first one with `-1728` and a per-track loop costs an
|
||||
Apple event apiece.
|
||||
|
||||
It checks against a **single directory walk**, not a test per file. On a
|
||||
50,000-track library that is ~7,000 directory reads instead of 50,000 stat
|
||||
calls, and over SMB every one of those stats is a network round trip.
|
||||
|
||||
Two comparisons that have to be loosened, or most of the library reads 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.
|
||||
Both sides are normalised to NFC.
|
||||
- **Case.** The share is very likely case-insensitive. A file is not missing
|
||||
because someone capitalised it differently.
|
||||
|
||||
Pass `--root` if the library holds anything outside the mirror: the root it
|
||||
otherwise derives is the common parent of every track, which can be `/`.
|
||||
|
||||
## Where this is going
|
||||
|
||||
| Stage | Status |
|
||||
|
||||
Reference in New Issue
Block a user