beets — read-only music catalogue#

beets indexes the music tree on voile into a queryable SQLite catalogue. It is used here purely as a query engine: nothing else in the fleet needs a database of 16.6k tracks, but the WeatherStar 4000 channel does — its music bed is built from a beets query.

  • Module: modules/nixos/media/beets.nix
  • Host: ereshkigal
  • Library: /mnt/voile/Inaba/Yunohost/yunohost.multimedia/share/Music (1,978 artist directories, ~13.9k .mp3, ~2.7k .m4a)
  • State: /var/lib/beets/{musiclibrary.db,import.log,state.pickle}

beets does not own this tree#

The library is written by Lidarr and by Yunohost’s multimedia app. beets is a reader there, and the generated config makes that structural rather than a matter of discipline:

Setting Value Effect
import.copy false never duplicates a file into directory
import.move false never relocates a file inside the tree
import.write false never rewrites ID3/Vorbis tags on disk
plugins [] none of the write-happy plugins (fetchart, replaygain, convert) are loaded

With all three off, beet import is a pure catalogue operation: walk the tree, read the tags that are already there, insert rows. Relaxing any of them hands beets co-ownership of a directory another daemon is actively reorganising, and the two will fight over it. That decision is deliberately still open — see issue #273.

import.incremental = true is also set, so a second pass skips directories already recorded and costs a fraction of the first.

Enabling#

# hosts/x86_64-nixos/ereshkigal/default.nix
tsunaminoai.media.beets = {
  enable = true;
  libraryPath = "/mnt/voile/Inaba/Yunohost/yunohost.multimedia/share/Music";
};
Option Default Meaning
enable false
package pkgs.beets
libraryPath (required) root of the tree to index
dbPath /var/lib/beets catalogue + import log + beets runtime state
user / group the media user owns the state, runs the index

dbPath is deliberately not under anyone’s home directory: this is a system service, and the media user’s home is the share (modules/users/media.nix). The predecessor of this module was a stray ~/.config/beets/musiclibrary.db with one row in it.

Running the index#

There is no timer. Indexing ~16.6k files over CIFS is an hours-scale job, so the first pass is manual and measured before anything periodic gets considered:

systemctl start beets-index.service     # returns immediately? no — oneshot, it blocks
journalctl -fu beets-index.service      # watch it
systemd-analyze timespan $(systemctl show -P ExecMainStartTimestampMonotonic beets-index.service)

To start it and detach:

systemctl --no-block start beets-index.service

The unit runs beet import -A -q <libraryPath> as the media user:

  • -Ano autotag. beets does not contact MusicBrainz and does not try to “correct” anything; it records the tags found on disk.
  • -q — never prompt, skip anything ambiguous. Skips land in /var/lib/beets/import.log.

It carries TimeoutStartSec=infinity (no sane upper bound exists for a first pass over CIFS), Nice=19 + IOSchedulingClass=idle (Jellyfin and Navidrome are reading the same share live), and a RequiresMountsFor on libraryPath — the automount point answers test -d while unmounted, so path ordering is the only thing that stops it recording an empty library.

Querying#

beet is on the system PATH, wrapped so it always finds the generated config and keeps its state in dbPath:

beet stats
beet ls -p 'genre:jazz' | head
beet ls -p 'genre:jazz' | wc -l
beet ls albumartist:'Miles Davis'

Run it as root or as the media user

SQLite opens the catalogue read-write even for a plain ls (beets issues CREATE TABLE IF NOT EXISTS at open). /var/lib/beets is 0775 media-user-owned; anyone else needs sudo.

Query syntax is the same string accepted by tsunaminoai.media.weatherChannel.ws4channels.music.query.

  • Media — the wider media stack.
  • Family Media — how the family reaches the music library.
  • TLMC v5.5 — the runbook for the 990 GB Touhou collection this catalogue is being grown to hold.