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
|
# beets-art: nightly cover-art sweep for Navidrome
A Kubernetes CronJob that runs `beets` against the Navidrome music library
to fetch external cover art (`cover.jpg` in each album folder) and embed
art into every audio file. Navidrome's own hourly scan
(`ND_SCANSCHEDULE: "1h"`, see the [navidrome chart](../../navidrome/helm-chart/templates/deployment.yaml))
then picks the new art up.
## What it does
Every day at 12:00 Europe/Sofia (noon local) the CronJob runs three
idempotent steps inside a single short-lived pod on r1:
1. `beet import -A -q --quiet-fallback=asis /music` — registers any new
albums in the beets library (`incremental: yes` skips already-known
folders). With `auto: yes` set on both `fetchart` and `embedart`, this
step alone is enough for newly imported music.
2. `beet fetchart` — backfill pass for albums that previously failed art
lookup (rate limit, network blip, missing MBID). Idempotent: skips
albums that already have art.
3. `beet embedart` — embeds art into audio files where the picture frame
is missing. `compare_threshold: 50` and `ifempty: no` make it refuse
to overwrite existing embeds.
## Why a CronJob (not a host systemd timer)
* Declarative: lives in git, deployed by ArgoCD, same pattern as every
other f3s service.
* Sandbox: beets, ImageMagick, and ffmpeg dependencies are pinned to the
container image — no host pollution, no Python venv drift on r1.
* Restart safety: if the job dies mid-run, k8s reaps it and the next run
picks up where it left off (`incremental: yes`).
## Storage
* `navidrome-music-pvc` (200 GiB hostPath PV at
`/data/nfs/k3svolumes/navidrome/music`) — **shared with Navidrome**.
Mounted at `/music` inside the CronJob pod. RWO is fine because both
pods are pinned to r1 (multi-pod single-node mounts are allowed for
RWO PVCs).
* `beets-art-state-pvc` (2 GiB local-path PVC) — holds `library.db` and
`import.log` between runs. Regenerable: deleting it just forces the
next run to re-import the whole library.
* `beets-art-config` ConfigMap — mounted at `/etc/beets/config.yaml`
(read-only) via `BEETSDIR=/etc/beets`. Single source of truth for
beets settings.
## First run (initial backfill)
The first run will be **slow** (potentially hours) because:
* It has to import every album into the beets DB.
* Every album with no embedded art triggers a Cover Art Archive lookup.
* Every audio file gets rewritten to embed the new picture.
Trigger it manually instead of waiting for the nightly schedule:
```bash
just run-now
just logs # tail the job
```
While the backfill runs, ZFS-snapshot the music dataset first so the
embed pass is reversible. Subsequent nightly runs touch only new or
previously-failed albums and finish in minutes.
## Operations
```bash
just status # CronJob, recent Jobs, PVC, ArgoCD state
just logs # follow logs of the most recent Job
just run-now # ad-hoc Job from the CronJob template
just suspend # pause the schedule
just resume # resume the schedule
just shell # interactive beets shell with the same mounts
just sync # trigger ArgoCD sync
```
## Tuning knobs
In [templates/configmap.yaml](templates/configmap.yaml):
* `embedart.maxwidth` — embedded image cap (default 1200 px). Lower for
smaller files.
* `embedart.compare_threshold` — 0 = always overwrite, 100 = never.
Default 50 is "overwrite only when the new art is broadly similar".
* `fetchart.sources` — order of art providers. Cover Art Archive first
exploits MusicBrainz IDs already present from beets imports.
* `embedart.remove_art_file: no` — keep `cover.jpg` in album folders too;
Navidrome and cmus both benefit.
In [templates/cronjob.yaml](templates/cronjob.yaml):
* `schedule` — change from nightly if you want a different cadence.
* `activeDeadlineSeconds` — current cap is 6 h; raise for the very first
run if your library is huge.
## Failure handling
* Job exits non-zero → visible in `just status` and `kubectl get jobs`.
Inspect with `just logs`. Common causes: NFS hiccup, Cover Art Archive
rate limit. The next nightly run usually clears it.
* Wrong art picked for an album → `just shell`, then
`beet mbsync album:"Bad Album"` to refresh the MBID and re-fetch, or
drop a hand-sourced `cover.jpg` into the album folder and run
`beet embedart -f cover.jpg -- album:"Bad Album"`.
* Library DB corruption → delete the `beets-art-state-pvc` (state is
regenerable) and let the next run rebuild it.
|