# music-mirror Maintain a lossy MP3 mirror of a lossless music library. Walks a source library and reproduces it, path for path, as MP3 in a separate tree. Tags and cover art are carried across; sources that are already MP3 are copied rather than re-encoded; mirror files whose source has been deleted are removed. **The source library is never written to** — it is mounted read-only in the supplied compose file, and nothing in the code opens it for writing. The intended use is an iPod. Apple's Music app cannot read FLAC at all, so a converted copy has to exist somewhere; this keeps that copy next to the library on a NAS instead of on a laptop, and keeps it current without a human remembering to do anything. ## How it decides what to do | Situation | Action | | -------------------------------- | ---------------------------------------- | | No mirror file | encode | | Source modified since the mirror | re-encode (a Lidarr quality upgrade) | | Mirror up to date | skip | | Source is already MP3 | copy verbatim | | Source gone | delete the mirror file, prune empty dirs | Freshness is modification time: an encoded file is stamped with its source's mtime, so a file is stale exactly when the two differ. There is no database to fall out of step with the library, which matters when something else — Lidarr, in this case — is the thing that owns and reorganises it. Encodes are written to a temporary file and renamed into place, so an interrupted run cannot leave a truncated MP3 that the next run mistakes for finished work. A lock file in the mirror root stops two passes overlapping. ## Usage ```sh music-mirror --source /music --mirror /music-mp3 # one pass music-mirror --source /music --mirror /music-mp3 --interval 6h # keep running music-mirror --source /music --mirror /music-mp3 --dry-run # report only music-mirror --source /music --mirror /music-mp3 --subdir "Artist/Album" ``` | Option | Environment variable | Default | Meaning | | ------------ | ----------------------- | --------- | ---------------------------------------------- | | `--source` | `MUSIC_MIRROR_SOURCE` | — | Root of the lossless library, read-only | | `--mirror` | `MUSIC_MIRROR_MIRROR` | — | Root of the MP3 mirror | | `--quality` | `MUSIC_MIRROR_QUALITY` | `V0` | LAME VBR level `V0`–`V9`, or kbps e.g. `256` | | `--jobs` | `MUSIC_MIRROR_JOBS` | CPU count | Concurrent encodes | | `--interval` | `MUSIC_MIRROR_INTERVAL` | unset | Repeat forever, e.g. `45m`, `6h`, `1d` | | `--subdir` | — | unset | Limit the pass to one directory; skips pruning | | `--no-prune` | — | off | Keep mirror files whose source has gone | | `--dry-run` | — | off | Report what would change, write nothing | `--subdir` never prunes: a partial pass cannot tell an orphan from a file outside its own scope. Requires `ffmpeg` and `ffprobe` on `PATH`. The container and the Nix package both provide them. ## Running it on TrueNAS Scale `compose.yaml` is a Custom App definition. Adjust the two host paths and the `user:` to match your pool, then add it as a custom app. The image is published to this Gitea's registry on every release: ``` code.emmathe.dev/lyrathorpe/music-mirror:latest ``` Tags are `latest`, the full version, and the truncated `major.minor` and `major` forms; builds that are not releases are published as `sha-`. Point the mirror at its own dataset rather than a directory inside the music dataset — it is derived data, so it wants its own snapshot policy, its own quota, and its own SMB share. The tool refuses to run with a mirror inside the source tree. New Lidarr imports are picked up on the next pass. With `MUSIC_MIRROR_INTERVAL` at `6h` that is the worst case; run `--subdir` by hand if you want an album immediately. ## Nix ```sh nix run .#music-mirror -- --source ./flac --mirror ./mp3 nix build .#music-mirror # the test suite runs as part of the build nix develop # python, pytest and ffmpeg ``` `overlays.default` provides `pkgs.music-mirror`. ## Tests ```sh pytest ``` The tests run real ffmpeg encodes rather than mocking them — the interesting failures are in what ffmpeg actually does with tags, cover art and container formats, and a mock cannot fail that way. They skip if ffmpeg is absent. ## Getting the result onto an iPod The mirror is just a directory of MP3s, so any client will do: - **macOS.** Add the mirror's SMB share to the Music app with _Copy files to Music Media folder_ and _Keep Media folder organised_ both **off**. The Mac then stores a library database and nothing else. Keep the share mounted at a stable path — if it is missing when Music opens, every track shows `!`. - **Linux.** Rhythmbox links `libgpod` and handles iPod sync. An iPod Video (5th generation) predates the models whose database has to be signed, so no firmware-hash trickery is needed. Neither client transcodes at sync time; they copy finished MP3s. Two device-side details worth knowing: the iPod reads cover art from the file's tags and ignores `folder.jpg`, which is why art is embedded here; and volume levelling on the device uses iTunes' Soundcheck tag, not ReplayGain, so ReplayGain tags in the source are not carried over as such.