211 lines
7.4 KiB
YAML
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
|