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¶
- Check VPN:
kubectl logs -n heezy <slskd-vpn-pod> -c gluetun | tail -20 - Check slskd data dir:
kubectl exec -n heezy <pod> -c slskd -- ls /app/data/ - If
/app/datais empty or missing DBs:kubectl exec -n heezy <pod> -c slskd -- chmod -R 777 /app/data/then wait 30s - 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:
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:
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:
Music Sweeper is what drains this. Trigger a run and read the Discord summary:
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.