Sync beets metadata into the musefs SQLite store
Project description
beets-musefs
A beets plugin that syncs your beets metadata (tags + cover art) into a musefs SQLite store, so a live musefs mount shows a re-tagged view of your library without rewriting any audio.
How it fits together
- The plugin owns the tags (and cover art, when beets has it) of each track, keyed by the file's canonical real path.
- The structural columns (audio offsets, size, mtime) can only come from musefs
probing the file, so the plugin runs
musefs scanfor you (via thebinconfig) before syncing — it never tries to compute those itself. beet musefsscans the library and then syncs; the import/write hooks scan just the touched file and then sync. musefs's auto-refresh shows changes live — no remount, and no separate scan step.
Install
Whichever path you choose below, the plugin needs the shared python-musefs
library as a runtime dependency. It's unpublished and lives in this repo,
so install it from the working tree first:
pip install -e contrib/python-musefs
Use via pluginpath (no package install)
The plugin itself doesn't need to be installed — point beets at the plugin's
beetsplug directory and it loads at runtime. beets adds pluginpath entries
directly to the beetsplug package path, so it must be the beetsplug dir
itself (not its parent). In your beets config.yaml:
pluginpath: /path/to/musefs/contrib/beets/beetsplug
plugins: musefs
musefs:
db: ~/musefs.db # path to the musefs SQLite store (required)
bin: musefs # musefs executable for auto-scan; use a full path if
# not on $PATH, e.g. /path/to/musefs/target/release/musefs
# autoscan: yes # default; runs `musefs scan` for you. Set `no` to
# # manage scanning yourself (hooks then best-effort).
# fields: # optional: map extra beets fields to musefs keys
# comments: comment
Development / test install
To run the test suite — or if you'd rather install the plugin as a package
than wire up pluginpath — install it editable instead:
pip install -e "contrib/beets[test]"
Workflow (test drive)
# Sync beets metadata into the store. Auto-scans the library first (creating the
# DB if needed) — no separate `musefs scan` step.
beet musefs # everything
beet musefs albumartist:"Boards of Canada" # a subset (scans just those files)
beet musefs -n # dry run: report counts, write nothing
# Mount the re-tagged view.
musefs mount ~/mnt --db ~/musefs.db \
--template '$albumartist/$album/$tracknumber - $title'
# ...or mirror your beets library layout exactly, via the computed beets_path tag.
musefs mount ~/mnt --db ~/musefs.db --template '$!{beets_path}'
Imports and tag write-backs auto-sync via event hooks: beet import and
beet modify -w … record the touched items and reconcile them once the command
finishes — when each file's path is final (beets has no move event, and a write
fires before its move). The reconcile scans the new path and prunes the row
left behind at the old one. A metadata-only beet modify (no -w) doesn't fire
a hook — re-run beet musefs. With autoscan: no, run musefs scan yourself
first; the hooks then skip gracefully if the DB is missing.
Notes
- Field coverage: every tag beets writes to a file (its
_media_tag_fields) is synced — ReplayGain, MusicBrainz IDs, comment, lyrics, grouping, isrc, multi-valued artists, and any custom field — under canonical musefs keys. Read-only file facts (bitrate, length, …) are never written as tags. - Merge, not replace: beets' values win for the fields it manages; any other tag already embedded in the file is preserved in the view.
- Deletions stick: the plugin records the keys it manages per track in a
musefs_managedbeets flexattr (stored in the beets DB only — never in your audio files or the musefs store). Remove a tag in beets and it is removed from the view and stays gone across re-scans. --restore-backing(orrestore_backing: yes): when you remove a tag in beets, let the file's original embedded value reappear instead of disappearing.- Caveat: sticky deletion relies on
autoscan: yes(the default), which re-derives the file's embedded tags before each sync. Withautoscan: no, a deletion only takes effect after your next manualmusefs scan. - Cover art: taken from the album's
artpath(beets' external cover file). beets art wins when present; otherwise any artmusefs scaningested from embedded pictures is preserved. - Computed path (
beets_path): each sync also writes abeets_pathtext tag holding the track's beets library-relative path (from yourpaths:config, viaitem.destination), with the file extension removed — musefs re-appends it. Mount with--template '$!{beets_path}'(the$!{}path field keeps/as directory separators) to mirror your beets layout, including layouts musefs's own template engine can't express. Setwrite_path: noin themusefs:config to skip it. Do not add an extension in a template that consumesbeets_path. See the computed-tag workflow in ARCHITECTURE.md. - Moves & deletes: every sync (the command and the end-of-command reconcile) prunes track rows whose backing file is no longer present, so renames/moves don't leave stale entries. Caveat: a file that's merely offline at sync time (e.g. an unmounted network share) is also pruned — sync while the library is available.
- Orphaned art: replacing art can orphan old blobs;
musefs scan --revalidategarbage-collects them. - Schema version: the plugin refuses to run if the DB's
user_versiondiffers from the version it targets — rebuild after upgrading musefs.
Tests
The tests live under tests/ and use a local virtualenv with beets + pytest.
cd contrib/beets
uv venv # create .venv (once)
source .venv/bin/activate
uv pip install -e ../python-musefs # shared library (unpublished; install first)
uv pip install -r requirements.txt # beets + pytest
python -m pytest # unit + integration (no Rust binary)
python -m pytest -m musefs_bin # path-matching gate vs the real `musefs` binary
python -m pytest -m e2e # full beets -> mount -> playback end-to-end
The musefs_bin gate shells out to the real musefs binary, so build it first
from the repo root (cargo build) and run it against a fresh build. The e2e
tier additionally needs ffmpeg and /dev/fuse + fusermount: it generates
audio, imports it with beets, retags, syncs, mounts via FUSE, and verifies the
mount's tags and byte-identical audio (including a move-reconcile case). Both
tiers are deselected from the default run and skip cleanly if their tools are
absent.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file beets_musefs-1.0.0.tar.gz.
File metadata
- Download URL: beets_musefs-1.0.0.tar.gz
- Upload date:
- Size: 28.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c127f46c7c46ec9b06f75e057f9b1c1a21aff645eb94483db4d6d72d2b78683
|
|
| MD5 |
ac3105b206dd86ff5cf0efeb9dfbb540
|
|
| BLAKE2b-256 |
b6813a5e368a7b013fe1b3c7ebcd317b9881bb28b9bfa7ccb322cd3992bb7a7a
|
Provenance
The following attestation bundles were made for beets_musefs-1.0.0.tar.gz:
Publisher:
release-python.yml on Sohex/musefs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
beets_musefs-1.0.0.tar.gz -
Subject digest:
3c127f46c7c46ec9b06f75e057f9b1c1a21aff645eb94483db4d6d72d2b78683 - Sigstore transparency entry: 1805240396
- Sigstore integration time:
-
Permalink:
Sohex/musefs@de6f17129eadc8883ec9ac0ef0eb976466723068 -
Branch / Tag:
refs/tags/py-v1.0.0 - Owner: https://github.com/Sohex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@de6f17129eadc8883ec9ac0ef0eb976466723068 -
Trigger Event:
push
-
Statement type:
File details
Details for the file beets_musefs-1.0.0-py3-none-any.whl.
File metadata
- Download URL: beets_musefs-1.0.0-py3-none-any.whl
- Upload date:
- Size: 13.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bcab1796e0cf03f34623dc0aaa6a566c312d7f3f5adc6d1cd8fcbd9bedebbbe8
|
|
| MD5 |
5f369bdf1e778dfdb2730ea282ce1e4e
|
|
| BLAKE2b-256 |
8535b7308a3a3770361b283133ff63d2950ac121a6c9e5b22d855db999d587c5
|
Provenance
The following attestation bundles were made for beets_musefs-1.0.0-py3-none-any.whl:
Publisher:
release-python.yml on Sohex/musefs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
beets_musefs-1.0.0-py3-none-any.whl -
Subject digest:
bcab1796e0cf03f34623dc0aaa6a566c312d7f3f5adc6d1cd8fcbd9bedebbbe8 - Sigstore transparency entry: 1805240427
- Sigstore integration time:
-
Permalink:
Sohex/musefs@de6f17129eadc8883ec9ac0ef0eb976466723068 -
Branch / Tag:
refs/tags/py-v1.0.0 - Owner: https://github.com/Sohex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@de6f17129eadc8883ec9ac0ef0eb976466723068 -
Trigger Event:
push
-
Statement type: