Apple's Music app cannot read FLAC, so getting a lossless library onto an iPod requires a converted copy somewhere. This keeps that copy beside the library on a NAS rather than on a laptop, and keeps it current unattended. Walks a source tree and reproduces it path for path as MP3: tags and cover art carried across, already-MP3 sources copied rather than re-encoded, and mirror files whose source has gone deleted along with any directories they emptied. The source library is never written to. Freshness is tracked by modification time -- an encoded file is stamped with its source's mtime, so a file is stale exactly when the two differ. That keeps runs idempotent without a database that could fall out of step with whatever owns the library, which here is Lidarr. Encodes go to a temporary file and are 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 prevents overlapping passes. Ships as a Python package with a console script, a container image with a TrueNAS Scale compose file, and a Nix flake providing the package, an overlay and a dev shell. The test suite runs real ffmpeg encodes rather than mocks -- the failures worth catching are in what ffmpeg does with tags, cover art and container formats. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
52 lines
1.3 KiB
Nix
52 lines
1.3 KiB
Nix
{
|
|
lib,
|
|
python3Packages,
|
|
makeWrapper,
|
|
ffmpeg,
|
|
}:
|
|
python3Packages.buildPythonApplication {
|
|
pname = "music-mirror";
|
|
inherit ((lib.importTOML ./pyproject.toml).project) version;
|
|
pyproject = true;
|
|
|
|
src = lib.fileset.toSource {
|
|
root = ./.;
|
|
fileset = lib.fileset.unions [
|
|
./music_mirror.py
|
|
./pyproject.toml
|
|
./pytest.ini
|
|
./tests
|
|
];
|
|
};
|
|
|
|
build-system = [ python3Packages.setuptools ];
|
|
nativeBuildInputs = [ makeWrapper ];
|
|
|
|
# ffmpeg and ffprobe are called as subprocesses, so they belong on the
|
|
# wrapper's PATH rather than in the Python environment.
|
|
makeWrapperArgs = [
|
|
"--prefix"
|
|
"PATH"
|
|
":"
|
|
(lib.makeBinPath [ ffmpeg ])
|
|
];
|
|
|
|
nativeCheckInputs = [
|
|
python3Packages.pytestCheckHook
|
|
ffmpeg
|
|
];
|
|
|
|
meta = {
|
|
description = "Maintain a lossy MP3 mirror of a lossless music library";
|
|
longDescription = ''
|
|
Reproduces a lossless library, path for path, as MP3 in a separate tree:
|
|
tags and cover art carried across, sources that are already MP3 copied
|
|
rather than re-encoded, and mirror files whose source has gone deleted.
|
|
The source library is never written to.
|
|
'';
|
|
homepage = "https://code.emmathe.dev/lyrathorpe/music-mirror";
|
|
mainProgram = "music-mirror";
|
|
platforms = lib.platforms.unix;
|
|
};
|
|
}
|