Files
homelab/kubernetes/apps/navidrome/beets.yaml
renovate-bot c7a4adf273
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/review-bot-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
chore(deps): update all non-major dependencies
2026-10-12 02:03:24 +00:00

211 lines
7.4 KiB
YAML

# Beets fixes the tags of the music library in place, once a night (REN-349).
# It never moves, copies, renames or deletes a file: Lidarr and Navidrome
# paths stay valid. Navidrome picks up the new tags on its hourly scan.
#
# REN-350: two dry runs with write: no were checked (match report and a
# 5-album spot check), so beets now writes tags. Art plugins stay off: they
# would add or embed cover files, a separate change.
#
# Retry: the first write run matched 74 of 560 albums; most skipped folders
# hold only part of an album. Partial-album matches are allowed now (see
# max_rec). A -W trial matched 158 albums, all at 96% or better, and none of
# the 74 written albums changed release, so beets now writes them. Muse "Black
# Holes and Revelations" matched an instrumental release: it is ignored and
# dropped from the DB (DB only, no file change).
#
# Lidarr does not write tags: lidarr-reconcile sets writeAudioTags "no" on
# every pod start (lidarr.yaml), so it cannot undo the beets tags.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: beets-config
namespace: navidrome
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled
spec:
accessModes:
- ReadWriteOnce
storageClassName: ceph-block
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: beets-config
namespace: navidrome
data:
config.yaml: |
directory: /music
library: /config/library.db
statefile: /config/state.pickle
plugins: [musicbrainz, fetchart, embedart, lastgenre]
# Beets defaults plus folders whose only match is a wrong release.
ignore: ['.*', '*~', 'System Volume Information', 'lost+found',
'Black Holes and Revelations']
# Keep the first-release date in year; reissues must not change it.
original_date: yes
match:
preferred:
countries: ['DE', 'XE', 'XW', 'US', 'GB|UK']
media: ['CD', 'Digital Media|File']
original_year: yes
# Partial albums may match; a missing track costs less than a wrong
# title or artist (defaults: medium cap, weight 0.9).
max_rec:
missing_tracks: strong
distance_weights:
missing_tracks: 0.2
import:
write: yes
copy: no
move: no
link: no
hardlink: no
reflink: no
delete: no
quiet: yes
quiet_fallback: skip
incremental: yes
# Do not record skipped folders: they are retried the next night.
incremental_skip_later: yes
resume: no
# Never "remove": that deletes files.
duplicate_action: skip
# Both add or change cover art in /music; not part of the tag fix.
fetchart:
auto: no
embedart:
auto: no
lastgenre:
auto: yes
run.sh: |
set -u
beet() { /lsiopy/bin/beet -c /etc/beets/config.yaml "$@"; }
mkdir -p /config/logs
# One-time reset: the first dry run stored reissue matches in the DB.
# The DB is beets state only; no file in /music is touched.
if [ ! -f /config/.reset-ren350 ]; then
rm -f /config/library.db /config/state.pickle
touch /config/.reset-ren350
fi
# One-time: forget the skipped folders so they are retried. The 74
# written albums are re-imported too: beets re-matches them under the
# looser weights and replaces their DB entry (no duplicate skip).
if [ ! -f /config/.retry-ren350 ]; then
rm -f /config/state.pickle
touch /config/.retry-ren350
fi
# One-time: the trial stored an instrumental release for this album.
# `rm` without -d drops the DB entry only; the file is not touched.
if [ ! -f /config/.drop-ren350 ]; then
beet rm -a -f 'path:/music/Muse/Black Holes and Revelations'
touch /config/.drop-ren350
fi
beet ls -a -f '$path $mb_albumid' | sort > /config/logs/albums-before.txt
LOG=/config/logs/import-$(date +%Y%m%d-%H%M).log
beet import -l "$LOG" /music
rc=$?
beet ls -a -f '$path $mb_albumid' | sort > /config/logs/albums-after.txt
comm -23 /config/logs/albums-before.txt /config/logs/albums-after.txt \
> /config/logs/albums-changed.txt
echo "== import exit $rc"
echo "== albums with a changed release: $(wc -l < /config/logs/albums-changed.txt)"
cat /config/logs/albums-changed.txt
echo "== albums in library: $(beet ls -a | wc -l)"
echo "== skipped this run: $(grep -c '^skip ' "$LOG")"
echo "== duplicates skipped: $(grep -c '^duplicate-skip ' "$LOG")"
# The trial (-W) stored matches in the DB only; `beet write` puts them
# into the files (tags only, no move).
beet write -p > /config/logs/write-preview.txt 2>&1
echo "== items to write: $(grep -vc '^ ' /config/logs/write-preview.txt)"
beet write
wrc=$?
echo "== write exit $wrc"
beet write -p > /config/logs/write-after.txt 2>&1
echo "== items still differing after write: $(grep -vc '^ ' /config/logs/write-after.txt)"
echo "== skipped albums"
grep '^skip ' "$LOG"
echo "== written changes (first 2000 lines)"
head -n 2000 /config/logs/write-preview.txt
[ "$rc" -eq 0 ] && rc=$wrc
exit $rc
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: beets
namespace: navidrome
labels:
app.kubernetes.io/name: beets
spec:
schedule: "30 3 * * *"
timeZone: Europe/Berlin
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
startingDeadlineSeconds: 3600
jobTemplate:
spec:
backoffLimit: 0
# The first full import is rate-limited by MusicBrainz (1 req/s). An
# interrupted run is safe: incremental import resumes the next night.
activeDeadlineSeconds: 72000
template:
metadata:
labels:
app.kubernetes.io/name: beets
spec:
restartPolicy: Never
enableServiceLinks: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: beets
image: lscr.io/linuxserver/beets:2.14.1-ls356@sha256:ae076820ec34ea5e9ec05a22931e98ab555e9c25e784c5fb001c84f411119c73
# Bypass the s6 init: it needs root, beet does not.
command: ["sh", "/etc/beets/run.sh"]
env:
- name: TZ
value: Europe/Berlin
- name: BEETSDIR
value: /config
- name: HOME
value: /config
volumeMounts:
- name: config
mountPath: /config
- name: beets-config
mountPath: /etc/beets
readOnly: true
# Writable for tag writes; move/copy/delete stay off above.
- name: music
mountPath: /music
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumes:
- name: config
persistentVolumeClaim:
claimName: beets-config
- name: beets-config
configMap:
name: beets-config
- name: music
persistentVolumeClaim:
claimName: navidrome-music-bulk