Skip to content

Media Pipeline

The music acquisition pipeline: Lidarr → Soularr → slskd → Soulseek network → NFS.

Architecture

Lidarr (wanted list)
    ↓ (soularr polls every 5m)
Soularr (bridges Lidarr ↔ slskd)
    ↓ (slskd API)
slskd (Soulseek client, behind NordVPN via gluetun)
    ↓ (Soulseek P2P network)
NFS downloads volume (nfs-slskd-downloads, 1Ti)
    ↓ (soularr renames, then calls Lidarr's DownloadedAlbumsScan)
Lidarr moves the files into its root folder
NFS music volume (/mnt/Arr1/SMB/Media/music)
Plex serves (library section 3, path /music)

Soularr does not move files itself. It renames the completed download folder to Artist - Album (Year) and calls Lidarr's DownloadedAlbumsScan API; Lidarr performs the move. Anything soularr is not tracking therefore never leaves the downloads PVC — see Music Sweeper, which exists to catch exactly that.

Directory Layout

Worth stating plainly, because three services mount the same two exports at different paths and the strings are not interchangeable.

Export slskd soularr Lidarr Plex music-sweeper
nfs-slskd-downloads /app/downloads /downloads /slskd-downloads /downloads
nfs-media-music /music /music /music /music

Lidarr's root folder is /music/lists, not /music. Imports land at /music/lists/<Artist>/. The album folders sitting directly under /music were copied in by hand and are outside Lidarr's management, though Plex indexes both because its library is the /music root.

Lidarr has renameTracks: false, so standardTrackFormat is inert and imported tracks keep their original filenames in a flat artist folder rather than being organized into Album (Year)/ subfolders.

Services

slskd

Property Value
Pod slskd-vpn-* in heezy namespace
Image slskd/slskd:latest
HTTP port 5030 (NodePort 30030, MetalLB VIP 192.168.1.34)
Soulseek port 50300 (NodePort 30300, VIP 192.168.1.34)
VPN gluetun sidecar, NordVPN Netherlands
Downloads PVC nfs-slskd-downloads (1Ti NFS, RWX)
Data PVC slskd-data (20Gi Longhorn)
Manifests heezy-k8s/apps/gluetun/deployment.yaml

Critical: slskd stores its SQLite databases in /app/data/. The init container creates this directory before chown. If the pod restarts and /app/data is missing, slskd will fail to connect to Soulseek. The init container command is mkdir -p /app/data && chown -R 1000:1000 /app.

API key: stored in k8s secret slskd-secrets, key SLSKD_API_KEY.

Soularr

Property Value
Pod soularr-* in heezy namespace
Image mrusse08/soularr:latest
Config ConfigMap soularr-config mounted at /data/config.ini
Downloads mount nfs-slskd-downloads PVC at /downloads
Manifests heezy-k8s/apps/soularr/

Config is in code. heezy-k8s/apps/soularr/configmap.yaml contains the full config.ini. To change Lidarr API key, slskd API key, or search settings — edit the ConfigMap, commit, push. The pod picks up changes on next restart.

Pod affinity: soularr is pinned to the same node as slskd-vpn via podAffinity. The downloads PVC is RWX NFS and does not require co-location; the affinity is there to keep soularr's slskd API calls on-node.

Lidarr

Property Value
Pod lidarr-* in heezy namespace
HTTP port 8686 (NodePort 30686)
API key 4800d497ba18406787014fd91137a917 (from /config/config.xml)
Manifests heezy-k8s/apps/lidarr/

Configuration

soularr config.ini

The config is managed as a Kubernetes ConfigMap (heezy-k8s/apps/soularr/configmap.yaml). Key settings:

[Lidarr]
api_key = 4800d497ba18406787014fd91137a917
host_url = http://lidarr:8686
# The staging directory as LIDARR sees it. soularr never stats this path, it
# only hands it to the DownloadedAlbumsScan API, so it must be valid inside the
# Lidarr container rather than inside soularr's.
download_dir = /slskd-downloads

[Slskd]
api_key = <from slskd-secrets>
host_url = http://slskd:5030
# The same PVC as SOULARR sees it.
download_dir = /downloads

[Search Settings]
allowed_filetypes = flac 24/192,flac 16/44.1,flac,mp3 320,mp3
search_source = missing
number_of_albums_to_grab = 10

Storage

Volume Type Size Mount Purpose
nfs-slskd-downloads NFS (RWX) 1Ti /app/downloads in slskd, /downloads in soularr, /slskd-downloads in lidarr Active downloads
slskd-data Longhorn (RWO) 20Gi /app in slskd slskd SQLite DBs, config
nfs-media-music NFS (RWX) 10Ti /music in slskd, lidarr, plex and music-sweeper Final music library

Backing paths on the NFS server (192.168.1.200): the downloads PVC is /mnt/Arr1/SMB/pvc-bc74231e-ac88-4991-902c-44fc8c5e8296, the music export is /mnt/Arr1/SMB/Media/music. They are separate mounts inside every container, so a move between them is a copy plus delete, not a rename.

Disk space alert: Grafana fires when nfs-slskd-downloads exceeds 80% (warning) or 95% (critical). Check the heezy — Media Pipeline dashboard for current usage.

Monitoring

Dashboard: heezy — Media Pipeline at grafana.heezy.info

  • slskd connection status (log-based)
  • slskd VPN status (log-based)
  • Downloads PVC usage (Prometheus kubelet_volume_stats_*)
  • Lidarr error and grab logs
  • Soularr activity logs

Troubleshooting

slskd not connecting to Soulseek

  1. Check VPN: kubectl logs -n heezy <slskd-vpn-pod> -c gluetun | tail -20
  2. Check slskd data dir: kubectl exec -n heezy <pod> -c slskd -- ls /app/data/
  3. If /app/data is empty or missing DBs: kubectl exec -n heezy <pod> -c slskd -- chmod -R 777 /app/data/ then wait 30s
  4. Check slskd API: kubectl exec -n heezy <pod> -c slskd -- wget -qO- --header="X-API-Key: <key>" http://127.0.0.1:5030/api/v0/application

Soularr "Config file does not exist"

The ConfigMap is not mounted. Check:

kubectl get configmap -n heezy soularr-config
kubectl exec -n heezy <soularr-pod> -- ls /data/
If the ConfigMap is missing, re-apply: kubectl apply -k heezy-k8s/apps/soularr/

Lidarr API key rejected (401)

The API key in soularr-config ConfigMap must match Lidarr's config.xml. Get the real key:

kubectl exec -n heezy <lidarr-pod> -- cat /config/config.xml | grep ApiKey
Update heezy-k8s/apps/soularr/configmap.yaml and push.

Downloads PVC full

The usual cause is the orphan backlog, not active transfers. Check the count before anything else:

kubectl exec -n heezy deploy/lidarr -- sh -c 'ls /slskd-downloads | wc -l'

Music Sweeper is what drains this. Trigger a run and read the Discord summary:

kubectl create job -n heezy --from=cronjob/music-sweeper sweep-manual-$(date +%s)

To drain faster, raise MAX_ALBUMS in heezy-k8s/apps/music-sweeper/cronjob.yaml.

Do not bulk-delete by extension or mtime. A folder in this directory may be the only copy of an album that was never imported, and age is not evidence otherwise — the backlog contains complete albums going back years. Let the sweeper match against the library first.