lidarr-musefs
A Lidarr integration that syncs Lidarr's metadata into a musefs SQLite store, so a live musefs mount shows a re-tagged view of your library without Lidarr ever copying, moving, or rewriting backing audio bytes.
Lidarr stays the downloader, matcher, and metadata source; its destination tree becomes a placeholder of symlinks that exists only so Lidarr can track files. Point Navidrome, Plex, Jellyfin, or other consumers at the musefs mount instead.
How it fits together
The package installs two console scripts that plug into Lidarr's hooks:
musefs-lidarr-import(Import Using Script) — replaces Lidarr's own copy/move when it imports a download: it creates the destination entry as a symlink (or hardlink) to the downloaded file and fails closed — it never falls back to copying bytes.musefs-lidarr-sync(Custom Script notification) — fires after an import or rename: it queries Lidarr's API for the affected tracks' metadata (title, artist/albumartist, album, track/disc numbers, release date, MusicBrainz ids, genres) plus each album's cover art, runsmusefs scanon the files to create/refresh their track rows (the structural columns only musefs can compute), and writes the tags and art into the store. Transient API failures (network errors, timeouts, 5xx) are retried with backoff so a blip or a Lidarr restart mid-import doesn't silently drop the sync.
musefs's auto-refresh surfaces each sync at the mount with no remount. Both
scripts build on the shared python-musefs
store-contract library.
Install
Install the package — with its python-musefs dependency — into the
environment Lidarr uses to run custom scripts, so both scripts are on
Lidarr's PATH:
pip install lidarr-musefs
This pulls in the shared python-musefs dependency
from PyPI automatically. To install from a checkout instead (e.g. for
development), install both editable so imports resolve to the local source:
pip install -e contrib/python-musefs
pip install -e contrib/lidarr
You also need the musefs binary reachable by the sync script (see
MUSEFS_BIN below) and a musefs store/mount of your own — see the
main README.
Required Lidarr settings
- Settings -> Media Management -> Import Using Script: enabled.
- Import Script Path:
musefs-lidarr-import. - Metadata Provider -> Write Audio Tags:
Never. - File Date:
None. - Linux permission management: disabled.
Do not rely on Lidarr's built-in "Use Hardlinks instead of Copy" for this
workflow. Lidarr uses a hardlink-or-copy transfer mode internally, so a hardlink
failure can copy bytes. musefs-lidarr-import creates the destination entry
itself and fails closed.
musefs-lidarr-sync --doctor verifies these settings over the API (see
Doctor).
Lidarr Custom Script
Configure a Custom Script notification (Settings -> Connect):
- On Release Import: enabled.
- On Rename: enabled.
- On Album Delete: enabled.
- On Artist Delete: enabled.
- Path:
musefs-lidarr-sync.
Test events exit successfully without touching files or the database.
TrackRetag events are skipped with a warning because they fire after Lidarr
writes tags.
Environment
Both scripts are configured through environment variables, set in the environment Lidarr launches scripts with.
Import script:
MUSEFS_LIDARR_LINK_MODE=symlink # default; use hardlink only if symlinks are unsuitable
Sync script:
MUSEFS_DB=/path/to/musefs.db # the musefs SQLite store (required)
MUSEFS_BIN=musefs # musefs executable; full path if not on PATH
MUSEFS_LIDARR_URL=http://localhost:8686
MUSEFS_LIDARR_API_KEY=your-api-key
MUSEFS_LIDARR_AUTOSCAN=1 # default; runs `musefs scan` before each sync
API keys are redacted from logs and errors.
Manual backfill
To sync every track file Lidarr already knows about (e.g. on first setup):
musefs-lidarr-sync --all
Manual backfill requires MUSEFS_LIDARR_URL and MUSEFS_LIDARR_API_KEY. It
runs the doctor preflight first (skip with --skip-lidarr-preflight), then
queries all Lidarr artists and syncs their known track files into the musefs
DB.
Migrating an existing Lidarr library
The forward path above (new import → import script symlink → sync) works cleanly on a fresh import. Re-homing a pre-existing Lidarr library onto the musefs symlink tree runs into several Lidarr behaviors; this is the working order (observed on Lidarr v1, lsio image). None of it is a musefs bug — these are Lidarr quirks an integrator only hits here.
- Reassign the artists to the new (musefs) root folder.
- Clear the stale trackfile records before re-importing. If the artists'
existing trackfiles still reference the old root, re-import fails with
NotParentException(/old/root/... is not a child of /new/root) — Lidarr'sRemoveExistingTrackFileschokes computing the relative path. Delete the stale trackfile records first.- The empty-root deletion guard: Lidarr blocks trackfile deletion while the new root folder is empty ("Artist's root folder is empty", a mass-deletion safety guard) — a chicken-and-egg with the symlinks not existing yet. Drop a placeholder file in the root until the first symlinks land, then remove it.
- Batch the bulk delete:
DELETE /api/v1/trackfile/bulkreturns 500 on large batches (~200 ids); send ~25 ids per call.
- Re-import. The import script creates the destination symlinks.
- Backfill the store:
musefs-lidarr-sync --all.
Point musefs scan at the backing directory, not the symlink tree. The
default (--follow-symlinks off) is exactly right here: the store should key
off the real files, while Lidarr's symlink tree is just its own tracking view.
Doctor
To verify your Lidarr settings are musefs-safe:
musefs-lidarr-sync --doctor
The doctor checks Lidarr's API for:
writeAudioTags = nofileDate = nonesetPermissionsLinux = false
If MUSEFS_LIDARR_URL and MUSEFS_LIDARR_API_KEY are not configured, doctor
and sync fail because the integration cannot verify safe settings or build
complete per-track metadata.
--doctor is a runtime / post-deploy check, not an offline one: it makes a
live Lidarr API call, so it needs MUSEFS_LIDARR_URL + MUSEFS_LIDARR_API_KEY
and a reachable Lidarr instance. Run it after deployment, not at container
build time — offline it fails with connection-refused even when the
toolchain itself is wired up correctly. There is no offline "are the binary and
plugins installed/wired" check; to confirm installation at build time, test that
the musefs-lidarr-import / musefs-lidarr-sync scripts and the musefs
binary are importable/on PATH.
Smoke test
- Build and install musefs.
- Install
python-musefsandlidarr-musefsinto the environment Lidarr uses for custom scripts. - Configure Import Using Script and Custom Script as described above.
- Import a small album.
- Confirm Lidarr's destination entry is a symlink by default.
- Run
musefs mount /tmp/mnt --db "$MUSEFS_DB". - Confirm the mount shows Lidarr metadata.
- Confirm the source file's bytes and mtime did not change.
Notes
- Tags are fully replaced with Lidarr's view on every sync (scanner-written binary tags always survive — see the external-writer contract).
- Cover art: each album's Lidarr cover is fetched and written as the front cover, replacing the track's art rows on every sync (an over-cap or unreachable cover is skipped, leaving any scanner-ingested art in place).
- Schema version: the sync refuses to run if the DB's
user_versiondiffers from the version it targets — rebuild the store after upgrading musefs. - Deletions prune by MusicBrainz id, scoped to rows this plugin owns. On an
Album/Artist delete, the sync removes the matching store rows
(
musicbrainz_albumid/musicbrainz_artistid) so the mount stops presenting them. The backing audio is never touched — pruning only drops the store rows, not the files Lidarr keeps in the backing directory. A delete event for a release with no MusicBrainz id cannot be mapped and is logged and skipped. - Ownership marker. Every track the sync writes is stamped with a
musefs_lidarr_managed=1tag, and a delete only removes rows carrying that marker. Without it, amusicbrainz_albumidthe scanner seeded from a file's own native tags is indistinguishable from one Lidarr wrote, so an unrelated Lidarr delete could drop an unmanaged track's metadata. The marker is a normal text tag, so it does appear in served files (e.g. as aMUSEFS_LIDARR_MANAGEDVorbis comment / aTXXXframe / an iTunes freeform atom). A track imported under an older plugin version (before the marker existed) is treated as unmanaged and is left in place on delete — re-sync it to stamp the marker. - CI coverage: a fast smoke (real Lidarr exec path + mocked API) gates PRs, and a full real-instance download-client import e2e gates the Python releases — see the Python plugins guide.
Tests
cd contrib/lidarr
python -m venv .venv && source .venv/bin/activate
pip install -e ../python-musefs # shared library (editable, from the working tree)
pip install -e ".[test]"
python -m pytest # unit + integration (no Rust binary)
python -m pytest -m musefs_bin # path-matching gate vs the real `musefs` binary
The musefs_bin gate shells out to the real musefs binary, so build it first
from the repo root (cargo build). It is deselected from the default run and
skips cleanly if the binary is absent.