feat: mirror a lossless library to MP3 for iPod sync
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
38f545ac32
commit
177761bb75
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user