feat: name the mirror so a FAT32 device will take it, and copy album art
Build and publish container / build (pull_request) Successful in 2m18s

Two changes for playing the mirror on a Rockbox iPod, where the device is FAT32
and Rockbox reads a plain directory tree rather than a database.

--fat32-safe names mirror files acceptably: the reserved characters and control
characters become underscores, trailing dots and spaces are stripped because
FAT eats them silently and the name then round-trips as a different one, and a
component left empty becomes an underscore. Names differing only in case are
detected as collisions, since two files here are one file there and the second
would silently overwrite the first. "Kick Out the Epic Motherf**ker" is a real
example from a real library, and without this it simply never arrives.

Off by default. It renames files, and that should be a decision rather than a
surprise on somebody's next pass.

Turning it on does not re-encode anything. Every track whose name held a
reserved character changes path, and encoding those again would be hours of
work producing files that already exist byte for byte, so the run moves them
instead and logs each one. Prune then finds nothing left behind.

Album art is now also copied into the mirror as cover.jpg beside the tracks.
Rockbox searches the filesystem for art -- cover.jpg, folder.jpg and the rest,
in the track's directory or its parent -- and that search never looks at the
picture embedded in the tag, so a mirror that only embeds art displays none of
it on the device. Embedding continues for the Apple firmware; both are now
satisfied. A cover whose tracks have all been pruned is removed too, or its
directory would never look empty and never go.

Adds tools/check_fat32.py, which reports unacceptable paths before a copy
rather than during one: rsync reports them too, but scattered through fifty
thousand files where they are easy to lose. It exits non-zero so it can gate a
script.

The README documents the rsync invocation, including why --modify-window=2 is
required against FAT and why Rhythmbox must be kept out of the transfer --
rb_ipod_helpers_is_ipod() reads access-protocols from media-player-info and
returns true on the USB id alone, without looking at the filesystem, so
removing iPod_Control changes nothing.
This commit is contained in:
Emma Thorpe
2026-08-25 10:29:55 +01:00
parent d5f4329f46
commit 3141f7ca87
7 changed files with 479 additions and 9 deletions
+71
View File
@@ -93,6 +93,7 @@ music-mirror --source /music --mirror /music-mp3 --subdir "Artist/Album"
| `--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 |
| `--fat32-safe` | `MUSIC_MIRROR_FAT32_SAFE` | off | Name files so a FAT32 device accepts them |
| `--no-prune` | — | off | Keep mirror files whose source has gone |
| `--dry-run` | — | off | Report what would change, write nothing |
@@ -137,6 +138,17 @@ 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.
## Tools
Host-side scripts under `tools/`, not part of the container image.
`check_fat32.py` reports paths a FAT32 device will not accept — reserved
characters, trailing dots and spaces, over-long components and paths, and names
colliding case-insensitively. Run it against the mirror **before** an rsync:
rsync reports the failures too, but scattered through fifty thousand files where
they are easy to miss. Exits non-zero when it finds anything, so it can gate a
script.
## Tests
```sh
@@ -158,6 +170,41 @@ Nix machine:
nix shell nixpkgs#python3Packages.pytest nixpkgs#ffmpeg -c pytest
```
## FAT32 and Rockbox
`--fat32-safe` names mirror files so a FAT32 device will accept them. Off by
default, because turning it on renames files and that should be a decision
rather than a surprise.
What it handles, per path component:
| Problem | Treatment |
| ------------------------------- | ------------------------------ |
| `< > : " \ \| ? *` and control characters | replaced with `_` |
| trailing dots and spaces | stripped — FAT eats them silently, so the name round-trips as a different name |
| a component left empty | becomes `_` |
| names differing only in case | detected and reported; one wins, as with any other collision |
`Dada Life - Kick Out the Epic Motherf**ker` is a real example from a real
library. Without this it simply never arrives on the device.
**Turning it on does not re-encode anything.** Every track whose name held a
reserved character changes path, and re-encoding those would be hours of work
producing files that already exist byte for byte. The run moves them instead,
and says so. Prune then finds nothing to remove because nothing was left
behind.
### Album art
Rockbox looks for cover art **on the filesystem**`cover.jpg`, `folder.jpg`
and friends beside the track or in its parent — and that search never touches
the picture embedded in the tag. So a JPEG cover found beside the source is now
copied into the mirror as `cover.jpg`, in addition to being embedded. The iPod
firmware reads the embedded one; Rockbox reads the file. Both are satisfied.
A cover left behind in a directory whose tracks have all gone is pruned, or the
directory would never look empty and never be removed.
## Getting the result onto an iPod
The mirror is just a directory of MP3s, so any client will do:
@@ -169,6 +216,30 @@ The mirror is just a directory of MP3s, so any client will do:
- **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.
- **Rockbox.** No database to write at all — it reads a plain directory tree
and builds its own index from tags. Copy the mirror across with rsync:
```sh
python3 tools/check_fat32.py /mnt/tank/media/music-mp3 # before, not during
rsync -rtv --delete --modify-window=2 \
/mnt/tank/media/music-mp3/ /media/IPOD/Music/
```
`--modify-window=2` because FAT stores modification times to two-second
resolution; without it rsync re-copies the entire library on every run. `-rt`
rather than `-a` because owners, groups and permissions mean nothing on FAT
and asking for them only produces errors.
Do not route this through Rhythmbox. Its `rb_ipod_helpers_is_ipod()` reads
`access-protocols` from media-player-info first and returns true without
looking at the filesystem at all, so an iPod in disk mode is identified by its
USB id and managed as an iPod — writing a database Rockbox does not want.
Deleting `iPod_Control` does not change this. Either use rsync, or untick
Preferences → Plugins → Portable Players - iPod.
Faster still for the first bulk copy: take the card out of the iFlash adapter
and use a card reader. Fifty thousand files over USB 2.0 through an iPod is a
long evening, and it avoids Rockbox's USB stack entirely.
Neither client transcodes at sync time; they copy finished MP3s.