Files
music-mirror/Dockerfile
T
Emma Thorpe 3141f7ca87
Build and publish container / build (pull_request) Successful in 2m18s
feat: name the mirror so a FAT32 device will take it, and copy album art
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.
2026-08-25 10:29:55 +01:00

34 lines
1.2 KiB
Docker

# Alpine rather than Debian slim purely because of ffmpeg's dependencies.
# Debian's ffmpeg depends on libavdevice, which can render to X11, Wayland and
# OpenGL, so it drags in Mesa and with it LLVM (127 MB) and Z3 (27 MB) to
# encode an MP3 on a headless NAS. Alpine's build brings none of that: 576 MB
# down to 189 MB.
FROM python:3.13-alpine AS runtime
ENV PYTHONUNBUFFERED=1
# ffmpeg does the encoding; the application itself has no Python dependencies.
RUN apk add --no-cache ffmpeg
WORKDIR /app
COPY pyproject.toml README.md ./
COPY music_mirror.py ./
RUN pip install --no-cache-dir . \
&& rm -rf build music_mirror.egg-info
# Runs as root by default so a bind-mounted dataset of any ownership is
# writable. Override with `user:` in compose to run as the dataset's owner.
ENTRYPOINT ["music-mirror"]
# Test stage: the suite runs against this image's own ffmpeg, which is the one
# that ships. Build it with `--target test`; a failing test fails the build.
# The published image is the `runtime` stage above and carries none of this.
FROM runtime AS test
RUN pip install --no-cache-dir pytest
COPY pytest.ini ./
# Host-side tools; not in the runtime image, but the suite covers them.
COPY tools ./tools
COPY tests ./tests
RUN python -m pytest