Configuration¶
Every setting can come from a TOML config file or be overridden by an environment variable (and a few by a CLI flag). This page documents the precedence, the complete environment-variable surface, the TOML config keys, and the default storage paths.
A fully commented config.example.toml ships in the repository root; copy it and edit the keys you need. The sections below cross-reference it.
Token precedence¶
A Musixmatch API token is required only when Musixmatch is the active provider (the default); the one-shot fetch CLI needs one supplied explicitly in that case. Petit Lyrics and the InnerTube lane are both tokenless. In serve mode with Musixmatch active, a token is optional (serve provisions and stores one itself). When you do supply a Musixmatch token, these are the sources in order of precedence (highest first):
--tokenCLI flag - highest priority.MUSIXMATCH_TOKENenvironment variable (MXLRC_API_TOKENis accepted as a lower-precedence alias).- Config file /
.envfile - place a.envin the working directory where you run the command, or set the token in the TOML config.
In serve mode a token is optional: when none is configured, Canticle obtains one automatically on first run and persists it in the encrypted secret store, reusing it on later starts. A token you configure here always takes precedence and is never overwritten by that automatic path. The one-shot fetch CLI is stateless and has no secret store, so it cannot persist a token and still requires one.
General precedence¶
For all settings, precedence is CLI flag > environment variable > config file > built-in default.
Storage paths¶
canticle resolves its config file and SQLite database using XDG base directories by default, with overrides for Docker and native packages.
| Install method | Config file | Database |
|---|---|---|
| XDG (default) | $XDG_CONFIG_HOME/mxlrcgo-svc/config.toml |
$XDG_DATA_HOME/mxlrcgo-svc/mxlrcgo.db |
Docker (MXLRC_DOCKER=true) |
/config/config.toml |
/config/mxlrcgo.db |
Native package (.deb/.rpm/.apk) |
/etc/mxlrcgo-svc/config.toml |
/var/lib/mxlrcgo-svc/mxlrcgo.db |
Native packages run the service as the mxlrcgo-svc system user; that user owns /var/lib/mxlrcgo-svc (mode 0750). The state directory and system user are preserved on package removal so the database survives an upgrade or reinstall.
Override any path explicitly with MXLRC_DB_PATH, MXLRC_OUTPUT_DIR, or the --config flag.
Instrumental tracks and --upgrade¶
Instrumental tracks write a .txt marker file. A marker is not a final state: --upgrade re-checks every instrumental marker, whether a provider or the audio detector wrote it, and replaces it when a provider now returns lyrics. A re-check never writes a worse result over what is on disk, so it cannot turn lyrics into a marker; only --update (full re-fetch) may do that. Each re-check costs a lookup per marker per run (a provider request or more on a cache miss) - see the CLI Reference for narrowing a run.
Environment variables¶
The table below is the complete env-var surface; the watcher and verification sections of the User Guide give the operational detail.
| Variable | Default | Purpose |
|---|---|---|
MUSIXMATCH_TOKEN |
(auto-provisioned in serve mode; required for fetch) |
Musixmatch API token. MXLRC_API_TOKEN is accepted as a lower-precedence alias. |
MXLRC_WEBHOOK_API_KEY |
(none) | Comma-separated webhook API key(s) accepted by the server. Generate with canticle keys create --scope webhook. |
MXLRC_SERVER_ADDR |
127.0.0.1:3876 |
HTTP listen address for serve. Docker images default this to 0.0.0.0:50705. |
MXLRC_WEB_UI_ENABLED |
false |
Enable the browser UI on the serve listener. Env override of web_ui_enabled (precedence: env > file). Restart to apply. |
MXLRC_PREVIEW_FLAC_FALLBACK |
false |
Retry a track the preview player cannot decode (ALAC, WMA) once as an ffmpeg FLAC conversion in a temp cache. Env override of preview_flac_fallback. Restart to apply. |
MXLRC_TRUSTED_CIDRS |
(none) | Comma-separated CIDRs of trusted client networks (serve mode). Loopback is always trusted. Requests from listed CIDRs may scrape GET /metrics and bypass the web UI session requirement. |
MXLRC_TRUSTED_PROXIES |
(none) | Comma-separated CIDRs of reverse proxies whose X-Forwarded-For is trusted to carry the real client IP (serve mode). Must not overlap MXLRC_TRUSTED_CIDRS. |
MXLRC_TLS_CERT_FILE |
(none) | PEM certificate path for the serve listener. Set with MXLRC_TLS_KEY_FILE to terminate TLS (minimum TLS 1.2). |
MXLRC_TLS_KEY_FILE |
(none) | PEM private key path. Required together with MXLRC_TLS_CERT_FILE. |
MXLRC_TLS_SELF_SIGNED |
false |
Generate and persist a self-signed certificate on first run under <dir(db_path)>/tls/. Mutually exclusive with the cert/key pair. |
MXLRC_TLS_REDIRECT_HTTP |
(none) | Plain-HTTP listen address (e.g. :80) that 301-redirects to HTTPS. Honored only when TLS is enabled. |
MXLRC_TLS_SELF_SIGNED_HOSTS |
(none) | Comma-separated extra SAN hostnames/IPs for the self-signed certificate (on top of localhost, canticle, 127.0.0.1, ::1). Honored only when self-signed. |
MXLRC_OUTPUT_DIR |
XDG / /music |
Output directory for fetch mode. Ignored in serve mode (lyrics are written next to the audio file; the metadata-only webhook fallback uses the internal default). |
MXLRC_EMBEDDED_LYRICS |
off |
Embedded lyrics handling. off - ignore (default); respect - skip fetching when embedded lyrics exist; extract - write embedded lyrics to a sidecar, then skip fetching. A timestamped Vorbis SYNCEDLYRICS comment is written as a synced .lrc (preferred); unsynced lyrics become .txt. ID3 SYLT / MP4 atoms are not handled. respect and extract bypass the tag cache, so every file without a lyric file is opened on each scan. |
MXLRC_BILINGUAL_OUTPUT |
false |
When true and a provider returns a translation track, interleave original and translated lines under shared timestamps in a single .lrc. |
MXLRC_OUTPUT_BUMP_AUDIO_MTIME |
false |
Writes to your audio files (modified time only). When true, Canticle bumps the audio file's mtime after it replaces an existing sidecar with different lyrics. See bump_audio_mtime. |
MXLRC_DB_PATH |
XDG / /config/mxlrcgo.db |
SQLite database path. |
MXLRC_DOCKER |
false |
When true, storage defaults resolve under /config. Set automatically in the images. |
MXLRC_MASTER_KEY |
(none) | Optional. Base64 of 32 random bytes; overrides the auto-generated key file as the master key for encrypted-at-rest secrets. When set, no key file is read or written. Use for key/data separation (recommended Docker hardening when the threat model includes whole-volume theft). Generate with openssl rand -base64 32. See the Encrypted secrets guide. |
MXLRC_SECRETS_KEY_FILE |
XDG / /config/.mxlrcgo.key |
Override for the auto-generated 0600 key-file location (the native-install default; used only when MXLRC_MASTER_KEY is unset). Point it at a separate mount to keep the key off the data volume. |
MXLRC_WEBAUTH_ADMIN_USER |
(none) | Bootstrap-only. With MXLRC_WEBAUTH_ADMIN_PASSWORD, creates the first web-UI admin on startup when none exists (skipped otherwise). Both must be set; an existing admin is never overwritten. Rotate the password after first run and remove these vars. See Web UI access. |
MXLRC_WEBAUTH_ADMIN_PASSWORD |
(none) | Bootstrap-only. The first web-UI admin password (at least 8 characters; a shorter value is a fatal startup error). Never logged. Pairs with MXLRC_WEBAUTH_ADMIN_USER. |
MXLRC_API_COOLDOWN |
15 |
Seconds between Musixmatch requests. MXLRC_COOLDOWN is a lower-precedence alias. |
MXLRC_API_CIRCUIT_OPEN_DURATION |
1800 |
Cap (seconds) for the worker circuit-breaker window; the window ramps geometrically up to this ceiling, and a token-renewal signal opens for the full cap (floor 300). |
MXLRC_API_CIRCUIT_BACKOFF_BASE |
60 |
Trip-1 circuit-breaker window (seconds); doubles each consecutive throttle up to MXLRC_API_CIRCUIT_OPEN_DURATION, resets on a successful fetch or clean miss (floor 15, capped at the open-duration). |
MXLRC_MISS_BACKOFF_BASE_HOURS |
168 |
Initial re-check delay in hours for a benign miss (7 days). Doubles on each successive miss up to MXLRC_MISS_BACKOFF_CAP_HOURS. Minimum 1. |
MXLRC_MISS_BACKOFF_CAP_HOURS |
672 |
Maximum re-check delay in hours for a benign miss (28 days). Clamped to at least MXLRC_MISS_BACKOFF_BASE_HOURS. |
MXLRC_MAX_MISS_ATTEMPTS |
15 |
Maximum number of miss re-fetches before retiring the queue row. 0 means no cap. |
MXLRC_SCAN_INTERVAL |
900 |
DEPRECATED (see MXLRC_SCAN_SCHEDULE). serve library-scan interval in seconds, measured from process start. 0 scans once without repeating. |
MXLRC_SCAN_SCHEDULE |
(unset) | Wall-clock scan schedule: hourly, daily, weekly, or off. Unset keeps the deprecated MXLRC_SCAN_INTERVAL behavior. |
MXLRC_SCAN_SCHEDULE_AT |
(unset) | Start time for the daily/weekly scan, HH:MM on a 24-hour local clock. |
MXLRC_SCAN_SCHEDULE_DAY |
(unset) | Weekday for the weekly scan (sunday..saturday). |
MXLRC_SCAN_SCHEDULE_ON_START |
false |
Also scan the whole library at startup. |
MXLRC_WORK_INTERVAL |
0 |
Worker poll interval in seconds. 0 falls back to api.cooldown (15s floor). |
MXLRC_PROVIDER_PRIMARY |
musixmatch |
Primary lyrics provider. |
MXLRC_PROVIDERS_DISABLED |
(none) | Comma-separated providers to disable. |
MXLRC_PROVIDERS_MODE |
ordered |
Multi-provider dispatch strategy: ordered (first suitable result in priority order) or parallel (race every lane). |
MXLRC_PROVIDERS_RACE_WAIT_SECONDS |
2 |
Parallel-mode synced-upgrade window in seconds. Ignored in ordered mode. |
MXLRC_PROVIDERS_FALLBACK_ORDER |
(none) | Comma-separated providers consulted, in order, after the primary returns no suitable result. Empty means primary-only. |
MXLRC_PROVIDERS_PETITLYRICS_COOLDOWN_SECONDS |
0 |
Minimum seconds between Petit Lyrics requests. 0 falls back to api.cooldown. |
MXLRC_PROVIDERS_INNERTUBE_COOLDOWN_SECONDS |
0 |
Minimum seconds between InnerTube requests. 0 falls back to api.cooldown. |
MXLRC_GUARD_ACCEPTED_SCRIPTS |
(none) | Comma-separated allowlist of Unicode script buckets a lyric body may use (Latin, Han, Kana, Hangul, Other). Empty disables the language/script guard. |
MXLRC_GUARD_THRESHOLD |
0.20 |
Maximum tolerated share of foreign-script letters before a result is rejected. Values outside (0, 1] reset to the default. |
MXLRC_QUEUE_RANDOMIZE |
true |
Shuffle worker dequeue order within each priority tier (anti-fingerprint). false restores deterministic order. |
MXLRC_QUEUE_BATCH_SIZE |
10 |
Shuffled lookahead buffer size: draw N eligible rows at random per batch and serve them in order, so the near-term queue is knowable. 0 disables batching (per-item random). Applies only when randomize = true. |
MXLRC_LOG_LEVEL |
info |
Minimum log level: debug, info, warn, error. debug exposes per-request detail, worker idle-polls, and watcher events. |
MXLRC_LOG_FORMAT |
text |
Log output format: text (human-readable) or json (structured, for log aggregators). |
MXLRC_LOG_FILE |
(none) | Log file path. Empty means console-only (stderr). When set, logs go to both stderr and the file with automatic rotation. |
MXLRC_LOG_MAX_SIZE_MB |
10 |
Maximum log file size in megabytes before rotation. |
MXLRC_LOG_MAX_FILES |
5 |
Number of rotated log files to retain. |
MXLRC_LOG_MAX_AGE_DAYS |
30 |
Maximum age in days of retained rotated log files. |
MXLRC_LOG_COMPRESS |
true |
Compress rotated log files with gzip. |
MXLRCGO_WATCH_ENABLED |
false |
Enable the optional low-latency filesystem watcher (see the User Guide). |
MXLRCGO_WATCH_DEBOUNCE_MS |
2000 |
Watcher debounce window in milliseconds. |
MXLRCGO_WATCH_MAX_DIRS |
100000 |
Watcher safety cap on directories watched. |
MXLRC_VERIFICATION_ENABLED |
false |
Enable Whisper-based lyric verification (requires a sidecar and ffmpeg). |
MXLRC_VERIFICATION_WHISPER_URL |
(none) | Whisper-compatible transcription endpoint. MXLRC_WHISPER_URL is an alias. |
MXLRC_VERIFICATION_FFMPEG_PATH |
ffmpeg |
Path to the ffmpeg binary used to extract audio samples. |
MXLRC_VERIFICATION_SAMPLE_DURATION_SECONDS |
30 |
Audio sample length sent to Whisper. MXLRC_VERIFICATION_SAMPLE_DURATION is an alias. |
MXLRC_VERIFICATION_MIN_CONFIDENCE |
0.85 |
Below this Musixmatch confidence, verify against Whisper (0-1). |
MXLRC_VERIFICATION_MIN_SIMILARITY |
0.35 |
Minimum transcript/lyric overlap to accept (0-1). |
MXLRC_INSTRUMENTAL_DETECTOR_ENABLED |
false |
Enable the audio-based instrumental detection sidecar. Requires MXLRC_INSTRUMENTAL_DETECTOR_CLASSIFIER_URL. |
MXLRC_INSTRUMENTAL_DETECTOR_CLASSIFIER_URL |
(none) | Base URL of the AudioSet classifier sidecar, e.g. http://yamnet:8080. Required when the detector is enabled. |
MXLRC_INSTRUMENTAL_DETECTOR_FFMPEG_PATH |
ffmpeg |
Path to the ffmpeg binary used for audio sampling by the instrumental detector. |
MXLRC_INSTRUMENTAL_DETECTOR_SAMPLE_DURATION_SECONDS |
30 |
Length of the audio sample sent to the classifier, clamped to [30, 60]. |
MXLRC_INSTRUMENTAL_DETECTOR_MIN_CONFIDENCE |
0.90 |
Minimum summed AudioSet class probability to mark a track instrumental. Values outside (0, 1] reset to 0.90. |
MXLRC_INSTRUMENTAL_DETECTOR_CLASSES |
Music,Musical instrument |
Comma-separated AudioSet class names whose probabilities are summed and compared against the confidence threshold. |
MXLRC_INSTRUMENTAL_DETECTOR_COOLDOWN_SECONDS |
5 |
Minimum gap in seconds between consecutive classifier inference calls. 0 disables the cooldown. |
MXLRC_INSTRUMENTAL_DETECTOR_VOCAL_CLASSES |
Singing,Vocal music,Choir,... |
Comma-separated AudioSet vocal-class names whose peak (max-over-frames) score blocks an instrumental marking (the vocal gate). |
MXLRC_INSTRUMENTAL_DETECTOR_VOCAL_MAX_CONFIDENCE |
0.015 |
Maximum vocal-class peak (0-1) tolerated before a track is excluded from instrumental. Values outside (0, 1] reset to 0.015. |
MXLRC_INSTRUMENTAL_DETECTOR_SPREAD_SAMPLES |
6 |
Number of short windows spread across the track and concatenated into one classifier sample. < 2 disables spreading (single contiguous window). |
MXLRC_INSTRUMENTAL_DETECTOR_FFPROBE_PATH |
(auto-discover) | Path to ffprobe used to read track duration for spread-sample placement. Empty auto-discovers (sibling of ffmpeg, then PATH). Set this when ffmpeg was auto-provisioned (no ffprobe). |
MXLRC_ENRICHMENT_ENABLED |
true |
Global default for recording enrichment (reading ISRC, MusicBrainz ID, and duration from audio tags). Per-library and per-run flags override this. Also decides whether a scan writes the tag cache. |
MXLRC_REALIGN_ENABLED |
false |
Master switch for serve-mode reactive realign (watcher/webhook/post-scan). The CLI command runs regardless. |
MXLRC_REALIGN_ON_SCAN |
false |
Also run a library-wide realign after each scan (watcher/webhook triggers are governed by enabled alone). |
MXLRC_REALIGN_REQUIRE_PROVENANCE |
false |
Only apply exact ISRC/MBID matches; heuristic candidates are reported but skipped. |
MXLRC_REALIGN_CROSS_DIRECTORY |
false |
Allow an exact match to move a sidecar across directories within a library. |
MXLRC_REALIGN_IDENTITY_KEYS |
mbid,isrc |
Ordered provenance identifiers the exact tier matches on (valid: mbid, isrc). |
MXLRC_REALIGN_MIN_CONFIDENCE |
0.75 |
Jaro-Winkler name-guard floor (0-1) for a heuristic rename. |
MXLRC_REALIGN_AUTO_APPLY_HEURISTIC |
false |
Whether serve-mode reactive realign may auto-apply heuristic (name-similarity) matches; off = exact-only. |
MXLRC_REALIGN_NAME_MATCH |
false |
Enable the N:M name-similarity matcher for directories with multiple orphans and multiple sidecar-less audio files. |
MXLRC_REALIGN_MIN_MARGIN |
0.05 |
Minimum score separation (0-1) an N:M candidate's best score must hold over its runner-up to be accepted; a near-tie is reported ambiguous. |
MXLRC_TIMING_VALIDATION_ENABLED |
false |
Master switch for the serve-mode timing sweep; the revalidate CLI runs regardless. |
MXLRC_TIMING_VALIDATION_REVALIDATE_EXISTING |
false |
Drain the backlog of .lrc files written before the accept-time timing guard existed. |
MXLRC_TIMING_VALIDATION_REVALIDATE_BATCH |
100 |
Sidecars judged per sweep cycle; values below 1 reset to the default. |
MXLRC_TIMING_VALIDATION_ON_MIS_SYNCED |
demote |
Action for a lyric whose cues overrun the audio: demote, quarantine, purge, off. |
MXLRC_TIMING_VALIDATION_ON_CATEGORICAL |
quarantine |
Action for a lyric belonging to a different song: quarantine, purge, off. |
MXLRC_INSTRUMENTAL_TITLE_MODE |
strong |
The instrumental title rule: off, strong, all. See [instrumental_title]. |
MXLRC_WORD_SYNC_RECHECK_ENABLED |
false |
Master switch for the serve-mode word-timing re-check sweep. |
MXLRC_WORD_SYNC_RECHECK_BATCH |
100 |
Most rows in word-recheck mode at once (1-1000); out-of-range values are ignored. |
MXLRC_UPGRADE_SWEEP_ENABLED |
false |
Master switch for the serve-mode upgrade sweep. |
MXLRC_UPGRADE_SWEEP_BATCH |
100 |
Most rows waiting for an upgrade re-fetch at once (1-1000); out-of-range values are ignored. |
MXLRC_WORD_SYNC_GENERATE_ENABLED |
false |
EXPERIMENTAL: master switch for the Auto alignment action (forced alignment via an external sidecar). The reference sidecar is slow on CPU (a published CUDA image is available, see deploy/aligner/README.md); leave this off unless you run a dedicated aligner. With the web UI on, it enables the on-demand Auto alignment action. |
MXLRC_WORD_SYNC_GENERATE_URL |
(none) | Base URL of the aligner sidecar. |
MXLRC_WORD_SYNC_GENERATE_CONCURRENCY |
1 |
Caps how many Auto alignment runs go at once across tracks; a start over the cap is refused as busy. Values below 1 reset to the default. |
MXLRC_WORD_SYNC_GENERATE_MODEL |
(none) | Reserved, not yet sent to the sidecar; the sidecar selects its own models via its own ALIGNER_* env vars. |
PUID / PGID |
99 / 100 |
Container-only: user/group the process drops to for file ownership. |
TOML config keys¶
The TOML config file mirrors the environment variables in named sections. The keys below correspond one-to-one with config.example.toml, which carries inline documentation for each.
[api]¶
[api]
cooldown = 15
circuit_open_duration = 1800
# circuit_backoff_base_seconds = 60
# miss_backoff_base_hours = 168 # initial re-check delay (hours; 7 days); minimum 1
# miss_backoff_cap_hours = 672 # maximum re-check delay (hours; 28 days)
# max_miss_attempts = 15 # 0 = no cap
Request cooldown, the worker circuit-breaker window, and miss re-check backoff (env: MXLRC_API_COOLDOWN, MXLRC_API_CIRCUIT_OPEN_DURATION, MXLRC_API_CIRCUIT_BACKOFF_BASE, MXLRC_MISS_BACKOFF_BASE_HOURS, MXLRC_MISS_BACKOFF_CAP_HOURS, MXLRC_MAX_MISS_ATTEMPTS).
miss_backoff_base_hours (default 168, i.e. 7 days) and miss_backoff_cap_hours (default 672, i.e. 28 days) govern the escalating re-check cadence for benign misses: the delay doubles each miss (base, 2 x base, 4 x base, ...) up to the cap. max_miss_attempts retires the queue row after N miss fetches; 0 means no cap.
[output]¶
[output]
dir = "lyrics"
# embedded_lyrics = "off"
# bilingual_output = false
# bump_audio_mtime = false # WRITES TO AUDIO FILES, see below
# word_sync = false # deprecated, see word_sync_mode
# word_sync_mode = "both"
Fallback output directory and per-file output controls (env: MXLRC_OUTPUT_DIR, MXLRC_EMBEDDED_LYRICS, MXLRC_BILINGUAL_OUTPUT, MXLRC_OUTPUT_BUMP_AUDIO_MTIME, MXLRC_WORD_SYNC, MXLRC_WORD_SYNC_MODE; CLI: --embedded-lyrics).
bump_audio_mtime (default false): enabling this makes Canticle write to your audio files, not only its own sidecars. Only the file's modified time changes (via os.Chtimes; the content is never opened and the access time is left alone), and only right after Canticle replaces an existing sidecar with different lyrics: a .txt promoted to .lrc, or an .lrc replaced by better or corrected content, by a directory-mode fetch (song and text-file mode never bump), --upgrade/--update, the serve upgrade sweep, or a word-sync recheck under word_sync_mode = replace (under the default both only the .elrc companion changes, so nothing bumps). Only the sidecar that sits beside the audio file and shares its name bumps that file. A symlinked audio file has its target bumped. A first-ever sidecar write, a refused write (a better sidecar is already on disk), and a rewrite whose lyrics are unchanged never bump. It exists for Music Assistant, which detects changes by audio mtime and never re-reads a rewritten sidecar. Emby re-reads sidecars on its own library scan and does not need it. A failed bump logs a warning and never undoes or fails the sidecar write. The blast radius is every tool that keys on audio mtime, so weigh it before turning this on:
- Lidarr may re-import or re-evaluate the file.
- Emby and Jellyfin may refresh the library item.
- tdarr may re-evaluate the file and potentially re-transcode it.
- Any other watcher keyed on audio mtime will fire.
- Backup tooling keyed on mtime will re-copy the file.
Canticle itself does not react to its own bump: the watcher ignores the event, no track is re-queued, and each mtime-keyed cache (duration, metadata, scan-failure store) re-reads the file once. Not covered: the first sidecar for a track, and sidecar changes made by realign, revalidate, scan reconcile-editor-tag, the LRC backfill, the instrumental backfill or its recalibrate command.
embedded_lyrics controls how lyrics already embedded in the audio file's tags are handled. off (default) ignores them and always fetches from providers. respect skips fetching for files that already carry embedded lyrics. extract writes the embedded lyrics to a sidecar (never overwriting an existing one) and then skips fetching: a Vorbis SYNCEDLYRICS comment carrying timestamped LRC text is written as a synced .lrc and takes precedence over unsynced lyrics, which are written as .txt. ID3 SYLT and MP4 synced-lyric atoms are intentionally not handled. respect and extract bypass the tag cache, so every file without a lyric file is opened on each scan (see What a scan reads).
bilingual_output (default false): when true and a provider returns a non-empty translation track, the original and translation lines are interleaved under shared timestamps in a single .lrc. See docs/multilingual-output-policy.md.
word_sync_mode (default "both") decides WHERE per-word (Enhanced LRC, "A2") timings go when a provider serves them -- Musixmatch (on most line-synced results) and Petit Lyrics' word-synced tier. With the default, a fetch that carries word timings writes a companion .elrc beside the .lrc; set off to keep the previous one-file output. Changing the mode affects new fetches only: tracks that already have a .lrc are not re-fetched on their own. To add word timings to them, run canticle scan reconcile-word-sync, which queues them for a paced re-check (read its cost first). That command refuses to run under off. Three values (#1072):
| value | the .lrc |
companion .elrc |
|---|---|---|
both (default) |
line-synced, clean | written |
off |
line-synced, clean | none |
replace |
carries <MM:SS.cc> word markers (and a trailing end tag) |
none |
sidecar and inline are deprecated aliases for both and replace; they still decode, with a deprecation warning naming the new value. An older config that already read both (markers in the .lrc AND a companion, the pre-#1072 meaning) silently gets the new meaning above instead -- that is the safe direction, since it can only remove markers from a .lrc a player might render as literal text, never add them, so it cannot warn (an old and a new config asking for both are indistinguishable). An unrecognized value resets to both rather than resolving to nothing. This meaning change is config-only: an existing .lrc written under the pre-#1072 both keeps whatever markers it already has on disk, since nothing rewrites a settled sidecar on its own -- only a re-fetch (or canticle scan reconcile-word-sync, above) touches the file.
replace needs a reader that understands inline word tags. A player or server without Enhanced LRC support shows the tags as raw text in the lyrics (Navidrome reads them from 0.63; older versions do not). The default both avoids this, because the .lrc carries no tags at all. Before setting replace for a whole library, verify your player actually renders A2 -- see A2 Player Verification, which also lists the format's known limits.
word_sync (default false) is deprecated and superseded by word_sync_mode. It still decodes and is still honored, so an existing config keeps booting unchanged, but only when word_sync_mode is unset: an explicit word_sync = false then resolves to off and an explicit true resolves to replace (not both -- someone who wrote true asked for inline markers and keeps getting them), with a deprecation warning naming the new key. When word_sync_mode is set it wins outright and word_sync is not consulted. That holds across sources: a word_sync_mode in the file also beats MXLRC_WORD_SYNC in the environment (which is logged as ignored), so a mode saved from the settings page is never reverted by a stale env var. This mirrors server.scan_interval_seconds -> [server.scan_schedule].
Historically, word_sync (default false): when true and a provider serves word-level timings (Musixmatch or Petit Lyrics, as above) each cue keeps its normal [MM:SS.cc] stamp and gains a <MM:SS.cc> marker before each word, plus one trailing <MM:SS.cc> tag after the last word that marks when it ends (only when that end is known and differs from the word's start at hundredths precision; Enhanced LRC, "A2"). This is the karaoke-style per-word highlighting some players support.
Leave this off unless you know your player handles A2. Support is not universal and there are three outcomes, only one of which is obviously wrong: the player highlights each word (the intent), silently ignores the markers (harmless), or renders them as literal text mixed into the lyrics (visibly broken). Music Assistant does not support A2; Symfonium reportedly strips the markers (forum-sourced and unverified against its current build). Verify against one album before enabling library-wide -- see A2 Player Verification for the procedure.
Word markers are emitted per line and only when they are trustworthy. A line whose timings do not reconstruct its text, whose words all share one timestamp, or whose text would break the line format falls back to a normal line-level cue -- so a partially-timed track degrades line by line rather than losing its words. Enabling this does not rewrite existing sidecars; a track gains markers on its next fetch.
[db]¶
SQLite database path (env: MXLRC_DB_PATH).
[server]¶
[server]
addr = "127.0.0.1:3876"
# webhook_api_keys = ["mxlrc_your-webhook-key"]
# scan_interval_seconds = 900
# work_interval_seconds = 0
# web_ui_enabled = false
# preview_flac_fallback = false
HTTP listen address, webhook keys, and the scheduler scan/worker poll intervals (env: MXLRC_SERVER_ADDR, MXLRC_WEBHOOK_API_KEY, MXLRC_SCAN_INTERVAL, MXLRC_WORK_INTERVAL; CLI: --listen, --scan-interval, --work-interval).
scan_interval_seconds is deprecated in favor of [server.scan_schedule] below. It still works, and still takes effect whenever scan_schedule.frequency is blank, so an existing config keeps running unchanged.
web_ui_enabled (default false, env: MXLRC_WEB_UI_ENABLED, precedence env > file) gates the browser UI on the serve listener. When enabled, the UI pages require a session login (a single admin account, separate from the webhook API key), or a request from a trusted network (the [server.trusted_networks] CIDR allowlist). Secret values (API token, webhook keys) are always redacted in the Config view. See Web UI access for the first-run onboarding flow.
preview_flac_fallback (default false, env: MXLRC_PREVIEW_FLAC_FALLBACK) lets the lyric preview player retry once, as a whole-file FLAC conversion made with ffmpeg, when the browser cannot decode a track's audio. Conversions live in a temp cache capped at 512 MiB (oldest evicted, at most 2 conversions at once) in a private directory under the OS temp dir, created per process (canticle-preview-flac-*) and removed on shutdown; a directory left by a process that did not shut down cleanly is removed at the next start (Linux and macOS), and conversions are never written to the library. It needs the web UI and an ffmpeg (a configured path, PATH, or the auto-provisioned build). See the User Guide.
[server.scan_schedule]¶
[server.scan_schedule]
frequency = "daily" # hourly | daily | weekly | off
at = "04:00" # daily/weekly start time, 24-hour local clock
day = "sunday" # weekly only
scan_on_start = false # also walk the library at startup
The wall-clock schedule for the serve-mode library scan, and the successor to scan_interval_seconds (env: MXLRC_SCAN_SCHEDULE, MXLRC_SCAN_SCHEDULE_AT, MXLRC_SCAN_SCHEDULE_DAY, MXLRC_SCAN_SCHEDULE_ON_START).
Why it replaces an interval. scan_interval_seconds started its timer at process start, which is a duration-from-boot rather than a schedule. Any supervisor that restarts the process more often than the interval - a nightly backup that stops the container, a watchdog, a routine deploy - reset the timer before it could fire. The result inverted the setting's intent: the knob meant to scan less often instead produced one full library walk on every restart, and the periodic scan never ran at all. A wall-clock anchor cannot be inverted that way, because "daily at 04:00" means the same thing however often the process restarts.
frequency - hourly fires at the top of each hour and needs no at. daily and weekly fire at the next occurrence of the configured wall-clock time. off disables periodic scanning entirely (the filesystem watcher and any manual scan run are unaffected). Blank is not the same as off: blank means "not configured", which keeps the deprecated scan_interval_seconds path alive so that upgrading changes nothing for an existing deployment. Set off when you actually want no periodic scan.
at - the daily/weekly anchor, HH:MM on a 24-hour local clock. Required for daily and weekly; a missing or malformed anchor is a startup error, not a silent default, since a scan firing at a time nobody chose is the failure this whole surface exists to prevent.
day - the weekly anchor, a lowercase weekday name. Required for weekly only.
scan_on_start - default false. The filesystem watcher already covers change-driven rescans, so the periodic walk is a backstop and a restart should not cost a full library pass. Set it true if you want the old always-scan-at-boot behavior. The deprecated interval path keeps its historical startup walk regardless of this field.
Missed windows are skipped, not run late. If the machine was asleep or the container was down through the scheduled time, the scan does not run on the way back up - it waits for the next occurrence. Running it late would restore precisely the "every restart triggers a full walk" behavior this replaces.
Daylight saving. The anchor is a wall-clock time, so daily at 04:00 stays 04:00 local on both sides of a transition; the interval between those two runs is 23 or 25 hours rather than 24. An anchor inside the hour that spring-forward skips still runs once, at the adjacent real instant, rather than being missed for the year.
[server.trusted_networks]¶
[server.trusted_networks]
# cidrs = ["192.168.1.0/24", "10.0.0.0/8"]
# trusted_proxies = ["172.16.0.0/12"]
Trusted-network allowlist for serve mode (env: MXLRC_TRUSTED_CIDRS, MXLRC_TRUSTED_PROXIES). Controls two things: access to GET /metrics (see the User Guide) and the web UI session bypass (requests from a trusted network skip the login prompt).
cidrs - CIDRs of trusted client networks. Loopback (127.x.x.x, ::1) is always trusted and does not need to be listed. An empty list (the default) means only loopback is trusted.
trusted_proxies - CIDRs of reverse proxies allowed to set X-Forwarded-For. Only when a request's immediate peer is within one of these networks is the XFF header consulted (walked right-to-left, skipping proxies) to find the real client IP. Default empty: XFF is never trusted, so a spoofed header cannot forge a trusted source. Entries must not overlap cidrs.
An invalid CIDR in either list is a fatal startup error.
Web UI access¶
The first time the UI is enabled there is no admin yet, so every UI page redirects to /setup. The onboarding form creates the admin account and can optionally store the Musixmatch token and webhook API key in the encrypted secret store.
/setup is reachable to any client that can reach the port until the first admin exists (#461). Once one does, the page closes permanently: a trusted client is redirected to /login, and an untrusted one gets a 404 that does not reveal the endpoint. The window is therefore self-closing and cannot reopen -- admin creation is a first-writer-wins atomic insert, so exactly one claim can succeed.
This is a deliberate trade. Gating /setup on the trusted-network list previously meant that bootstrapping required adding your browser's network to that list, and a trusted CIDR bypasses the session check on every UI route, permanently -- so you had to grant an indefinite, network-wide authentication bypass just to create your first account, and it stayed on unless you remembered to remove it. Keying the setup page on admin-existence instead bounds the exposure to the pre-onboarding period. Canticle logs a warning each time it serves the form to a client the trust policy would not have admitted.
Close the window immediately on an exposed network by bootstrapping the admin at startup with MXLRC_WEBAUTH_ADMIN_USER / MXLRC_WEBAUTH_ADMIN_PASSWORD (below), which creates the account before the listener ever serves /setup. Otherwise, create your admin account promptly on first start.
The webhook API and health endpoints are unaffected before onboarding - an existing deployment with a webhook key and no admin keeps processing the queue and serving webhooks; only the browsable UI is gated.
For headless/Docker deployments, set both MXLRC_WEBAUTH_ADMIN_USER and MXLRC_WEBAUTH_ADMIN_PASSWORD to bootstrap the admin at startup instead of using the form. This is idempotent (an existing admin is never overwritten), the password (minimum 8 characters) is never logged, and a too-short password is a fatal startup error. Treat them as bootstrap-only: sign in and rotate the password after first run, then drop the variables.
[server.tls]¶
[server.tls]
# cert_file = "/config/tls/cert.pem"
# key_file = "/config/tls/key.pem"
# self_signed = false
# redirect_http = ""
# self_signed_hosts = []
Optional transport security for the serve listener. TLS is off by default (plain HTTP). Two modes are supported and are mutually exclusive: bring-your-own certificate (cert_file + key_file, both required together) or a self-signed bootstrap (self_signed = true). A contradictory combination is a fatal startup error (env: MXLRC_TLS_CERT_FILE, MXLRC_TLS_KEY_FILE, MXLRC_TLS_SELF_SIGNED, MXLRC_TLS_REDIRECT_HTTP, MXLRC_TLS_SELF_SIGNED_HOSTS).
| Key | Default | Purpose |
|---|---|---|
cert_file |
(none) | PEM-encoded certificate path. With key_file, the listener terminates TLS itself (minimum TLS 1.2). |
key_file |
(none) | PEM-encoded private key path. Required together with cert_file. |
self_signed |
false |
Generate and persist a self-signed certificate on first run (ECDSA P-256, ~365-day validity) under <dir(db_path)>/tls/, regenerating when missing or expired. Mutually exclusive with cert_file/key_file. Browsers show an untrusted-certificate prompt. |
redirect_http |
(none) | Optional plain-HTTP listen address (e.g. :80) whose every request 301-redirects to the HTTPS address. Empty means no redirect listener. Honored only when TLS is enabled. |
self_signed_hosts |
[] |
Extra hostnames and IP literals to add as Subject Alternative Names in the generated certificate, on top of the built-in SANs (localhost, canticle, 127.0.0.1, ::1). Invalid entries are a startup error. Honored only when self_signed is true. |
TLS is considered enabled when either a cert_file+key_file pair is set or self_signed is true; otherwise the listener stays plain HTTP.
[secrets]¶
Key-file location override for encrypted-at-rest secrets (env: MXLRC_SECRETS_KEY_FILE). Empty uses the default (the hidden .mxlrcgo.key beside the database, auto-generated 0600 on first use). The master key is resolved from MXLRC_MASTER_KEY first (when set, the key file is skipped entirely), then from this key file; it is never stored in the config. See the Encrypted secrets guide for the secrets import / set / list commands and key-loss recovery.
[providers]¶
[providers]
primary = "musixmatch"
disabled = []
# mode = "ordered" # "ordered" (default) or "parallel"
# race_wait_seconds = 2 # parallel-mode synced-upgrade window
# fallback_order = [] # providers consulted after the primary, in order
# petitlyrics_cooldown_seconds = 0 # 0 uses api.cooldown
# innertube_cooldown_seconds = 0 # 0 uses api.cooldown
Provider selection and multi-provider dispatch. Musixmatch is the default primary provider; Petit Lyrics and InnerTube are the two alternate lanes, selectable as primary or added to fallback_order (env: MXLRC_PROVIDER_PRIMARY, MXLRC_PROVIDERS_DISABLED, MXLRC_PROVIDERS_MODE, MXLRC_PROVIDERS_RACE_WAIT_SECONDS, MXLRC_PROVIDERS_FALLBACK_ORDER, MXLRC_PROVIDERS_PETITLYRICS_COOLDOWN_SECONDS, MXLRC_PROVIDERS_INNERTUBE_COOLDOWN_SECONDS). See Multi-provider orchestration for the full dispatch model.
| Key | Default | Purpose |
|---|---|---|
primary |
musixmatch |
The primary lyrics provider, always the first lane. |
disabled |
[] |
Provider names to disable. |
mode |
ordered |
Dispatch strategy. ordered queries lanes in priority order (primary, then each fallback_order entry) and returns the first suitable result. parallel dispatches every lane concurrently and races them. Any other value is rejected at load. |
race_wait_seconds |
2 |
Parallel-mode only: after a suitable unsynced result arrives, wait up to this many seconds for a synced result (a strict quality upgrade) to preempt it. Non-positive values are reset to the default. Ignored in ordered mode. |
fallback_order |
[] |
Provider names consulted, in order, after the primary when it returns no suitable result. Each name must be a known provider; unknown names are rejected at load. Empty means no fallback (only the primary lane runs). |
petitlyrics_cooldown_seconds |
0 |
Minimum seconds between Petit Lyrics requests. 0 falls back to api.cooldown, which is the behavior this lane had before the key existed. The two providers share neither a rate-limit budget nor a throttle profile, so this lets you pace them independently. The Petit Lyrics client enforces its own floor (10s) on any positive value, so this key can raise the interval but never lower it past that floor. |
innertube_cooldown_seconds |
0 |
Minimum seconds between InnerTube requests. 0 falls back to api.cooldown. A negative value is not a disable request, and what it does depends on how it is set: accepted and treated as a fallback to api.cooldown when written in the TOML file, but rejected on the env-override path (MXLRC_PROVIDERS_INNERTUBE_COOLDOWN_SECONDS warns and keeps the current value) and by canticle config set (which returns an error). The InnerTube client raises any positive value below its own 2s floor up to that floor; note the clamp does not apply to 0, so with this key unset and api.cooldown also 0 the lane is unpaced. Precedence is innertube_cooldown_seconds > api.cooldown, same pattern as petitlyrics_cooldown_seconds. The interval is counted per outbound request, and a lookup on this lane costs one (candidate rejected at the search), two (no lyrics tab), or three (search, next, browse, only when it serves a result); on a fallback lane, whose traffic is mostly misses, the typical lookup costs one, the same as the other providers. |
parallel mode makes more upstream calls (every lane is queried per dispatch), so it is not advised against rate-limited providers unless latency matters more than call volume.
[verification]¶
[verification]
enabled = false
whisper_url = ""
ffmpeg_path = "ffmpeg"
sample_duration_seconds = 30
min_confidence = 0.85
min_similarity = 0.35
Optional Whisper-based speech-to-text verification for low-confidence scanned audio. When enabled, the worker extracts a bounded mono 16 kHz WAV sample using sample_duration_seconds, then sends it to a Whisper-compatible /v1/audio/transcriptions sidecar for audio whose Musixmatch metadata confidence is below min_confidence. The transcript must overlap the candidate lyrics by at least min_similarity. Environment variables override the TOML keys (MXLRC_VERIFICATION_*); MXLRC_WHISPER_URL and MXLRC_VERIFICATION_SAMPLE_DURATION remain accepted as legacy aliases.
ffmpeg (used to extract the audio sample) is resolved automatically: see ffmpeg resolution below. Set ffmpeg_path only to pin a specific binary.
[instrumental_detector]¶
[instrumental_detector]
# enabled = false
# classifier_url = "http://yamnet:8080"
# ffmpeg_path = "ffmpeg"
# sample_duration_seconds = 30
# min_confidence = 0.90
# instrumental_classes = ["Music", "Musical instrument"]
# vocal_classes = ["Singing", "Vocal music", "Choir", "A capella", "Chant", "Rapping", "Child singing", "Synthetic singing", "Yodeling", "Humming"]
# vocal_max_confidence = 0.015
# speech_classes = ["Speech"]
# speech_max_confidence = 0.20
# spread_samples = 6
# ffprobe_path = ""
# cooldown_seconds = 5
# [instrumental_detector.backfill]
# enabled = true
# batch_size = 100
# interval_minutes = 60
# cooldown_seconds = 0
Optional audio-based instrumental detection sidecar (env: MXLRC_INSTRUMENTAL_DETECTOR_ENABLED, MXLRC_INSTRUMENTAL_DETECTOR_CLASSIFIER_URL, MXLRC_INSTRUMENTAL_DETECTOR_FFMPEG_PATH, MXLRC_INSTRUMENTAL_DETECTOR_SAMPLE_DURATION_SECONDS, MXLRC_INSTRUMENTAL_DETECTOR_MIN_CONFIDENCE, MXLRC_INSTRUMENTAL_DETECTOR_CLASSES, MXLRC_INSTRUMENTAL_DETECTOR_VOCAL_CLASSES, MXLRC_INSTRUMENTAL_DETECTOR_VOCAL_MAX_CONFIDENCE, MXLRC_INSTRUMENTAL_DETECTOR_SPEECH_CLASSES, MXLRC_INSTRUMENTAL_DETECTOR_SPEECH_MAX_CONFIDENCE, MXLRC_INSTRUMENTAL_DETECTOR_SPREAD_SAMPLES, MXLRC_INSTRUMENTAL_DETECTOR_FFPROBE_PATH, MXLRC_INSTRUMENTAL_DETECTOR_COOLDOWN_SECONDS).
The [instrumental_detector.backfill] sub-table configures the serve-mode sweep that classifies tracks the detector has never scored (env: MXLRC_INSTRUMENTAL_DETECTOR_BACKFILL_ENABLED, MXLRC_INSTRUMENTAL_DETECTOR_BACKFILL_BATCH_SIZE, MXLRC_INSTRUMENTAL_DETECTOR_BACKFILL_INTERVAL_MINUTES, MXLRC_INSTRUMENTAL_DETECTOR_BACKFILL_COOLDOWN_SECONDS). It is on by default -- the work is local and bounded, and without it an install that never runs scan reconcile-instrumental accumulates never-scored tracks indefinitely. batch_size bounds one cycle, interval_minutes spaces the cycles, and backfill.cooldown_seconds (separate from the detector's own, and 0 by default) is what governs how long a cycle takes. See Taming it on a low-wattage or shared host for the measured per-track costs and the recipes -- in particular, bound the sweep between cycles rather than within one, or the library disks stay awake longer to do the same work.
When enabled and a classifier_url is set, the detector samples each track's audio with ffmpeg and sends the sample to an external AudioSet classifier (a YAMNet sidecar; published as ghcr.io/sydlexius/canticle-yamnet, source vendored at deploy/yamnet-detector/). In the interactive worker path it runs only on provider misses and never overrides provider-supplied data; the [instrumental_detector.backfill] sweep above is the exception, running independently of any provider miss to classify tracks the detector has never scored. A track is marked instrumental only when all three gates pass: the music gate (summed mean probability of the instrumental_classes meets min_confidence), the sung-vocal gate (no vocal_classes member peaks at or above vocal_max_confidence), and the speech gate (#403 -- the frame mean of speech_classes stays below speech_max_confidence; Speech moved out of vocal_classes into its own mean-based gate so sustained speech blocks while brief incidental speech does not). The decision is conservative: any doubt resolves to "not instrumental", because a false instrumental suppresses a real lyrics fetch.
To catch vocals that enter after an instrumental intro (arias, jazz, classical), the detector samples spread_samples short windows spread across the whole track, concatenated into one sample, and gates on the per-class max-over-frames peak (the loudest singing moment), which the frame mean dilutes. This requires the sidecar to return {"mean": {...}, "max": {...}}; a legacy mean-only sidecar degrades safely to never-instrumental.
sample_duration_seconds is clamped to [30, 60]. min_confidence and vocal_max_confidence values outside (0, 1] reset to 0.90 and 0.015 respectively. cooldown_seconds is the minimum gap between consecutive inference calls; 0 disables the cooldown. Track-duration probing needs ffprobe; the auto-provisioned ffmpeg ships none, so set ffprobe_path (or rely on a PATH ffprobe) or the detector falls back to a single window. Deploy order: upgrade Canticle before the sidecar (new Canticle tolerates the old flat-map response; old Canticle cannot parse {mean,max}). See Instrumental Detection for the full reference (decision model, sidecar setup, and tuning), and the Instrumental detection guide for the per-library and per-run override controls.
ffmpeg resolution is shared with [verification]; see ffmpeg resolution below. Set ffmpeg_path here only to pin a binary separately from the verification path.
[word_sync_generate]¶
These are the keys of the Auto alignment action in the lyric preview editor. EXPERIMENTAL and opt-in. Forced-aligns lyric lines Canticle already believes are correct to a track's own audio, via an external sidecar (deploy/aligner: Demucs vocal separation, then a faster-whisper transcript plus a direct wav2vec2 forced alignment). This is the "generate" counterpart to provider word-sync coverage (Petit Lyrics, Musixmatch richsync): it produces per-word timings for a track whose lyric text is already known, rather than depending on a provider having word-sync coverage for it, and it never overrides a provider-supplied word timing.
The reference sidecar image is published to GHCR in two variants, CPU and CUDA (NVIDIA GPU); the image name, tags and platforms are listed under "Published images" in deploy/aligner/README.md, and docs/INSTALL.md has the GPU host requirements. Forced alignment (vocal separation, transcription, alignment) is far heavier per track than any other sidecar in this file -- one call can take minutes, longer on a cold start while models load lazily on first use. Leave enabled off unless you run a dedicated aligner sidecar (see deploy/aligner/README.md) with hardware and time budget for that. url points at that sidecar; it also accepts an optional language field (an ISO 639-1 hint that skips its own language detection), which the client does not send. concurrency caps how many Auto alignment runs go at once across tracks; a start over the cap is refused as busy (default 1). model is reserved and not yet sent: the aligner client's wire contract has no model parameter, and the sidecar picks its own models via its own ALIGNER_* environment variables.
Nothing runs in the background. With the web UI on, enabled = true and a url enable the on-demand Auto alignment action in the lyric preview editor, which asks the aligner sidecar at url. Nothing is written until you accept a result.
budget_per_cycle (and MXLRC_WORD_SYNC_GENERATE_BUDGET_PER_CYCLE) is retired: it belonged to a background sweep that was removed. A leftover still boots, whatever its value, and logs a warning that it has no effect; delete it.
| Key | Env | Default | Meaning |
|---|---|---|---|
enabled |
MXLRC_WORD_SYNC_GENERATE_ENABLED |
false |
With the web UI on and a url set, enables the Auto alignment action. Stays off by default: forced alignment is heavy, slow work even on the GPU variant, and the CPU variant is slower still. |
url |
MXLRC_WORD_SYNC_GENERATE_URL |
(none) | Base URL of the aligner sidecar the Auto alignment action calls. |
concurrency |
MXLRC_WORD_SYNC_GENERATE_CONCURRENCY |
1 |
Caps how many Auto alignment runs go at once across tracks; a start over the cap is refused as busy. The reference sidecar admits only ALIGNER_MAX_PENDING requests at once (default 2: one running, one queued) and answers 429 with Retry-After beyond that, rather than refusing outright. Values below 1 reset to the default. |
model |
MXLRC_WORD_SYNC_GENERATE_MODEL |
(none) | Reserved, not yet sent to the sidecar (no model parameter on the client's wire contract); the sidecar selects its own models via its own ALIGNER_* env vars. |
[enrichment]¶
Global default for recording enrichment (env: MXLRC_ENRICHMENT_ENABLED). When enabled, the scanner reads the ISRC, MusicBrainz recording ID, and audio duration from each file's tags and passes them to the matcher to disambiguate results.
This switch also decides whether a scan writes the tag cache, and the worker follows this global setting. With it off, a file with no cache row is opened on every scan; see What a scan reads.
Default true, preserving the always-on behavior from before per-library control existed. Per-library and per-run flags override this; see Recording enrichment for the full precedence chain.
[realign]¶
[realign]
enabled = false
on_scan = false
require_provenance = false
cross_directory = false
identity_keys = ["mbid", "isrc"]
min_confidence = 0.75
name_match = false
min_margin = 0.05
Governs the realign command, which re-attaches orphaned .lrc / .txt sidecars to renamed audio via a confidence resolver (see Realign), and serve mode's reactive realign that runs it automatically. Defaults are conservative: the feature is off, provenance is not required, matches are confined to the orphan's directory, identity is matched MBID-first then ISRC, and the N:M name matcher is off.
When enabled, serve mode runs a scoped realign on three triggers: the filesystem watcher's directory-change events, Lidarr rename/import/upgrade webhooks (complementing contrib/lidarr-rename-sidecars.sh; the webhook sweeps both the new and the old/previous directories, so a manual import that strands a sidecar self-heals), and -- when on_scan is also set -- after each library scan. Reactive realign always writes a JSONL backup (<db-dir>/realign-serve-backup.jsonl), never clobbers an existing sidecar, and auto-applies only exact provenance (ISRC/MBID) matches unless auto_apply_heuristic is set. Note that relocating a sidecar to audio in a different directory additionally requires cross_directory and a provenance tag embedded in the sidecar.
| Key | Env | Default | Meaning |
|---|---|---|---|
enabled |
MXLRC_REALIGN_ENABLED |
false |
Master switch for serve-mode reactive realign (watcher / webhook / post-scan). The realign CLI command runs regardless. |
on_scan |
MXLRC_REALIGN_ON_SCAN |
false |
Also run a library-wide realign after each periodic or manual scan (the watcher and webhook triggers are governed by enabled alone). |
require_provenance |
MXLRC_REALIGN_REQUIRE_PROVENANCE |
false |
Only apply exact (ISRC/MBID) matches; heuristic candidates are reported but skipped. |
cross_directory |
MXLRC_REALIGN_CROSS_DIRECTORY |
false |
Allow an exact match to move a sidecar to an audio file in a different directory within the same library. |
identity_keys |
MXLRC_REALIGN_IDENTITY_KEYS |
["mbid","isrc"] |
Ordered provenance identifiers the exact tier matches on, most authoritative first. Valid values: mbid, isrc. |
min_confidence |
MXLRC_REALIGN_MIN_CONFIDENCE |
0.75 |
Jaro-Winkler name-similarity floor (0-1) for a heuristic rename. Only the title is compared (the sidecar's [ti:] or its filename stem vs the audio's title or stem); the artist is excluded, because inside an album directory every candidate shares it and it discriminates between none of them. When neither side yields an artist/title the check is skipped (positional matching). |
auto_apply_heuristic |
MXLRC_REALIGN_AUTO_APPLY_HEURISTIC |
false |
Whether serve-mode reactive realign may auto-apply heuristic (name-similarity) matches. Off = unattended realign applies only exact provenance matches; the manual CLI is unaffected. |
name_match |
MXLRC_REALIGN_NAME_MATCH |
false |
Enable the opt-in N:M name-similarity matcher for directories with multiple orphaned sidecars and multiple sidecar-less audio files. Each orphan is scored against every remaining candidate and paired only when unambiguous (see min_margin); off by default, such directories are reported ambiguous. |
min_margin |
MXLRC_REALIGN_MIN_MARGIN |
0.05 |
Minimum score separation (0-1) an orphan's best-scoring candidate must hold over its runner-up before the pairing is accepted. Applies to both name-similarity tiers: for heuristic-nm the runner-up is the orphan's next-best candidate in the matrix; for the single-candidate heuristic tier it is the orphan's best score against any other audio file in the directory, including ones that already have a sidecar. Values outside [0,1) reset to the default. |
The exact tier requires ISRC/MBID-tagged audio; libraries without those tags fall back to the heuristic tier (or, with name_match enabled, the N:M matcher for directories the single-candidate heuristic can't resolve).
[timing_validation]¶
[timing_validation]
enabled = false
revalidate_existing = false
revalidate_batch = 100
on_mis_synced = "demote"
on_categorical = "quarantine"
Governs serve mode's ongoing timing sweep, which re-judges synced .lrc files already on disk against the exact duration of the audio they sit beside. Four verdicts matter: ok, mis_synced (the cues run past the end of the audio, but the words are the right song's - the timing is wrong, not the content), categorical (the last cue sits far past the end, so the file holds a different song's lyrics entirely), and degenerate (every cue carries the same timestamp, so the file is not synced at all). A degenerate file has the right words and unusable timing, so on_mis_synced governs it as well. The revalidate command (dry-run by default, --apply to act) does the same work on demand and runs regardless of every key here; see Revalidate.
There is no threshold knob, deliberately. The overrun tolerance and the categorical ratio are a co-calibrated pair owned by internal/timing, and one predicate serves all four callers: the accept-time write guard, the worker's outcome stamp, the CLI, and this sweep. A sweep-local threshold would let an unattended pass demote a lyric the write path had already accepted, and would make recorded timing_outcome values incomparable across a single deployment's own history. What this section configures is the remediation - what happens to a file once the verdict is in.
The sweep is designed to converge: it judges a batch per cycle, records each verdict against the row, and then idles. A row carrying a verdict leaves the candidate query, so the backlog drains once rather than being re-read every cycle. Judging is ONE-WAY - a file judged once is left alone, and editing a .lrc by hand does not re-queue it; use the revalidate CLI to re-check a file that already carries a verdict.
Setting both action keys to off gives an observability-only mode: every verdict is still evaluated and recorded (visible on /metrics and in the Review queue report), and nothing on disk is touched. That is the recommended way to see what a sweep would do before letting it act.
| Key | Env | Default | Meaning |
|---|---|---|---|
enabled |
MXLRC_TIMING_VALIDATION_ENABLED |
false |
Master switch for the serve-mode sweep. The revalidate CLI command runs regardless. |
revalidate_existing |
MXLRC_TIMING_VALIDATION_REVALIDATE_EXISTING |
false |
Drain the backlog of .lrc files written before the accept-time guard existed. Both this and enabled must be true for the sweep to run. |
revalidate_batch |
MXLRC_TIMING_VALIDATION_REVALIDATE_BATCH |
100 |
How many sidecars one cycle judges. Modest on purpose: each candidate costs a read of the .lrc, and on a cold duration cache one duration probe of its companion audio to bank that duration - usually a header read, but a whole-file read for a VBR MP3 with no Xing header (~7% of MP3s measured), which is frame-counted end to end. That probe is paid once per file VERSION, never once per cycle, so the cost falls away as the cache fills; a small batch keeps a parked library array quiet meanwhile. Values below 1 reset to the default. |
on_mis_synced |
MXLRC_TIMING_VALIDATION_ON_MIS_SYNCED |
demote |
Action for a MisSynced lyric. One of demote (write the plain words as .txt beside the audio, move the .lrc aside), quarantine (move it aside, keep nothing), purge (delete it - irreversible), off (record only). |
on_categorical |
MXLRC_TIMING_VALIDATION_ON_CATEGORICAL |
quarantine |
Action for a categorical lyric. One of quarantine, purge (irreversible), off. No demote: the words belong to another song, so there is nothing worth keeping as .txt. |
Quarantined files are moved under a quarantine root, preserving their path relative to their library root so two same-named sidecars from different albums cannot collide. purge unlinks the file outright; the JSONL backup record is then the only trail, which is why it is never a default.
[instrumental_title]¶
The instrumental title rule: in serve mode, a track whose title says it is an instrumental version is settled with an instrumental marker instead of being looked up. It is on by default (strong); set mode = "off" to disable it. It applies to serve mode only (one-shot fetch is unaffected), and never replaces a lyric file or an instrumental marker already on disk: it changes only tracks with nothing on disk yet. The mode is read at startup, so a change needs a restart. A sung track whose title carries a strong qualifier gets a marker and no lyrics while the rule is on; the rationale and that residual risk are in "Title rule" in instrumental-detection.md. The bulk undo is to set the mode back to off, then run scan purge-provenance --source title-rule.
| Key | Env | Default | Meaning |
|---|---|---|---|
mode |
MXLRC_INSTRUMENTAL_TITLE_MODE |
strong |
strong (the default): a track whose title carries a strong instrumental qualifier (a bracketed or dash-suffixed "Instrumental" or "Off Vocal") is settled with an instrumental marker without asking a lyric provider. Karaoke and sing-along releases are never settled this way. off: the track title is ignored; set it to disable the rule. all: as strong, and a track with a weaker title or album signal (the word outside a qualifier, a Piano Version, an album named Instrumental) is put to the audio detector first and settled with the detector's marker only if it agrees; otherwise it is looked up as usual. The weak tier needs the detector and ordered provider dispatch. strong_weak is the old name for all and is still accepted. An unrecognized file value resets to off; config set rejects an unrecognized value and leaves the setting unchanged; an invalid env value warns and keeps the current setting. |
[word_sync_recheck]¶
The unattended counterpart of canticle scan reconcile-word-sync. That command re-examines the tracks that are settled when you run it; this sweep keeps feeding newly settled line-synced tracks (including line-synced files a scan adopted) into the same word-timing re-check, so a library does not depend on someone re-running the command. Each re-checked track costs at least one provider request at the worker's pace; read Word-timing re-check cost before enabling it.
The sweep runs at startup and then on the scan interval (every 6 hours in scan-once mode). It fetches nothing itself: it flips candidates into the same re-check mode the CLI uses, and the worker drains them behind fresh work. While they wait they count as deferred rows. Like the CLI, the sweep does nothing while output.word_sync_mode is off (it logs that once at startup). A track that comes back without a verdict, because its word lane never answered within the worker's wait budget or a rescan reopened it for an ordinary fetch, is held out for a week before the sweep queues it again, so an unavailable provider does not cost a wait budget per track per cycle. The CLI is not held by that week.
| Key | Env | Default | Meaning |
|---|---|---|---|
enabled |
MXLRC_WORD_SYNC_RECHECK_ENABLED |
false |
Master switch for the serve-mode sweep. The scan reconcile-word-sync command runs regardless. |
batch |
MXLRC_WORD_SYNC_RECHECK_BATCH |
100 |
The most rows the serve sweep keeps in word-recheck mode at once, across cycles: each cycle admits only batch minus the rows still waiting, so re-checks never pile up behind themselves. scan reconcile-word-sync --limit queues independently of this cap, so the true in-flight count can exceed it. Range 1-1000; a file value outside it resets to the default, and an out-of-range environment variable is ignored (the file or default value stands). For a one-time bulk pass larger than that, use the CLI's --limit. |
[upgrade_sweep]¶
The serve-mode counterpart of --upgrade, which does not re-queue a track whose queue row is already done, with one exception: a scan run with --upgrade or --update re-queues a file a scan adopted, once (see CLI reference). On the same cadence as [word_sync_recheck], it re-queues settled tracks below line sync (unsynced lyrics, instrumental markers, a .lrc with no timestamps; this includes files a scan adopted, which Canticle never fetched) for a re-fetch behind fresh work, each at most once a week. A worse result never replaces what is on disk, even under serve --update. A re-fetch that lands nothing (no match, a result refused for its timing or rejected by the script guard or verification, or three failed attempts) keeps the file, leaves the track's record as it was, and does not count toward max_miss_attempts. A scan or webhook for the same track while it waits does not cancel it. Line-synced tracks are left to [word_sync_recheck]. The same sweep also gives a track whose timing was judged mis_synced after it settled (by the timing sweep or revalidate --apply, never by a fetch) one re-fetch under the current providers. Only a result judged correctly timed against the audio file's own duration replaces the demoted or kept file; when that duration is unavailable (enrichment.enabled off, or the file's tags cannot be read), the file is kept. Once the providers have answered (including with a result no better than the file already on disk), the track is not re-asked until the provider set changes; a re-fetch that never reached a provider is retried after a week. Word-sync generation waits for that re-fetch before it retimes such a track, unless this sweep is not running (disabled, or lyrics fetching is off), in which case generation retimes it without one and serve says so once at startup. Each re-fetch costs at least one provider request at the worker's pace.
| Key | Env | Default | Meaning |
|---|---|---|---|
enabled |
MXLRC_UPGRADE_SWEEP_ENABLED |
false |
Master switch. |
batch |
MXLRC_UPGRADE_SWEEP_BATCH |
100 |
The most tracks waiting for a re-fetch at once; each cycle admits only batch minus the tracks still waiting. Range 1-1000, validated like word_sync_recheck.batch. |
ffmpeg resolution¶
Both verification and the instrumental detector need ffmpeg to extract an audio sample. You do not have to install or locate it yourself: when verification is enabled or a classifier URL is configured, the service resolves ffmpeg in this order:
- an explicit configured path (
ffmpeg_path), if set - a missing configured binary is a hard error, never a silent download (air-gapped installs get a clear failure); - a previously auto-provisioned binary in the cache;
- an
ffmpegalready on yourPATH; - otherwise, a checksum-pinned static
ffmpegbuild is downloaded over HTTPS, verified against a pinned SHA256, and cached.
The auto-download is available for Linux (amd64/arm64) and Windows (amd64). On macOS there is no published static build, so install ffmpeg yourself (brew install ffmpeg) or set ffmpeg_path. The cached binary lives next to the database (the same data directory, or /config under Docker), under ffmpeg-<version>/.
Licensing: we do not bundle ffmpeg. The auto-downloaded build is a GPL static build from BtbN/FFmpeg-Builds, fetched at runtime only when needed. The official Docker images already include Alpine's ffmpeg package, so containers resolve it on PATH (step 3) and never download.
[guard]¶
Optional language/script guard. It rejects fetched lyrics whose body is dominated by scripts outside the allowlist, so a wrong-language match (for example a Cyrillic or CJK body returned for a Latin-script track) is never written or cached. An empty accepted_scripts list (the default) disables the guard. Supported buckets: Latin, Han, Kana, Hangul, Other (env: MXLRC_GUARD_ACCEPTED_SCRIPTS, MXLRC_GUARD_THRESHOLD).
[queue]¶
The worker shuffles its dequeue order within each priority tier so it stops querying the upstream API in strict alphabetical (library insertion) order, which is a plausible scraping fingerprint. This is on by default and affects only the library/serve worker path (Dequeue); inspection output (queue list) stays deterministic, and the one-shot fetch CLI never touches the work queue. Set randomize = false (or MXLRC_QUEUE_RANDOMIZE=false) to restore the deterministic created_at/id ordering. The env var overrides the TOML key; an invalid value warns and keeps the current setting.
batch_size controls a shuffled lookahead buffer: rather than reshuffling the whole eligible pool on every claim (ORDER BY RANDOM()), the worker draws batch_size rows at random once, stamps them with an order, and serves them in it before drawing again. This gives the queue a stable, knowable near-term order (so upcoming work can be displayed and starvation is observable) and lets the claim use an index instead of sorting the whole table each time. The randomness moves from per-item to per-batch, so an external observer still sees a randomly composed request stream and the anti-fingerprint property is preserved. Webhook-priority work still preempts within one dequeue. Default 10; set batch_size = 0 (or MXLRC_QUEUE_BATCH_SIZE=0) to disable batching and restore the per-item random path, a clean rollback. Only applies when randomize = true.
[watcher]¶
Optional low-latency filesystem watcher that layers targeted scans over the periodic scheduler (env: MXLRCGO_WATCH_ENABLED, MXLRCGO_WATCH_DEBOUNCE_MS, MXLRCGO_WATCH_MAX_DIRS). It is off by default. See Filesystem watcher in the User Guide for the operational detail.
| Key | Default | Purpose |
|---|---|---|
enabled |
false |
Master switch for the watcher. |
debounce_ms |
2000 |
Quiet period in milliseconds after the last filesystem event before a targeted scan fires; coalesces event storms. A non-positive value is clamped to the default at construction. |
max_dirs |
100000 |
Caps how many directories may be watched before startup fails - a safety valve so a misconfigured root fails fast instead of exhausting the kernel inotify watch budget. A non-positive value is clamped to the default. |
[logging]¶
[logging]
level = "info"
format = "text"
# file = ""
# max_size_mb = 10
# max_files = 5
# max_age_days = 30
# compress = true
Log level, format, and the rotating file-log settings (env: MXLRC_LOG_LEVEL, MXLRC_LOG_FORMAT, MXLRC_LOG_FILE, MXLRC_LOG_MAX_SIZE_MB, MXLRC_LOG_MAX_FILES, MXLRC_LOG_MAX_AGE_DAYS, MXLRC_LOG_COMPRESS).