Skip to content

Music Sweeper

k8s: heezy namespace, one CronJob. No web UI, no Service, no hostname. Source: heezy-containers/dockerfiles/music-sweeper/ Manifests: heezy-k8s/apps/music-sweeper/ Image: 025066240222.dkr.ecr.us-east-2.amazonaws.com/heezy-music-sweeper Schedule: */30 * * * * Last Updated: 2026-08-14


Why It Exists

soularr is the only thing in the media pipeline that moves files, and it only imports albums it grabbed inside its own run, tracked in memory. Three things fall through:

  • Manual slskd searches. Nothing scans the downloads directory, and Lidarr has no slskd download client configured, so a hand-grabbed album has no path to /music at all.
  • Downloads that outlive the run that started them. soularr moves on; the transfer completes into a folder nobody revisits.
  • Failed imports. These land in failed_imports/ and stay.

Measured on 2026-08-14, before the sweeper existed:

Count
Folders in the downloads PVC 6,559
Still carrying raw remote names (never handed to Lidarr) 6,239
Carrying soularr's Artist - Album (Year) form 320
In failed_imports/ 129
Name-identical to a folder already under /music 252

The worked example was Chris Hülsbeck: a monitored artist in Lidarr with all four Turrican Soundtrack Anthology volumes listed as monitored albums, 2.5GB of FLAC sitting in the PVC, and zero mentions of "Turrican" in soularr's logs. Hand-grabbed, so nothing was ever going to import it.


What One Run Does

  1. Ask slskd which folders it is currently writing into. If slskd cannot be reached the run stops here — "no answer" is not "nothing active", and moving a folder mid-transfer corrupts it.
  2. Take up to MAX_ALBUMS (25) folders that are not in flight, not modified in the last QUIET_MINUTES (15), and actually contain audio.
  3. Delete slskd's ticks-suffixed re-grabs, keeping the largest of each collision.
  4. Identify the album from tags, falling back to parsing the folder name.
  5. If the library already holds it, delete the download.
  6. Otherwise rename to Artist - Album (Year) and hand it to Lidarr via DownloadedAlbumsScan. Success is measured by the audio being gone afterwards, not by the command status, which reports completed even when it imported nothing.
  7. If Lidarr declines, move the folder into /music directly.
  8. Refresh only the Plex paths that changed, then post a summary to Discord.

Steps 6 and 7 are the reason both destinations exist. Lidarr-imported albums land organized under /music/lists/<Artist>/; everything else lands flat at /music/<Artist> - <Album> (<Year>)/. Both are inside Plex's library.


The slskd Ticks Suffix

When a file arrives whose name already exists in the destination, slskd appends .NET ticks rather than overwriting:

01_Shoot or Die.flac
01_Shoot or Die_639223320842621518.flac

These are re-grabs of the same track, and an album carrying them will fail a Lidarr import because the track count no longer matches the release. Turrican Vol. 1 and Vol. 2 had 15 each.

The sweeper groups files by their ticks-stripped name and keeps the largest, tie-breaking to the unsuffixed original. A group with no ticks-suffixed member is two genuinely different files and is left alone.


Safety Properties

These are deliberate and worth preserving through any edit.

Property Mechanism
Unidentifiable albums are never deleted album_key() returns None unless both artist and album normalize to something non-empty
A partial in the library cannot delete a full download Library copy must have ≥ 90% as many tracks (COMPLETENESS_RATIO)
Destinations are never overwritten An existing path gets a timestamp suffix
Blast radius is bounded MAX_ALBUMS per run
Manual runs cannot collide with scheduled ones Lock file under .sweeper/, stale after 3h
A pulled image cannot do damage Dockerfile CMD passes --dry-run
One bad folder does not end the batch Per-folder exception handling, reported in the summary

Configuration

Full table in the app README. The ones that matter operationally:

Variable Default Notes
MAX_ALBUMS 25 Folders per run. Raise to drain the backlog faster
QUIET_MINUTES 15 Ignore recently-modified folders
DRY_RUN false Set true to report without changing anything
DELETE_DUPLICATES true Set false to report duplicates and keep them
LIDARR_DOWNLOADS_DIR /slskd-downloads The downloads PVC as Lidarr mounts it

LIDARR_DOWNLOADS_DIR is the same trap soularr documents in its ConfigMap. The sweeper mounts the PVC at /downloads, Lidarr at /slskd-downloads, and Lidarr is handed a string it must resolve itself. Change one mount, change both.


The Library Index

Tag-reading a 40TB library every 30 minutes is not viable over NFS, so the normalized artist|album index is cached to $STATE_DIR/library-index.json (default /downloads/.sweeper/) and invalidated per folder by mtime. The first run is slow; later runs re-read only what changed.

Force a rebuild:

kubectl exec -n heezy deploy/lidarr -- rm /slskd-downloads/.sweeper/library-index.json

Operations

Trigger a run:

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

The Discord embed is the intended interface — counts per outcome, what moved, what was deleted and what it already had. Logs are the fallback:

kubectl logs -n heezy -l job-name=sweep-manual-<ts> --tail=50

It reports "slskd unreachable"

Expected during a gluetun VPN reconnect. Nothing was swept and nothing was harmed. If it persists, see Media Pipeline.

It deleted something it should not have

Every deletion names the library folder it matched against. If the match was wrong, the fix is DELETE_DUPLICATES=false while the normalization in app/naming.py is corrected — the matching rules are shared with media-audit.py, so a fix belongs in both.

The backlog is not shrinking

Check the still queued count in the embed. At 25 albums per run on a half-hourly schedule the ceiling is 1,200 a day, so a 6,000-folder backlog takes about five days. Raise MAX_ALBUMS to move faster, at the cost of more NFS traffic per tick.