🎵 Ainulindale
"There was Eru, the One [...] and he made first the Ainur [...] and they sang before him, and he was glad."
— J.R.R. Tolkien, The Silmarillion
A fully self-hosted, automated music discovery and streaming system.
No Spotify. No YouTube Music. No ads. Just your music, your server, your rules.
What This Stack Does
- Streams your music library to any device via a Spotify-like interface
- Automatically discovers new music weekly based on your listening habits
- Downloads discovered tracks at high quality (FLAC/WAV preferred, 320kbps minimum) from the Soulseek network
- Scrobbles everything you listen to Last.fm and ListenBrainz
- Routes all P2P traffic through a VPN so your real IP is never exposed
Components
| Component | Role |
|---|---|
| Navidrome | Music server — indexes your library, streams to clients |
| Substreamer | Mobile app (iOS/Android) — plays music from Navidrome |
| Pangolin | Reverse proxy — exposes Navidrome securely to the internet |
| Gluetun | VPN gateway container — all Soulseek traffic exits via ProtonVPN |
| slskd | Soulseek client — searches and downloads music from the P2P network |
| autoheal | Watches slskd/gluetun health status and force-restarts them if they hang |
| Explo | Discovery engine — connects ListenBrainz recommendations to slskd |
| ListenBrainz | Open-source scrobbler + recommendation engine |
| Last.fm | Secondary scrobbler for stats and social features |
Prerequisites
- Unraid NAS (or any Linux server with Docker)
- ProtonVPN account (paid, for P2P/WireGuard support)
- Soulseek account — free at slsknet.org
- ListenBrainz account — free at listenbrainz.org
- Last.fm account — free at last.fm
- Last.fm API key — free at last.fm/api/account/create
- Substreamer app on your phone
- Pangolin or any reverse proxy for external access
Folder Structure
/mnt/user/appdata/
├── gluetun/ ← VPN state (auto-populated)
├── slskd/
│ └── slskd.yml ← slskd config (copy from repo)
└── explo/
├── .env ← Explo config (copy from .env.example)
└── config/ ← Explo's playlist cache + cover art (auto-populated, must persist)
/mnt/user/data/media/music/
├── incomplete/ ← In-progress downloads (Navidrome ignores these)
├── explo/ ← Explo-managed discovery downloads
└── (your library)/ ← Everything else
Setup
1. Clone the repo
git clone https://github.com/yourusername/ainulindale
cd ainulindale
2. Create your config files
cp .env.example .env
Fill in .env with your WireGuard private key, paths, and VPN country.
Copy slskd.yml to your appdata folder:
cp slskd.yml /mnt/user/appdata/slskd/slskd.yml
Fill in your Soulseek credentials and choose a web UI username/password.
Copy the Explo env block from .env.example into a separate file:
cp .env.example /mnt/user/appdata/explo/.env
Fill in your ListenBrainz username, Navidrome URL/credentials, and slskd API key.
3. Create required folders
mkdir -p /mnt/user/appdata/{gluetun,slskd,explo/config}
mkdir -p /mnt/user/data/media/music/{explo,incomplete}
4. Get your ProtonVPN WireGuard key
- Log into account.proton.me → VPN → Downloads
- Select WireGuard protocol and a P2P-capable server
- Generate the config and copy the
PrivateKeyvalue into your.env
5. Start the stack
# Start VPN first and verify it connects
docker compose up -d gluetun
docker compose logs -f gluetun
# Look for: "Public IP address is x.x.x.x"
# Then start slskd
docker compose up -d slskd
Open http://your-nas-ip:5030, log in with your slskd credentials, go to Options → API Keys, generate a key and paste it into /mnt/user/appdata/explo/.env as SLSKD_API_KEY.
6. Install Navidrome via Unraid Community Apps
Add these environment variables in the Navidrome container template:
| Variable | Value |
|---|---|
ND_LASTFM_APIKEY |
your Last.fm API key |
ND_LASTFM_SECRET |
your Last.fm shared secret |
ND_LASTFM_ENABLED |
true |
ND_SCANSCHEDULE |
1m |
7. Start Explo and autoheal
docker compose up -d explo autoheal
Unlike the rest of the stack, Explo doesn't need any external scheduler (no Unraid User Scripts, no cron). It runs continuously and schedules Weekly Exploration and Daily Jams itself via WEEKLY_EXPLORATION_SCHEDULE / DAILY_JAMS_SCHEDULE in docker-compose.yml, and EXECUTE_ON_START=false means restarting or updating the container never triggers an extra, out-of-schedule run. This is what keeps it from downloading the same week twice — see Troubleshooting below for the full explanation.
autoheal also just runs continuously in the background; it watches slskd and gluetun and restarts either one automatically if Docker marks it unhealthy.
How Discovery Works
You listen in Substreamer
↓
Navidrome scrobbles to Last.fm + ListenBrainz
↓
ListenBrainz builds your taste profile over time
↓
Every Monday night: ListenBrainz generates Weekly Exploration playlist
↓
Tuesday 00:15: Explo pulls recommendations
↓
slskd searches Soulseek network (behind ProtonVPN)
↓
Downloads FLAC/WAV/320kbps+ to /mnt/user/data/media/music
↓
Navidrome scans every 1 minute → appears in Substreamer
↓
You discover new music 🎵
Troubleshooting
slskd hangs and needs a manual restart every ~24h
This was almost always one of two things, both addressed in the current compose file:
- slskd itself getting stuck (memory pressure / stuck transfers) — fixed by pinning
slskd/slskd:0.25.1, which bundles a round of upstream fixes for stuck and failing transfers. - gluetun's tunnel silently hanging — since slskd runs inside gluetun's network namespace, a frozen gluetun looks identical to a frozen slskd. Fixed by pinning
qmcgaw/gluetun:v3.41.1, which resolves a healthcheck race condition that could make gluetun hang completely.
Both containers already ship a Docker healthcheck, but Docker does not restart a container just because it's unhealthy — that gap is why a manual restart was needed. The new autoheal service closes it: it watches both containers (via the autoheal=true label) and force-restarts whichever one goes unhealthy, with no manual intervention.
If slskd still misbehaves after this, check docker ps for its health status and docker logs slskd/docker logs gluetun before restarting manually.
Weekly Exploration downloaded multiple times
The old setup ran two one-shot explo-weekly / explo-daily containers (restart: "no", EXECUTE_ON_START=true) that were triggered by an external Unraid User Scripts cron. That design had two compounding problems:
- Every container restart (a host reboot, a Docker update, the scheduler firing slightly out of sync) re-ran Explo immediately because of
EXECUTE_ON_START=true, regardless of whether that week's playlist already existed. - Explo's playlist cache had nowhere to persist between runs (no
/opt/explo/configvolume), so each fresh container had no memory of "I already built this week's playlist" and would build it again.
The current compose runs a single, always-on explo container with its own internal cron (WEEKLY_EXPLORATION_SCHEDULE / DAILY_JAMS_SCHEDULE), EXECUTE_ON_START=false, and a persistent /opt/explo/config volume, plus the newer v1.1.0 image which includes further upstream fixes for scheduled-job bugs. Together this means Explo only ever runs on its own schedule, and remembers what it already did across restarts.
One-time cleanup: this fix prevents future duplicates, but it won't retroactively merge the duplicate Weekly-Exploration-2026-WeekXX playlists already sitting in Navidrome. Delete the extras once, manually, from the Navidrome UI (or via its API) — new runs going forward should stay to one playlist per period.
Tips
- ListenBrainz needs time — it takes a few weeks of scrobbling before Weekly Exploration playlists are generated. Speed this up by importing your Google Takeout YouTube Music watch history via ytm-extractor.
- Manual downloads — search for any artist or album directly in the slskd web UI at
http://your-nas-ip:5030. Downloads land straight in your Navidrome library. - Playlist import — use Soundiiz to transfer playlists from YouTube Music/Spotify directly into Navidrome. Tracks already in your library get matched automatically.
- Quality — slskd is tried first for every download. YouTube is only used as a last resort fallback for tracks not found on Soulseek.
- Sharing — slskd shares your music library back to the Soulseek network. The more you share, the better your download priority from other peers.
- ProtonVPN — the WireGuard private key in Gluetun pins you to a specific server. Make sure to use a P2P-capable server when generating your WireGuard config on ProtonVPN's site.
License
MIT — do whatever you want with it.