blob: 132160d53929562d139347e9bc1ca9140ba3e3bd (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
# beets-art: nightly sweep that fetches and embeds cover art for every
# album in the Navidrome music library, then lets Navidrome's own hourly
# scan (ND_SCANSCHEDULE=1h) pick the changes up.
#
# Why this works alongside Navidrome:
# - The music PVC is RWO. Kubernetes RWO permits multi-pod mounts as long
# as all pods land on the *same node*. Both Navidrome and this CronJob
# pin to r1 via nodeSelector, so concurrent mounts are fine.
# - Navidrome and beets only conflict on file *write* if they touch the
# same file at the same instant. Navidrome writes nothing under /music
# (read-only consumer); beets writes art into album folders and embeds
# into audio files. No real contention.
#
# Why it is safe to re-run forever:
# - import.incremental: yes -> already-imported album folders are skipped.
# - fetchart skips albums that already have a cover image.
# - embedart with ifempty: no + compare_threshold: 50 only embeds where
# missing, and refuses risky overwrites.
apiVersion: batch/v1
kind: CronJob
metadata:
name: beets-art
namespace: services
spec:
# Noon local time. timeZone is GA in k8s 1.27+; k3s 1.32 supports it.
schedule: "0 12 * * *"
timeZone: Europe/Sofia
# Forbid stacking: a long backfill run must not get a second worker on
# top of it (would race on the SQLite library DB).
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
# Do not auto-retry: art-fetch failures are usually rate-limit or
# network blips that resolve by the next nightly run. Spamming
# retries would just hammer Cover Art Archive.
backoffLimit: 0
# Cap a runaway run at 6h. The first backfill of a 200 GB library
# may take a few hours; steady state is minutes.
activeDeadlineSeconds: 21600
template:
spec:
restartPolicy: Never
nodeSelector:
kubernetes.io/hostname: r1.lan.buetow.org
containers:
- name: beets
image: lscr.io/linuxserver/beets:latest
imagePullPolicy: IfNotPresent
env:
# BEETSDIR must be writable: beets stores its incremental
# import state (state.pickle) there alongside the config.
# The ConfigMap mount at /etc/beets is read-only (kernel-
# enforced for ConfigMap volumes), so point BEETSDIR at the
# state PVC and pass `-c` on each command to load the
# ConfigMap-supplied config.yaml.
- name: BEETSDIR
value: /state
# Override the linuxserver s6 entrypoint; we just need the
# `beet` CLI for a one-shot job. Running as root (the image
# default when s6 is bypassed) so we can write into the NFS
# music tree, which Navidrome also writes as root.
command: ["/bin/sh", "-c"]
args:
- |
set -u
echo "=== $(date -u) beets-art sweep starting ==="
CONF=/etc/beets/config.yaml
BEET="beet -c $CONF"
# 1. Register any new albums. -A skips autotag (we trust
# the existing tags); auto fetchart/embedart fire here
# for newly imported albums via config.
echo "--- import (incremental) ---"
$BEET import -A -q --quiet-fallback=asis /music || \
echo "import returned non-zero (continuing)"
# 2. Backfill external cover.jpg for albums missing it.
echo "--- fetchart (backfill) ---"
$BEET fetchart || echo "fetchart returned non-zero (continuing)"
# 3. Embed art into audio files where missing.
# `beet embedart` (no -f) prompts "Modify artwork for N
# albums (Y/n)?" with no flag to bypass; pipe `yes` so
# it proceeds non-interactively. embedart still respects
# ifempty: no and compare_threshold: 50 from config.
echo "--- embedart (backfill) ---"
yes | $BEET embedart || echo "embedart returned non-zero (continuing)"
echo "=== $(date -u) beets-art sweep finished ==="
volumeMounts:
- name: music
mountPath: /music
- name: state
mountPath: /state
- name: config
mountPath: /etc/beets
readOnly: true
- name: tmp
mountPath: /tmp
resources:
# Generous because ImageMagick + ffprobe + SQLite scans can
# spike. Tighten after observing real usage.
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "2"
memory: 1Gi
volumes:
- name: music
persistentVolumeClaim:
# Reuse the existing Navidrome music PVC — single source of
# truth for the library tree. RWO is OK because both pods
# are pinned to r1.
claimName: navidrome-music-pvc
- name: state
persistentVolumeClaim:
claimName: beets-art-state-pvc
- name: config
configMap:
name: beets-art-config
- name: tmp
emptyDir: {}
|