8c3e554c884ee91a0141f76ee028fb1aadfceddd
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ef59e52bca |
ci: build the image once instead of twice
Build and publish container / build (pull_request) Successful in 4m4s
Runs take fifteen to eighteen minutes, and the log shows why: the image is built twice, in full. The test stage is built by the runner's docker daemon. The runtime stage was then built by docker/build-push-action, which runs under a buildx builder that setup-buildx-action creates in its own container with its own cache. The two share nothing, so the second build spent seventy-six seconds booting buildkit and then installed ffmpeg and the package all over again -- around a hundred and ten seconds for the apk and another hundred for pip, neither of which produced anything the first build had not already made. The comment above the test step claimed those layers were shared, which is what made this look reasonable. buildx earns that overhead when producing several architectures. This produces linux/amd64 only, by an explicit decision recorded in the workflow, so it earns nothing here. Use plain docker build against the same daemon that ran the tests, and push with docker push. The runtime stage is a strict prefix of the test stage, so every layer is a cache hit: measured at 1.3 seconds locally. Identical to the change made in music-curator, whose workflow this one was copied from. |
||
|
|
6f840c2d55 |
fix: use the registry secret that exists, and allow manual releases
Build and publish container / build (pull_request) Successful in 4m16s
The login step referenced PACKAGES_SECRET while the secret configured on this repository -- and on its sibling -- is PACKAGES_TOKEN. The expression resolved to an empty string and docker/login-action failed with "Password required", so the first release published nothing and created no tag. The push trigger only fires on image-affecting paths, which a workflow-only change is not, so merging this fix alone would not produce a release. workflow_dispatch can now cut one, restricted to main so a manual run on another branch cannot tag a commit that is not on the default branch. Verified by running the version step across every event and ref combination: push and workflow_dispatch on main release, everything else does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d4ccff3b75 |
feat: mirror a lossless library to MP3 for iPod sync (#1)
Build and publish container / build (push) Failing after 51s
## What A path-for-path MP3 mirror of a lossless library. FLAC in, MP3 out, same relative layout, tags and cover art carried across. Already-MP3 sources are copied rather than re-encoded. Mirror files whose source has gone are deleted, along with any directory they emptied. The source library is never written to — mounted read-only in the compose file, and never opened for writing in code. | Situation | Action | | -------------------------------- | ------------------------------------ | | No mirror file | encode | | Source modified since the mirror | re-encode (a Lidarr quality upgrade) | | Mirror up to date | skip | | Source is already MP3 | copy verbatim | | Source gone | delete, prune empty dirs | ## Design notes - **No database.** Freshness is mtime: an encode is stamped with its source's mtime, so a file is stale exactly when the two differ. Lidarr owns the library; a second tool with its own index would only fall out of step with it. This is also why it is not beets. - **Atomic writes.** Encode to a temp file, rename into place. An interrupted run cannot leave a truncated MP3 that the next run treats as finished. - **A lock file** in the mirror root stops two passes overlapping. - **Refuses a mirror inside the source tree**, which would otherwise recurse. - `--subdir` never prunes: a partial pass cannot distinguish an orphan from a file outside its scope. ## Shipping - Python package with a `music-mirror` console script, no runtime dependencies beyond ffmpeg. - `Dockerfile` plus `compose.yaml` as a TrueNAS Scale Custom App: source dataset read-only, mirror dataset writable, `MUSIC_MIRROR_INTERVAL=6h`. - CI runs the tests **inside the image**, against the ffmpeg that ships, on every pull request, and on merge publishes multi-arch (amd64 + arm64) to this Gitea's registry using the `PACKAGES_SECRET` repository secret. Versioning follows the same conventional-commit scheme as `legacy-email-proxy`, including writing the released version back into `pyproject.toml` so the packaging metadata cannot drift behind the tag. ## Behaviour under Lidarr | Lidarr does this | The mirror does this | | --------------------------------------- | -------------------------------------------------------------------------------- | | Replaces a file with a better rip | Re-encodes in place; same path in, same path out, so no duplicate | | Upgrades MP3 to FLAC | Both map to the same `.mp3` mirror path, so the old one is overwritten | | Renames a track, album or artist folder | Old path pruned, new path encoded — correct, though it re-encodes rather than moving | | Deletes an album or artist | Every orphaned mirror file goes, and the directories they emptied with them | Three defects were found and fixed while writing those tests, each verified to fail against the previous code: - Pruning probed the source tree for a mirror file's original name, lowercase extensions only. A `.FLAC` source was never found, so its mirror file was deleted as an orphan and rebuilt on the next pass, for ever. Pruning now works from the set of paths the pass actually accounted for. - Two sources could claim one mirror path — `01 Song.flac` beside a leftover `01 Song.mp3`. Both encoded to the same destination and each pass found the loser stale. The better format now wins, ties break on path. - The source mtime was read after encoding rather than before, so a file still being written when the pass reached it could be stamped current while holding truncated audio. ## Why there is no flake The deployment target is a container. A flake here would sit on no path between the source and the NAS, and `nix flake check` would test against nixpkgs' ffmpeg while the artefact ships Debian's — precisely the layer these tests exercise. It was removed in favour of running the suite inside the image. The sibling `legacy-email-proxy` keeps its flake because there the flake *is* the deployment mechanism. ## Verification - 21 tests, all real ffmpeg round-trips: layout, tag survival, MP3 output, skip-when-current, re-encode-on-change, orphan pruning, empty-dir removal, `--no-prune`, MP3 passthrough, `--dry-run`, `--subdir`, external cover art embedding, both refusal paths, and the Lidarr lifecycle cases below. - The suite also runs in the multi-stage Docker `test` stage: 21 passed. The published `runtime` stage carries neither the tests nor pytest, verified by inspecting the image. - Container built and run locally against a sample library: correct output path, Cyrillic tags intact. - The release step was extracted from the workflow and run against a scratch repository: it commits and tags when the version changes, and skips the commit but still tags when `pyproject.toml` already carries it. - **Not tested against the real library** — the first run there should be `--dry-run`. ## Follow-ups, not in this PR - Lidarr imports are picked up on the next scheduled pass rather than instantly. A webhook trigger is the obvious next step if six hours feels slow. - `PACKAGES_SECRET` must exist as a repository secret before the first merge, or the login step fails. --------- Co-authored-by: Emma Thorpe <emma.thorpe@citrix.com> Reviewed-on: #1 |