summaryrefslogtreecommitdiff
path: root/f3s/beets-art/helm-chart/templates/cronjob.yaml
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: {}