The iPod is now running Rockbox on an iFlash card: a FAT32 device that reads a
plain directory tree and builds its own index from tags. Two things about the
mirror do not survive that trip.
FAT32-safe naming
--fat32-safe, off by default because it renames files and that should be a
decision rather than a surprise.
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
reported as a collision
Dada Life - Kick Out the Epic Motherf**ker is a real track in a real library.
Without this it never arrives on the device, and rsync's complaint about it is
one line somewhere in a run of 49,503 files.
Turning it on does not re-encode anything. Every track whose name held a
reserved character changes path. Encoding those again would be hours of work
producing files that already exist byte for byte, so the run moves them and
logs each one. A test asserts the bytes and the mtime survive, so a future
refactor cannot quietly turn this back into a re-encode.
Album art
Verified in apps/recorder/albumart.c: Rockbox's art search is filesystem-only
— cover.jpg, folder.jpg, <album>.jpg, cover.bmp, in the track's
directory or its parent. That module never touches the picture embedded in the
tag.
The mirror embedded art and copied no image, so covers would not have shown.
A JPEG cover found beside the source is now copied in as cover.jpg as well as
being embedded — the Apple firmware reads the tag, Rockbox reads the file. A
cover whose tracks have all been pruned is removed too, or its directory would
never look empty and never go.
tools/check_fat32.py
Reports unacceptable paths before the copy: reserved characters, trailing
dots and spaces, over-long components and paths, and case-insensitive
collisions. Exits non-zero so it can gate a script.
Documented
The rsync invocation, in the existing iPod section:
--modify-window=2 because FAT stores mtimes to two-second resolution and
rsync otherwise re-copies the whole library every run. -rt rather than -a
because owners and permissions mean nothing on FAT.
Also documented: 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
deleting iPod_Control does not help, which is the obvious thing to try.
Testing
51 tests, green locally and in docker build --target test. New coverage: nine
sanitiser cases, the flag on and off, rename-not-re-encode, case collisions,
cover copying, cover permissions, cover pruning, and seven for the checker.
Also: the transfer itself
tools/sync-to-ipod.sh — submit scrobbles, check, rsync, sync, unmount. The
only manual step left is Menu+Select then Select+Play.
The guards are the substance
rsync --delete is being aimed at a whole filesystem. It refuses unless the
destination exists, is its own mount point, and is a FAT filesystem; and
unless the mirror is non-empty and is not the destination. Emptying the wrong
directory is not a mistake that announces itself.
It also excludes /.rockbox, the scrobbler logs and the usual filesystem
metadata directories. The first draft of this script would have deleted the
Rockbox installation on a sync to the card root, because the mirror does not
contain it.
The unmount is why this is a script rather than a README line. FAT32 has no
journal, disk mode is dumb mass storage, and an interrupted write is corruption
that needs fsck.vfat from another machine.
tools/submit_scrobbles.py
Sends the Rockbox scrobbler log to Last.fm, then sets it aside. Runs before
the copy — the plays already happened, and a failed transfer is no reason to
lose them too.
Verified against Rockbox's own writer in apps/plugins/lastfm_scrobbler.c:
AUDIOSCROBBLER 1.1, tab-separated, artist / album / title / tracknum / length / rating / timestamp / mbid, rated L listened or S skipped. Only L is a
play.
Three things that differ from every other Last.fm call in these projects:
It is a write method. Needs LASTFM_API_SECRET and a session key
obtained once through the browser, not just the read-only key. Skipped with a
note when unset.
Targets without a real-time clock get /.scrobbler-timeless.log with
every timestamp set to zero. Counted and reported, never submitted —
scrobbling them would mean inventing when they happened.
Signatures sort parameter names by ASCII, not numerically, so artist[10] precedes artist[1]. Getting that wrong yields an invalid
signature and no other symptom, so there is a test pinning it.
The log is renamed rather than deleted once accepted. If Last.fm quietly
dropped something, the evidence is still on the device.
Testing
65 tests, green locally and in docker build --target test. shellcheck clean.
## Why
The iPod is now running Rockbox on an iFlash card: a FAT32 device that reads a
plain directory tree and builds its own index from tags. Two things about the
mirror do not survive that trip.
## FAT32-safe naming
`--fat32-safe`, off by default because it renames files and that should be a
decision rather than a surprise.
| 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 | reported as a collision |
`Dada Life - Kick Out the Epic Motherf**ker` is a real track in a real library.
Without this it never arrives on the device, and rsync's complaint about it is
one line somewhere in a run of 49,503 files.
**Turning it on does not re-encode anything.** Every track whose name held a
reserved character changes path. Encoding those again would be hours of work
producing files that already exist byte for byte, so the run **moves** them and
logs each one. A test asserts the bytes and the mtime survive, so a future
refactor cannot quietly turn this back into a re-encode.
## Album art
Verified in `apps/recorder/albumart.c`: Rockbox's art search is filesystem-only
— `cover.jpg`, `folder.jpg`, `<album>.jpg`, `cover.bmp`, in the track's
directory or its parent. That module never touches the picture embedded in the
tag.
The mirror embedded art and copied no image, so covers would not have shown.
A JPEG cover found beside the source is now copied in as `cover.jpg` as well as
being embedded — the Apple firmware reads the tag, Rockbox reads the file. A
cover whose tracks have all been pruned is removed too, or its directory would
never look empty and never go.
## tools/check_fat32.py
Reports unacceptable paths **before** the copy: reserved characters, trailing
dots and spaces, over-long components and paths, and case-insensitive
collisions. Exits non-zero so it can gate a script.
## Documented
The rsync invocation, in the existing iPod section:
python3 tools/check_fat32.py /mnt/tank/media/music-mp3
rsync -rtv --delete --modify-window=2 \
/mnt/tank/media/music-mp3/ /media/IPOD/Music/
`--modify-window=2` because FAT stores mtimes to two-second resolution and
rsync otherwise re-copies the whole library every run. `-rt` rather than `-a`
because owners and permissions mean nothing on FAT.
Also documented: 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
deleting `iPod_Control` does not help, which is the obvious thing to try.
## Testing
51 tests, green locally and in `docker build --target test`. New coverage: nine
sanitiser cases, the flag on and off, rename-not-re-encode, case collisions,
cover copying, cover permissions, cover pruning, and seven for the checker.
---
## Also: the transfer itself
`tools/sync-to-ipod.sh` — submit scrobbles, check, rsync, sync, unmount. The
only manual step left is Menu+Select then Select+Play.
### The guards are the substance
`rsync --delete` is being aimed at a whole filesystem. It refuses unless the
destination exists, is **its own mount point**, and is a FAT filesystem; and
unless the mirror is non-empty and is not the destination. Emptying the wrong
directory is not a mistake that announces itself.
It also excludes `/.rockbox`, the scrobbler logs and the usual filesystem
metadata directories. **The first draft of this script would have deleted the
Rockbox installation** on a sync to the card root, because the mirror does not
contain it.
The unmount is why this is a script rather than a README line. FAT32 has no
journal, disk mode is dumb mass storage, and an interrupted write is corruption
that needs `fsck.vfat` from another machine.
## `tools/submit_scrobbles.py`
Sends the Rockbox scrobbler log to Last.fm, then sets it aside. Runs **before**
the copy — the plays already happened, and a failed transfer is no reason to
lose them too.
Verified against Rockbox's own writer in `apps/plugins/lastfm_scrobbler.c`:
AUDIOSCROBBLER 1.1, tab-separated, `artist / album / title / tracknum / length /
rating / timestamp / mbid`, rated `L` listened or `S` skipped. Only `L` is a
play.
Three things that differ from every other Last.fm call in these projects:
- **It is a write method.** Needs `LASTFM_API_SECRET` and a session key
obtained once through the browser, not just the read-only key. Skipped with a
note when unset.
- **Targets without a real-time clock** get `/.scrobbler-timeless.log` with
every timestamp set to zero. Counted and reported, never submitted —
scrobbling them would mean inventing when they happened.
- **Signatures sort parameter names by ASCII, not numerically**, so
`artist[10]` precedes `artist[1]`. Getting that wrong yields an invalid
signature and no other symptom, so there is a test pinning it.
The log is renamed rather than deleted once accepted. If Last.fm quietly
dropped something, the evidence is still on the device.
## Testing
65 tests, green locally and in `docker build --target test`. `shellcheck` clean.
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.
tools/sync-to-ipod.sh does the whole transfer to a Rockbox device, so the only
manual part left is the disk-mode button sequence.
The guards are the substance rather than decoration. rsync --delete is being
aimed at a whole filesystem, so the destination must exist, be its own mount
point, and be a FAT filesystem; the mirror must be non-empty and must not be
the destination. Emptying the wrong directory is not a mistake that announces
itself.
It also excludes /.rockbox, the scrobbler logs and the usual filesystem
metadata directories. The mirror does not contain them, so a sync to the card
root would otherwise have deleted the Rockbox installation -- which the first
draft of this script would have done.
The unmount is why this is a script at all. FAT32 has no journal, the device is
reached through the Apple firmware's disk mode because Rockbox's own mass
storage is unreliable on an iFlash, and an interrupted write is corruption that
needs fsck.vfat from another machine.
tools/submit_scrobbles.py sends the Rockbox scrobbler log to Last.fm and sets
it aside. Rockbox writes it in AUDIOSCROBBLER 1.1: tab-separated, one line per
track, rated L for listened or S for skipped, and only the listened ones are a
play. It runs before the copy, because the plays already happened and a failed
transfer is no reason to lose them as well.
Two things there differ from every other Last.fm call in these projects.
Scrobbling is a write method, so it needs the API secret and a session key
obtained once through the browser rather than the read-only key. And a target
with no real-time clock gets /.scrobbler-timeless.log with every timestamp set
to zero; those are counted and reported but never sent, since submitting them
would mean inventing when they happened.
Signature generation sorts parameter names by the ASCII table rather than
numerically, so artist[10] precedes artist[1]. Sorting them the obvious way
produces an invalid signature and no other symptom, so there is a test for it.
The log is renamed rather than deleted once accepted, so that if Last.fm
quietly dropped something the evidence is still on the device.
lyrathorpe
changed title from feat: name the mirror so a FAT32 device will take it, and copy album art to feat: FAT32-safe mirror, album art, and a Rockbox sync script2026-08-25 11:05:31 +01:00
The option table listed it, but compose.yaml is what actually gets copied into
a TrueNAS Custom App, so an option absent from there is an option nobody finds.
Quoted deliberately: an unquoted yes or true is a YAML 1.1 boolean, and compose
rejects a boolean as an environment value outright.
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
The iPod is now running Rockbox on an iFlash card: a FAT32 device that reads a
plain directory tree and builds its own index from tags. Two things about the
mirror do not survive that trip.
FAT32-safe naming
--fat32-safe, off by default because it renames files and that should be adecision rather than a surprise.
< > : " \ | ? *and control characters__Dada Life - Kick Out the Epic Motherf**keris a real track in a real library.Without this it never arrives on the device, and rsync's complaint about it is
one line somewhere in a run of 49,503 files.
Turning it on does not re-encode anything. Every track whose name held a
reserved character changes path. Encoding those again would be hours of work
producing files that already exist byte for byte, so the run moves them and
logs each one. A test asserts the bytes and the mtime survive, so a future
refactor cannot quietly turn this back into a re-encode.
Album art
Verified in
apps/recorder/albumart.c: Rockbox's art search is filesystem-only—
cover.jpg,folder.jpg,<album>.jpg,cover.bmp, in the track'sdirectory or its parent. That module never touches the picture embedded in the
tag.
The mirror embedded art and copied no image, so covers would not have shown.
A JPEG cover found beside the source is now copied in as
cover.jpgas well asbeing embedded — the Apple firmware reads the tag, Rockbox reads the file. A
cover whose tracks have all been pruned is removed too, or its directory would
never look empty and never go.
tools/check_fat32.py
Reports unacceptable paths before the copy: reserved characters, trailing
dots and spaces, over-long components and paths, and case-insensitive
collisions. Exits non-zero so it can gate a script.
Documented
The rsync invocation, in the existing iPod section:
--modify-window=2because FAT stores mtimes to two-second resolution andrsync otherwise re-copies the whole library every run.
-rtrather than-abecause owners and permissions mean nothing on FAT.
Also documented: why Rhythmbox must be kept out of the transfer.
rb_ipod_helpers_is_ipod()readsaccess-protocolsfrom media-player-info andreturns true on the USB id alone, without looking at the filesystem — so
deleting
iPod_Controldoes not help, which is the obvious thing to try.Testing
51 tests, green locally and in
docker build --target test. New coverage: ninesanitiser cases, the flag on and off, rename-not-re-encode, case collisions,
cover copying, cover permissions, cover pruning, and seven for the checker.
Also: the transfer itself
tools/sync-to-ipod.sh— submit scrobbles, check, rsync, sync, unmount. Theonly manual step left is Menu+Select then Select+Play.
The guards are the substance
rsync --deleteis being aimed at a whole filesystem. It refuses unless thedestination exists, is its own mount point, and is a FAT filesystem; and
unless the mirror is non-empty and is not the destination. Emptying the wrong
directory is not a mistake that announces itself.
It also excludes
/.rockbox, the scrobbler logs and the usual filesystemmetadata directories. The first draft of this script would have deleted the
Rockbox installation on a sync to the card root, because the mirror does not
contain it.
The unmount is why this is a script rather than a README line. FAT32 has no
journal, disk mode is dumb mass storage, and an interrupted write is corruption
that needs
fsck.vfatfrom another machine.tools/submit_scrobbles.pySends the Rockbox scrobbler log to Last.fm, then sets it aside. Runs before
the copy — the plays already happened, and a failed transfer is no reason to
lose them too.
Verified against Rockbox's own writer in
apps/plugins/lastfm_scrobbler.c:AUDIOSCROBBLER 1.1, tab-separated,
artist / album / title / tracknum / length / rating / timestamp / mbid, ratedLlistened orSskipped. OnlyLis aplay.
Three things that differ from every other Last.fm call in these projects:
LASTFM_API_SECRETand a session keyobtained once through the browser, not just the read-only key. Skipped with a
note when unset.
/.scrobbler-timeless.logwithevery timestamp set to zero. Counted and reported, never submitted —
scrobbling them would mean inventing when they happened.
artist[10]precedesartist[1]. Getting that wrong yields an invalidsignature and no other symptom, so there is a test pinning it.
The log is renamed rather than deleted once accepted. If Last.fm quietly
dropped something, the evidence is still on the device.
Testing
65 tests, green locally and in
docker build --target test.shellcheckclean.feat: name the mirror so a FAT32 device will take it, and copy album artto feat: FAT32-safe mirror, album art, and a Rockbox sync script