delune
delune finds music on Soulseek (and Bandcamp and SoundCloud), checks every file, and files it into your Navidrome library — but only after you’ve looked at it. Think of it as Overseerr for Soulseek: the people who use your Navidrome can search, ask for albums, and follow artists, and you decide what lands in the library.
flowchart LR
A[Search or paste a link] --> B[Soulseek results,<br>ranked by quality]
B --> C[Download to staging]
C --> D[Decode, check,<br>tag, artwork, lyrics]
D --> E{You review}
E -- approve --> F[Move into the library<br>and rescan Navidrome]
What you get
- One server (
delune serve) with a web app that installs on your phone, a native Soulseek client (no slskd), and an HTTP API. - A terminal client (
delune-tui) for any machine that can reach the server. - A setup wizard (
delune setup) that asks the questions, tries each connection, and sets delune up as a service. - Sign-in with Navidrome accounts, per-person permissions, requests and approvals.
- A wishlist that keeps searching, follows for new releases, and optional quality upgrades of lossy albums.
- Sharing your library back on Soulseek, with slots, speed limits and schedules.
- Bandcamp prices and purchase downloads, SoundCloud artists and free downloads, and a hook for a downloader of your own.
Where it runs
delune runs next to Navidrome, on the machine (or network share) that holds your music, because Navidrome has no upload API: delune writes into the music folder and asks Navidrome to rescan. The web app and the terminal client work from anywhere that can reach the server.
If you just want it running, start with the Quick start.
Quick start
On Linux or macOS, one command installs delune and walks you through setting it up:
curl -fsSL https://raw.githubusercontent.com/PndaMan/delune/main/install.sh | sh
The script:
- works out your system (Linux or macOS, Intel or ARM),
- downloads the latest release and checks it against its published SHA-256,
- installs
deluneanddelune-tui(to~/.local/bin, or/usr/local/binas root), - starts
delune setup.
The wizard asks for your music folder, Navidrome and a Soulseek account, tries each
one, saves the settings and starts delune as a service. When it’s done it prints the
address to open, usually http://<this machine>:7474.
On NixOS, use the module instead: it keeps secrets out of the store and can route delune through a VPN.
With Docker, see Docker and Compose.
What you need
| Why | |
|---|---|
| A Navidrome server and an admin account on it | Sign-in, library checks and rescans |
| Write access to Navidrome’s music folder | Imports move files there |
| A Soulseek account | Searching and downloading (a new name is registered on first sign-in) |
| Optionally, a forwarded port | Other Soulseek users can reach you: more results, faster downloads |
After installing
- Open the web app and sign in with your Navidrome account. Navidrome admins are delune admins.
- Search for an album, open a result and press Download. It arrives in Review.
- Approve it, and it’s in Navidrome a moment later.
- On your phone, use Add to Home Screen for the app version. See On your phone.
The setup wizard
delune setup is a guided first run in the terminal. The install script starts it for
you; run it again at any time to change the answers.
delune setup # data in ~/.local/share/delune (or /var/lib/delune as root)
delune setup --data-dir /srv/delune
It needs a real terminal. Everything it sets can also be changed later in the web app under Settings → Connections.
The screens
| Screen | What it asks | What it checks |
|---|---|---|
| Folders | delune’s data folder, and your music folder | The data folder can be created; the music folder exists and is writable |
| Navidrome | Address, admin username and password (empty skips it) | Signs in, and confirms the account is an admin |
| Soulseek | Username, password, listening port (empty skips it) | Signs in to the Soulseek network |
| Network | The web address to listen on, and whether to open the port with UPnP | — |
| Keep it running | A user service, a system service, or nothing; whether to start now | — |
| Finish | A summary | — |
Each screen with a connection tries it when you press Enter and only moves on once it works. Keys: Enter next, Esc back, ↑↓ move, Space choose, Ctrl+C quit without changing anything.
What it writes
<data folder>/config.toml— the connections, readable only by you (mode 0600).- The UPnP choice, in delune’s database.
- A systemd unit:
- User service:
~/.config/systemd/user/delune.service, enabled and started withsystemctl --user. To keep it running while you’re logged out:loginctl enable-linger $USER. - System service (as root):
/etc/systemd/system/delune.service. Undersudo, it runs as the user who ran sudo, not root. - On NixOS it prints the module configuration instead, since
/etcis managed by your configuration.
- User service:
Checking a Soulseek account signs it in. If the same account is already signed in somewhere else (another delune, slskd, Nicotine+), that session is signed out.
Install script
curl -fsSL https://raw.githubusercontent.com/PndaMan/delune/main/install.sh | sh
install.sh is a POSIX shell script (tested under dash in CI) that:
- detects the platform: Linux or macOS,
x86_64oraarch64(and the native build under Rosetta); - finds the latest release, or the one you ask for;
- downloads it with retries and checks it against the release’s published SHA-256 (it refuses to install anything that doesn’t match);
- makes sure the programs run on this machine before replacing anything;
- installs
deluneanddelune-tuiby copying next to the target and renaming, so an interrupted install never leaves half a program; - starts
delune setupon a first install.
Running it again upgrades: it skips a version that’s already installed, restarts a running user service after an upgrade, and leaves your settings alone.
Options
Pass options after sh -s --:
curl -fsSL https://raw.githubusercontent.com/PndaMan/delune/main/install.sh | sh -s -- --version v0.1.0 --no-setup
| Option | Environment variable | What it does |
|---|---|---|
--version <tag> | DELUNE_VERSION | Install this release (v0.1.0 or 0.1.0). Default: the latest. |
--bin-dir <dir> | DELUNE_BIN_DIR | Install here. Default: ~/.local/bin (or $XDG_BIN_HOME), /usr/local/bin as root. |
--no-setup | DELUNE_NO_SETUP=1 | Don’t start delune setup afterwards. |
--force | DELUNE_FORCE=1 | Reinstall even if this version is already there. |
-y, --yes | DELUNE_YES=1 | Don’t ask questions (use sudo without asking if the folder needs it). |
--mirror <url> | DELUNE_DOWNLOAD_BASE | Download the release files from here instead of GitHub. |
--uninstall | Remove the programs and your user service. Your data is kept. | |
--dry-run | Show what would happen without changing anything. | |
-h, --help | Show the options. |
NO_COLOR turns colours off.
Root and sudo
The script never needs root for the default folder. If you choose a folder you can’t
write to (like /usr/local/bin as a normal user), it asks before using sudo, and
only uses it for creating, copying and renaming the programs.
Reading it first
Piping to a shell runs whatever the URL serves. To look before you run:
curl -fsSLO https://raw.githubusercontent.com/PndaMan/delune/main/install.sh
less install.sh
sh install.sh
Where things go
| Programs | ~/.local/bin/delune, ~/.local/bin/delune-tui |
| Data | ~/.local/share/delune (or /var/lib/delune as root) |
| Service | ~/.config/systemd/user/delune.service |
| Client settings | ~/.config/delune/tui.toml |
If ~/.local/bin isn’t on your PATH, the script prints the line to add to your shell’s
profile.
When there’s no release yet
Until the first release is tagged, the script says so and points to building from source.
NixOS
delune is a flake with a NixOS module. It runs delune as a hardened systemd service, reads secrets from a file outside the store, and can route everything through a VPN.
{
inputs.delune.url = "github:PndaMan/delune";
outputs = { nixpkgs, delune, ... }: {
nixosConfigurations.server = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
delune.nixosModules.default
({ config, ... }: {
services.delune = {
enable = true;
libraryDir = "/srv/music"; # the folder Navidrome scans
group = "navidrome"; # so delune can write there
soulseek.username = "your-name";
navidrome.url = "http://127.0.0.1:4533";
navidrome.username = "delune"; # a Navidrome admin
environmentFile = "/run/secrets/delune.env";
openFirewall = true; # the Soulseek port
};
})
];
};
};
}
The environment file holds the passwords:
DELUNE_SLSK_PASSWORD=…
DELUNE_NAVIDROME_PASSWORD=…
With sops-nix, render it from secrets:
sops.templates."delune.env".content = ''
DELUNE_SLSK_PASSWORD=${config.sops.placeholder."soulseek-password"}
DELUNE_NAVIDROME_PASSWORD=${config.sops.placeholder."navidrome-delune-password"}
'';
services.delune.environmentFile = config.sops.templates."delune.env".path;
The service listens on 127.0.0.1:7474; put your reverse proxy in front of it (see
Reverse proxy and HTTPS).
Keep delune’s own nixpkgs
Don’t make delune follow an older nixpkgs (a stable release, or an older unstable
pin). delune needs a recent Rust and Bun, and the web dependencies are pinned by a
hash that matches the Bun in delune’s own lock file. Following something older fails
with Unknown lockfile version or a hash mismatch.
inputs.delune.url = "github:PndaMan/delune"; # no inputs.nixpkgs.follows
Through a VPN
See Through a VPN for services.delune.vpn.container (gluetun
and friends) and services.delune.vpn.namespace.
Just the programs
nix run github:PndaMan/delune -- serve # the server
nix profile install github:PndaMan/delune # delune and delune-tui on your PATH
All options are listed in NixOS module options.
Updating
nix flake lock --update-input delune
sudo nixos-rebuild switch --flake .#your-host
Docker and Compose
The repository’s compose.yaml runs delune next to an existing Navidrome.
git clone https://github.com/PndaMan/delune && cd delune
cp .env.example .env # fill in Soulseek and Navidrome
docker compose up -d
services:
delune:
build: .
restart: unless-stopped
env_file: .env
environment:
DELUNE_DATA_DIR: /data
DELUNE_LIBRARY_DIR: /music
ports:
- "7474:7474" # web app and API
- "2234:2234" # Soulseek, so peers can reach you
volumes:
- delune-data:/data
- /path/to/music:/music # the folder Navidrome scans
volumes:
delune-data:
The image runs as an unprivileged user (nonroot, uid 65532). The music folder must
be writable by it. For a NAS share owned by a particular user, run the container as
that user (user: "1024:100") and make sure /data belongs to them too.
Reaching Navidrome from the container
localhost inside the container is the container, not your host. Point
DELUNE_NAVIDROME_URL at something the container can reach:
- another container on the same Compose network:
http://navidrome:4533; - a service on the host:
http://host.docker.internal:4533(addextra_hosts: ["host.docker.internal:host-gateway"]on Linux); - your reverse proxy’s public name.
If Navidrome only listens on 127.0.0.1 on the host, the container can’t reach it at
all. Let it listen on the bridge too, and keep the port closed to your LAN with the
firewall.
Through a VPN
compose.yaml has a commented-out gluetun service.
See Through a VPN.
NAS shares
Mount the share on the host, then bind it in. If it’s mounted on demand (autofs,
systemd automount), use :rslave so the container sees it once it appears:
volumes:
- /mnt/nas/music:/music:rslave
Arch Linux and Homebrew
Arch Linux
packaging/aur/PKGBUILD builds delune from source and installs:
/usr/bin/deluneand/usr/bin/delune-tui- a systemd service running as the
deluneuser /etc/delune/delune.envfor settings and passwords
sudoedit /etc/delune/delune.env
sudo systemctl enable --now delune
The service can write to /srv/music. For another music folder, run
systemctl edit delune and add it to ReadWritePaths=.
Homebrew (macOS and Linux)
packaging/homebrew/delune.rb installs the release binaries, and
brew services start delune runs the server with its data in Homebrew’s var.
The AUR package and the Homebrew tap are published with the first tagged release. Until then, use the install script (once a release exists) or build from source.
From source
You need Rust 1.90 or newer and Bun.
git clone https://github.com/PndaMan/delune && cd delune
(cd web && bun install && bun run build) # the web app, embedded at compile time
cargo build --release -p delune -p delune-tui
./target/release/delune setup
Or straight from Git with Cargo (it builds the web app for you only if web/dist
exists, so prefer the steps above):
cargo install --git https://github.com/PndaMan/delune delune delune-tui
A portable Linux binary
Release builds for Linux are static (musl), so one file runs on any distribution:
rustup target add x86_64-unknown-linux-musl
sudo apt install musl-tools # or your distribution's musl-gcc
cargo build --release --target x86_64-unknown-linux-musl -p delune -p delune-tui
Developing
scripts/dev.sh runs the API server and the hot-reloading web app together; open
http://localhost:5173. See Contributing.
Running as a service
delune setup writes a unit for you. If you’d rather do it by hand, this is what it
writes for a user service (~/.config/systemd/user/delune.service):
[Unit]
Description=delune
Wants=network-online.target
After=network-online.target
[Service]
ExecStart="/home/you/.local/bin/delune" serve
Environment="DELUNE_DATA_DIR=/home/you/.local/share/delune"
Environment="DELUNE_BIND=0.0.0.0:7474"
Restart=on-failure
RestartSec=5
UMask=0002
[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now delune
loginctl enable-linger $USER # keep it running when you're logged out
journalctl --user -u delune -f # logs
A system-wide unit is the same with User= set and WantedBy=multi-user.target,
in /etc/systemd/system/. The packaged unit (packaging/systemd/delune.service) adds
sandboxing: a dedicated user, a read-only system, and write access only to the data
and music folders.
Passwords can live in config.toml (written by the wizard or the web app, mode 0600)
or in an EnvironmentFile= that only the service can read.
macOS
brew services start delune, or a LaunchAgent that runs delune serve with
DELUNE_DATA_DIR set.
Navidrome
delune uses Navidrome for three things:
- Sign-in. Everyone signs in with their Navidrome account. Navidrome admins are delune admins, and that follows Navidrome within about 20 seconds when it changes.
- Library checks. Search results and album pages show what you already have.
- Rescans. After an import, delune asks Navidrome to scan, and retries for a few minutes if Navidrome is down.
The account delune uses
Give delune its own admin account in Navidrome (Settings → Users → add, with
Is admin on), for example delune. Admin rights are needed to look people up and
start scans. Its password goes in DELUNE_NAVIDROME_PASSWORD, config.toml, or your
secrets manager — never in the Nix store.
Check it before you point delune at it:
# Subsonic's token auth: md5(password + salt)
salt=$(head -c6 /dev/urandom | od -An -tx1 | tr -d ' \n')
token=$(printf '%s%s' "$PASSWORD" "$salt" | md5sum | cut -d' ' -f1)
curl -s "http://127.0.0.1:4533/rest/getUser.view?u=delune&t=$token&s=$salt&v=1.16.1&c=check&f=json&username=delune"
# … "status":"ok" … "adminRole":true
Can delune reach it?
DELUNE_NAVIDROME_URL must be an address delune can reach, which isn’t always the
one you use in a browser:
| delune runs… | Navidrome at | Use |
|---|---|---|
| on the same host | 127.0.0.1:4533 | http://127.0.0.1:4533 |
| in a container, Navidrome on the host | host loopback only | Not reachable. Make Navidrome listen on the container bridge too (see below). |
| in the same Compose project | a service called navidrome | http://navidrome:4533 |
| inside a VPN container’s network | host loopback only | The bridge’s gateway, e.g. http://10.88.0.1:4533 (podman) or http://172.17.0.1:4533 (Docker) |
Inside a container, 127.0.0.1 is the container itself. If Navidrome only listens on
the host’s loopback, let it listen more widely and use the firewall to keep it off your
LAN. On NixOS:
services.navidrome.settings.Address = "0.0.0.0";
networking.firewall.interfaces.podman0.allowedTCPPorts = [ 4533 ]; # the bridge only
The music folder
delune moves approved albums into the folder Navidrome scans (DELUNE_LIBRARY_DIR), so
it needs write access there:
- on one machine, run delune in Navidrome’s group (the NixOS module’s
groupoption) — files are written group-writable (UMask=0002); - on a NAS share, run delune as the share’s owner (
services.delune.vpn.user = "1024:100", oruser:in Compose).
Downloads are staged in delune’s data folder first. If that’s on a different disk from the library, imports copy and then delete instead of renaming, which is slower but safe.
Without Navidrome
With no Navidrome configured, delune runs in open mode: no sign-in, and anyone who can reach it is an admin. The fetch command is disabled in open mode. Keep an open-mode delune off the internet.
Soulseek
delune has its own Soulseek client: it doesn’t need slskd or Nicotine+.
The account
Any username and password work: the Soulseek server registers a new name the first time it signs in. One name can only be signed in once. Signing in somewhere else (another delune, slskd, Nicotine+, or the setup wizard’s check) signs the other one out. If you’re replacing slskd, stop it first and give delune the same account.
Settings → Connections shows the account, whether it’s online, the address the Soulseek server sees, and whether other users can reach you.
The listening port
Other users connect to you on the listening port (DELUNE_SLSK_PORT, default 2234).
When they can, you get more search results and downloads start straight away; when
they can’t, some peers can’t send to you at all.
- At home: forward the port on your router, or turn on Forward the Soulseek port automatically (UPnP) in Settings → Sharing.
- Behind a VPN: forward it with your VPN provider, and let the VPN container accept
it (gluetun:
FIREWALL_VPN_INPUT_PORTS). See Through a VPN.
Settings → Connections says reachable once someone on the internet has connected to that port.
Sharing
Soulseek works on give and take, and many people only upload to those who share. Settings → Sharing:
- Share my library — off until you turn it on. People browse it as
Music, never the real path. - Upload slots, files per person, and speed limits, with an optional schedule (for example, slower during the evening).
- Only share with people who share, and a blocked list.
- Pass searches on to others — take part in the distributed search network.
The share list is rebuilt after imports and every six hours.
Searching well
- delune retries a search with fewer words when the network returns nothing.
- Pasting a link from Spotify, Apple Music, Tidal, Qobuz, Deezer, YouTube Music, SoundCloud, Bandcamp or MusicBrainz resolves the release first, then ranks folders by how much of its tracklist they hold.
- The Soulseek server limits how often you can search; wishlist searches take one turn per interval the server sets.
Through a VPN
Soulseek shows the people you trade with your IP address. To keep delune’s traffic on a VPN, run delune inside a network that only has the VPN: if the VPN drops, delune loses the network instead of falling back to your home connection.
There are three ways, from most common to least.
1. Inside a VPN container (gluetun)
gluetun runs a VPN client in a container, and other containers can share its network. delune then:
- sends and receives everything through the VPN;
- is reached through ports that gluetun publishes;
- sees
127.0.0.1as gluetun, not your host (so point it at Navidrome by another address).
NixOS
virtualisation.oci-containers.containers.gluetun.ports = [
"127.0.0.1:7474:7474" # delune's web app, for your reverse proxy
];
services.delune = {
enable = true;
listen = "0.0.0.0:7474"; # inside gluetun's network
libraryDir = "/mnt/nas/media/music";
soulseek.port = 50300; # the port your VPN forwards
navidrome = { url = "http://10.88.0.1:4533"; username = "delune"; };
environmentFile = config.sops.templates."delune.env".path;
vpn = {
container = "gluetun";
user = "1024:100"; # owner of the NAS share
};
};
The module builds delune into a container image from the package, runs it with
--network=container:gluetun, mounts the data and music folders (the music folder with
rslave, so an on-demand NAS mount shows up), and waits for the music folder’s mount
before starting.
Docker Compose
services:
gluetun:
image: qmcgaw/gluetun
cap_add: [NET_ADMIN]
devices: [/dev/net/tun]
env_file: gluetun.env # provider, keys, FIREWALL_VPN_INPUT_PORTS=2234
ports:
- "7474:7474"
- "2234:2234"
delune:
build: .
network_mode: service:gluetun
env_file: .env
environment:
DELUNE_DATA_DIR: /data
DELUNE_LIBRARY_DIR: /music
volumes:
- delune-data:/data
- /path/to/music:/music
The listening port
Other Soulseek users can only reach you if the VPN forwards a port to gluetun, and gluetun lets it in:
- Forward a port with your VPN provider (or, on your own VPS running WireGuard, DNAT
it to the tunnel address:
iptables -t nat -A PREROUTING -p tcp --dport 50300 -j DNAT --to 10.100.0.2:50300). - Tell gluetun:
FIREWALL_VPN_INPUT_PORTS=50300. - Set delune’s
soulseek.port/DELUNE_SLSK_PORTto the same number.
Test from outside the VPN. A check run from inside gluetun leaves through the VPN and comes back to itself, so it can say closed when the port is open. Test from another network, or trust delune’s own reachable indicator.
2. Inside a network namespace
If you run WireGuard in its own network namespace (namespaced-wireguard, vopono, or your
own ip netns setup), point the module at it:
services.delune.vpn.namespace = "wg"; # /run/netns/wg
delune runs as the normal hardened service, with NetworkNamespacePath set, so it only
has the namespace’s interfaces.
3. Your own setup
Anything that puts delune’s process in a network where the only route out is the VPN
works: a systemd unit with NetworkNamespacePath=, a container with its network
attached to a VPN container, and so on.
Check it
Settings → Connections shows the address the Soulseek server sees. It should be your VPN’s address, not your home’s.
Reverse proxy and HTTPS
delune serves plain HTTP. For anything beyond your LAN, put a reverse proxy with HTTPS
in front of it, and keep delune itself on 127.0.0.1 (the NixOS module’s default).
Two things matter:
- Live updates use Server-Sent Events (
/api/v1/events, and search results stream the same way). The proxy must not buffer those responses. - Tell delune the page came over HTTPS with
X-Forwarded-Proto: https, so its sign-in cookie is markedSecure. Most proxies send it already.
Caddy
delune.example.com {
reverse_proxy 127.0.0.1:7474
}
Caddy streams responses and sets the forwarding headers by default.
Traefik
services.traefik.dynamicConfigOptions.http = {
routers.delune = {
rule = "Host(`delune.example.com`)";
service = "delune";
tls.certResolver = "letsencrypt";
};
services.delune.loadBalancer.servers = [ { url = "http://127.0.0.1:7474"; } ];
};
Traefik doesn’t buffer by default.
nginx
server {
server_name delune.example.com;
listen 443 ssl http2;
location / {
proxy_pass http://127.0.0.1:7474;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Live updates and streamed search results
proxy_buffering off;
proxy_read_timeout 1h;
}
}
Uploads
Add music on the Downloads page uploads whole albums, up to 20 GB at a time. The browser sends them in pieces of at most 64 MB, each sized to take about ten seconds, and a piece that fails is sent again from where it stopped. That keeps uploads working behind Cloudflare (which refuses requests over 100 MB) and proxies that time out slow requests.
nginx refuses anything over 1 MB by default, so raise its limit to fit a piece:
client_max_body_size 64m;
proxy_request_buffering off;
Caddy and Traefik don’t limit request sizes unless told to.
A sub-path
delune expects to be at the root of its host (https://delune.example.com/). Give it
its own (sub)domain rather than a path like /delune.
Remote access
Tailscale
The simplest private way in. On the delune machine:
tailscale serve --bg --https=8446 http://127.0.0.1:7474
Then https://<machine>.<tailnet>.ts.net:8446 works from your other devices, with a
real certificate — and the installed phone app works over it.
Pick a port nothing else uses.
tailscale servereally listens on the tailnet address. If another service (a reverse proxy on:443, an identity provider on:8443) binds all addresses on the same port, whichever starts first after a reboot wins, and the other fails. Checkss -tlnand your configuration first.
A public name
Behind a reverse proxy, delune is fine on the internet as long as Navidrome sign-in is on: every route except health, sign-in and the web app itself needs a session. Failed sign-ins are limited (five, then a minute’s wait). Don’t expose a delune running in open mode.
The terminal client
delune-tui connects over whatever you’ve set up:
delune-tui https://delune.example.com
delune-tui https://machine.tailnet.ts.net:8446
delune-tui myserver # finds delune on that host
Searching and pasting links
The search box takes words (artist and album works best) or a link.
Links
Paste a link from Spotify, Apple Music, Tidal, Qobuz, Deezer, YouTube Music, SoundCloud, Bandcamp or MusicBrainz. delune works out the release from the service’s public data, matches it across services by barcode (UPC) and ISRC through MusicBrainz, then searches Soulseek for it. Results are ranked by how much of that release’s tracklist each folder has.
On your phone, Share → delune from those apps does the same.
A playlist link (Spotify, Deezer) offers to put its songs, or their albums, on the wishlist.
Results
Results stream in as peers answer (about 20 seconds). Each is one folder from one person, ranked:
- lossless before lossy, hi-res first;
- complete before partial;
- then by quality, a free upload slot, and speed.
Badges show In library, N missing, and the quality. Filters narrow by quality and “ready now” (a free slot). People you’ve hidden don’t show up.
If nothing comes back, delune tries again with fewer words, and offers Keep looking for it (the wishlist).
Artists, albums and songs
When the search names an artist (“gorillaz”, or “radiohead creep”), their picture appears at the top. Tap it for their page: albums from Deezer, and which of them you already have. All · Artists · Albums · Tracks switch what’s shown:
- All: the artist, their albums on Deezer (tap one for its tracklist), a few matching songs, then album folders on Soulseek.
- Artists: everyone the search could mean.
- Albums: album folders on Soulseek, as above.
- Tracks: single songs whose title is in the search, one row per person sharing them, with the album’s cover. Opening one downloads just that song. Name the artist and the song (“fred again jungle”) for the best matches.
Scene release names (Hum-Youd_Prefer_An_Astronaut-1995-FLAC) are read like any
other, so they get their cover and proper album name.
Searches Soulseek won’t run
The Soulseek server tells every client to ignore searches containing some phrases, usually at a rights holder’s request, so nobody answers them. delune says so instead of reporting “nothing found”. The artist’s page still shows what they’ve released.
A release
Opening a result shows its tracklist, quality per file, how downloads from that person have gone before, and whether the album is already in your library. Choose Download (the whole folder, or tick tracks), or Request if your account asks for albums instead.
The album and artist names open album and artist pages.
Downloads and review
Downloads
Every file in a folder is queued with that person at once. The Downloads page shows progress, your place in their queue, and lets you:
- Stop (keeping what’s arrived) and Resume;
- Start next, when only a few downloads may run at once (Settings → Sharing → downloads at once);
- Try someone else, when a download failed: delune searches again and takes the best copy from anyone this album hasn’t already failed with;
- Find another copy, to search for it yourself;
- Remove, which deletes what was downloaded.
Downloads survive restarts: delune picks up where it left off.
Review
When a download finishes, delune checks it before anything touches your library:
- every file is decoded end to end;
- lossless files are checked for a brick-wall cutoff that gives away a transcode (an MP3 dressed up as FLAC);
- tags are read and the destination path is planned with your naming template;
- conflicts with files already in the library are found.
The Review page shows all of that per track, with the planned paths. Then:
- Import moves the files into the library, embeds artwork, fetches lyrics, and asks Navidrome to rescan. The album shows up in Navidrome a moment later.
- Discard deletes the download.
Who reviews what
| Admins (and people who manage delune) | A clean download — nothing blocked, no conflicts, no suspected transcodes or unreadable files — imports itself, with a notification. Anything flagged waits in Review. |
| Members | Their downloads wait in Review for them to import. |
| Members, with approval on | They can’t import; an admin approves from Review. People can be allowed to skip approval. |
The Review tab shows how many albums are waiting.
Adding music you have
Add music (the button at the top of Downloads) takes tracks, a whole folder (choose it, or drag it in) or a zip. Only music and pictures are kept. The upload becomes a download like any other: its files are played through and checked, named from their tags (the album and artist fields are only needed for untagged files), joined to an album you already have part of, and wait here for review, or go straight in for people who may import their own downloads. Keep this album complete also follows the album, so its missing tracks are looked for.
Listening first
Every track in Review has a play button: the file streams from the server, so it starts at once and you can skip around. The length and frequency figure beside a track opens its spectrogram, the track’s frequencies over time. A file converted from MP3 shows a flat ceiling (often around 16 or 19 kHz) well below the top, and the ceiling delune detected is marked.
Filling gaps in an album you have
When the library already has part of an album, the release view picks only the missing tracks. On import they go into the album’s existing folder and get its album, artist and year tags, so Navidrome shows one album rather than two. Albums that change after release (tracks added, order changed, like Fred again..’s USB) are numbered by their current tracklist: new tracks get their place in it, and tracks already in the library are renamed and renumbered to match. The review lists what moves. On albums with more than one disc, numbers are left as they are.
Artwork and lyrics
On import (Settings → Library and imports):
- Artwork: the folder’s cover image goes alongside, and is embedded in each file.
- Lyrics from LRCLIB: a
.lrcfile beside each track, in the tags, both, or off. Synced lyrics are used when they exist.
History
The History page (in the profile menu) lists what you’ve downloaded: everything, what was added to the library, requests, and what didn’t work out. Admins can switch to someone else’s history, or everyone’s.
Naming and the library
The naming template
Settings → Library and imports → Naming decides where imported files go, relative to the music folder. The default:
{album_artist}/[{year} - ]{album}/{track} - {title}
{token}is replaced with a value; see Naming tokens.[ … ]is an optional section: it disappears when a token inside it is empty. Above, albums without a year becomeArtist/Album/….[[and]]are literal brackets.
Match my library looks at the files already in the music folder and suggests the template they follow. The preview shows real examples as you type.
Options: track number padding, how multi-disc albums are laid out, what replaces characters filesystems don’t allow, whitespace handling, and a limit on the length of each part of the path.
These settings are for admins; they shape everyone’s library.
Home and stats
The home screen, under the search box, shows what’s downloading, what’s waiting for review, the albums added to Navidrome lately, and the library at a glance. All stats opens the full picture: albums, artists, songs, listening time and size; how much is hi-res, CD quality or lossy; the artists with the most albums; releases by decade; genres; what came in through delune each month and who added it; and (for people who manage delune) the Soulseek users you’ve downloaded most from. The counts come from Navidrome and are kept for an hour; Count again refreshes them.
Album and artist pages
Album, artist and song names are links throughout the app.
- Album: cover, tracklist and running time, whether it’s in your library, Find on Soulseek, Keep looking for it, the Bandcamp price, and following the artist.
- Artist: what your library has by them, their albums and singles (from Deezer), their SoundCloud, and Follow artist.
- Song: where it’s from, and its lyrics, verse by verse, with Copy.
Each has its own address (/album/<artist>/<title>, /artist/<name>,
/song/<artist>/<title>), so they can be shared and opened directly.
Checking the library
Settings → Library and imports → Library check (for people who manage delune) looks through the music folder for three things:
- An album in several folders. This happens when the artist’s folder is spelled in
different ways (
Fred again..,Fred again._) or the folder names carry different years. Merge moves the tracks into the fullest folder, keeping the better copy where both folders have a track. It then makes the album one album, as below. - An album that shows up more than once. Players group tracks by their tags, not their folder. Tracks from different sources often disagree on the album artist (or list it twice), the release date or the MusicBrainz release, and each variant shows as its own album. Make it one album gives every track the album title, album artist, dates (original dates too, in the same order) most of them already have, and drops release ids the tracks don’t share. The folder is marked changed so Navidrome rescans it. Tracks tagged for a different album move to that album’s folder. The album gets a cover from Deezer if it has none.
- A track twice in one folder, such as an MP3 beside the FLAC. Keep the best keeps the highest-quality copy of each track.
Tracks imported into an album you already have get the same treatment automatically.
These fixes never delete anything:
- Whatever a fix takes out goes to
.delune-trashin the music folder, which Navidrome doesn’t show. Anything in the trash is emptied after 30 days. - Recent changes lists each fix and upgrade: what it was, and every file it moved, retagged or put away. Undo reverses all of it, tags included.
- Before each fix, delune checks that the files are still exactly as they were when you looked. If anything changed, it asks you to check again.
- A fix that touches more than 20 files needs a second tap. One that would touch more than 400 files is refused.
Ignore hides a finding for good: the same folders won’t come up again, even as their files change. Show again, below the list, brings every ignored finding back.
New tracks go into an album’s existing folder even when that folder is spelled differently, so new splits shouldn’t appear.
Wishlist, following and automation
Wishlist
The wishlist keeps searching for things that aren’t on Soulseek yet, or not in a good enough copy. Open it from the search page.
- Search for an artist or album inside the sheet and add it, or Keep looking for it after a search that found nothing.
- Each item has a minimum quality (any, lossless, hi-res) and can download automatically when a good copy turns up, or just tell you.
- Items are searched one per interval the Soulseek server allows (usually about twelve minutes), oldest first.
- A download that fails or is removed puts its item back in line.
- Pause or remove items any time. Up to 500 items can be waiting per server.
Following artists
Follow artist (on their page, an album, or in the wishlist) puts their new albums and EPs on your wishlist as they’re released. delune checks each followed artist once a day, using Deezer’s catalogue. Following is per person, and artists you follow are listed under Artists you follow in the wishlist.
An album view has two buttons, and they do different things:
| Button | Watches | Puts on the wishlist |
|---|---|---|
| Follow artist | the artist | their next albums and EPs |
| Keep album complete | this album only | its missing tracks, and any added later |
SoundCloud artists can be followed too; see SoundCloud.
New releases
New releases (from the home screen, or /radar) lists what the artists you follow
released in the last four months, and what’s announced, with whether each is already
in your library, on your wishlist or downloading. Wish for it adds one; upcoming
releases can be wished for before they’re out. You’re also notified when a followed
artist’s new album or EP goes on your wishlist.
Keeping albums complete
Keep album complete (in a release or album view) watches that one album. Once a day delune compares the album’s current tracklist with your library and puts each missing track on the wishlist, once. That covers tracks you skipped, and tracks the artist adds after release, as with albums that keep growing. Each is found in a copy of the album and imported into the album you already have, numbered in the album’s current order (see Filling gaps). An album you don’t have yet goes on the wishlist whole.
Followed albums are listed under Albums kept complete in the wishlist.
Quality upgrades
Settings → Automation → Quality upgrades (off by default) slowly walks through the library, a few dozen albums an hour, and puts every lossy album on the wishlist with the quality you choose. Upgrades download like anything else and still go through review.
When an import brings a better copy of a track the album already has, the new copy
takes its place and the old one (with its lyrics file) moves to .delune-trash in your
library folder. Navidrome doesn’t show that folder, and delune empties anything in it
after 30 days. A copy that isn’t better than what you have is left out, and Review says
so.
Downloading what it finds
Settings → Automation → Download what it finds (on by default) lets items that following and upgrades add start downloading as soon as a match turns up; otherwise they wait on the wishlist for someone to download. Everything still stops at review, and admins’ clean downloads import themselves (see Review).
Notifications
delune’s bell (top of the app) lists what happened: downloads ready for review or failed, albums added to the library, requests decided, new requests (for people who manage delune), and new releases from artists you follow. Settings → Notifications sends them on, so you hear about them when delune isn’t open. Each person sets their own.
On your phone or browser
Turn on Notifications on this device and allow them when the browser asks. It works in Chrome, Edge and Firefox on desktop and Android, and in Safari on macOS. On an iPhone or iPad, add delune to the home screen first (Share → Add to Home Screen) and turn it on from the app opened there; Apple only allows it for installed apps.
Every device you turn on is listed, and can be removed from any other. Push needs delune to be reached over HTTPS (a reverse proxy or Tailscale), as browsers require.
ntfy
Paste an ntfy topic address, like https://ntfy.sh/some-long-name,
and subscribe to the same topic in the ntfy app. Anyone who knows a topic on ntfy.sh can
read it, so pick a name nobody would guess, or run your own ntfy server. People who
manage delune may also use a plain http:// address, for an ntfy on the local network.
Discord
In a Discord channel’s settings, Integrations → Webhooks → New Webhook → Copy Webhook URL, and paste it here. Messages arrive as the webhook, with a link back to delune.
Choosing what’s sent
What to send mutes kinds of notification everywhere except the bell. Send a test tries every channel you’ve set up and says how each went.
Links in ntfy and Discord messages point at the address you last saved these settings from.
People, permissions and requests
Accounts
Everyone signs in with their Navidrome account; delune keeps no passwords of its own. Navidrome admins are delune admins, and that follows Navidrome within seconds. Sessions last 30 days from last use. Settings → Your account lists your devices, and can sign the others out.
Permissions
Settings → People (admins) lists everyone who has signed in. Each person has:
| Permission | Allows |
|---|---|
| Search | Searching Soulseek, opening releases, browsing people |
| Download | Starting downloads, the wishlist, following artists |
| Request | Asking for albums for someone to approve |
| Skip approval | Importing their own downloads when imports need approval |
| Manage | Everyone’s downloads and requests, people, and all settings |
New people can search, download and request. Imports need approval (a switch on the same page) makes members’ downloads wait for an admin.
Members don’t see the Soulseek tab (the account, chat, rooms and uploads) or the Library and imports settings.
Requests
People who can’t download (or would rather ask) press Request on a release, or ask for an album by name. Admins, and anyone who can manage, see requests on the Downloads page and approve or decline them. An approved request downloads, and the requester follows it through: pending, approved, downloading, ready for review, in the library, or declined — with a notification at each step that matters.
Notifications
The bell collects what you need to know: a request decided, a download ready or failed, an album added to the library. For admins: new requests.
Soulseek: people, favourites, chat, sharing
The Soulseek tab (admins) is everything about the account beyond search.
People
Look someone up by name to see their profile, picture, speed, queue, and everything they share, as a folder tree you can filter. Any folder opens like a search result, ready to download.
Favourites
Press the star on someone’s profile to make them a favourite. delune then keeps their share list saved, so their shares open instantly next time instead of waiting on their connection, and quietly refreshes it: when you open them (if it’s older than ten minutes) and every six hours. Favourites are listed on the People tab, per person.
Messages and rooms
Private messages with other users, and public chat rooms: join, read and talk. delune rejoins your rooms when it restarts.
Uploads
What you give back, for the last 7 or 30 days or all time:
- your seed ratio (uploaded ÷ downloaded), with both totals and a day-by-day chart;
- what’s going out right now, and who’s waiting, with the option to cancel or block;
- who downloads from you and how much, and the albums people want most;
- recent uploads, including ones that failed and why.
delune starts keeping this history when you upgrade to a version that has it; the totals go back further. Only people who manage delune can see it. Sharing itself is set up in Settings → Sharing.
Stats
The top of the People tab shows what you share, uploads and downloads running, and the totals transferred, kept across restarts.
Hiding people
Hide results from … on a release keeps someone out of your search results (in this browser), for example after a fake or a failed download.
Bandcamp
Buying
Album pages show Buy on Bandcamp with the digital price (and “or more” when the artist lets you pay what you like). It opens the album on Bandcamp: payment always happens there, never through delune.
Linking your account
Each person can link their own Bandcamp account (Settings → Bandcamp). Then:
- your purchases are listed (the Purchases button on the search page), refreshed when you link, when you ask, and every twelve hours;
- albums you’ve bought say Bought on Bandcamp instead of showing a price;
- Get the files downloads a purchase in the format you choose (FLAC, ALAC, MP3 320, MP3 V0), unpacks it, and sends it to Review like any other download.
Bandcamp has no public API for this, so delune uses your browser’s sign-in cookie,
called identity:
- Sign in at bandcamp.com on a computer.
- Open the developer tools (F12) → Application (Chrome) or Storage
(Firefox) → Cookies →
https://bandcamp.com. - Copy the value of
identityand paste it into Settings → Bandcamp.
A whole Cookie: header, or a cookies.txt export, works too. delune checks it with
Bandcamp before keeping it.
The cookie is a login: delune stores it only in its database (readable only by delune), never logs it, never sends it back to the browser, and only ever sends it to Bandcamp. Unlink forgets it. If Bandcamp signs delune out, Settings says so and asks you to link again.
SoundCloud
SoundCloud no longer hands out API keys, so delune only reads what anyone can: an artist’s public page, the RSS feed SoundCloud publishes for every account, and a track’s page. It never downloads audio from SoundCloud itself.
On artist pages
If delune finds the artist’s own SoundCloud (an account with the same name that’s verified or well followed), their page shows it, with their newest tracks. Open a track for:
- Soulseek — search for it;
- Free download — when the artist gives it away, either their own download link or SoundCloud’s download button;
- Listen — on SoundCloud.
When an admin has set up a fetch command, a track with SoundCloud’s own download button can be fetched straight into Review.
Following
Follow new tracks puts each track the artist posts from now on onto your wishlist (as a single song), checked every few hours. SoundCloud follows are listed in the wishlist, per person.
Other sources (fetch command)
delune doesn’t download from streaming services: that means getting around copy protection and their terms. Instead, an admin can give delune a downloader they trust, and delune runs it for a link. Whatever it fetches goes through the same review as everything else.
Settings → Other sources:
- Program — for example
yt-dlp, found on delune’sPATH, or a full path. - Arguments — one per line.
{url}is replaced with the link and{output}with the folder to write into; both are required.
-x
--audio-format
flac
-o
{output}/%(title)s.%(ext)s
{url}
The program is started directly, not through a shell, and only {url} and {output}
are substituted, so a link can’t become a command. It runs as the delune service, with
an hour’s limit, and can be stopped from Downloads.
Once it’s set up, pasting a link delune can’t search for offers Fetch with ….
The fetch command needs sign-in: it’s switched off when delune runs in open mode.
On your phone
The web app is built for phones first.
Install it
- iPhone and iPad: open delune in Safari → Share → Add to Home Screen.
- Android: open it in Chrome → ⋮ → Install app.
It opens full screen, with shortcuts (long-press the icon) for Search, Wishlist and Review. On Android it shows up in the share sheet: share a Spotify, Apple Music or Bandcamp link to delune to search for it.
HTTPS is needed to install it — use your reverse proxy or Tailscale.
Around the app
- The bottom bar has Search, Downloads, Review, Soulseek (admins) and Settings. It steps aside while you type.
- Releases, albums and wishlists open as sheets from the bottom.
- The Review tab shows how many albums are waiting.
- Live updates keep every screen current without refreshing.
Terminal client
delune on its own opens the terminal client (so does delune tui, and the standalone
delune-tui), for any machine that can reach the server. It does
the whole flow: search, download, watch progress, review, and import. It marks what you
already have: releases in your library, songs missing from a partial copy, and albums
already downloading or waiting for review. Everything updates live.
delune # the server you used last time
delune tui myserver # the same as delune-tui myserver
delune-tui # the server you used last time
delune-tui myserver # finds delune on that host
delune-tui https://delune.example.com
delune-tui --forget # forget the server and sign-in
Finding the server
Give it delune’s address, just the host name, or your Navidrome address. It tries, in order:
- exactly what you typed;
- the same host on delune’s port,
7474; - the same host at
/delune(for a reverse proxy set up that way).
An https:// address is never tried as plain http://. If the server redirects (say
from http:// to https://), the client remembers where it ended up, since a redirect
would otherwise drop the sign-in.
Signing in
If the server has sign-in on, it asks for your Navidrome username and password (or reads
DELUNE_USERNAME and DELUNE_PASSWORD). The password is never stored. The session
token and the server are kept in ~/.config/delune/tui.toml, readable only by you, so
later starts go straight in. When the session ends (it expired, or you signed out of
that device from the web app), the client asks you to sign in again.
Screens
- Search streams Soulseek releases, best quality first. Each is marked
✓ have,◐ 6/10(some of its songs are in your library),↓ 42%,… queued,● review, or↓ elsewhere(another copy is downloading). Matching artists and albums from the music catalogue appear beside the results. Opening a release shows that person’s files with the album cover: tick the tracks you want and download them, or pressifor the album’s tracklist (which can still download that copy). An artist opens with their picture and releases.
Covers and pictures are real images in terminals known to show them (kitty, WezTerm,
iTerm2, Ghostty, Konsole, foot), and coloured blocks elsewhere. Set
DELUNE_TUI_IMAGES to images to try real images in another terminal, blocks to
always use blocks, or off for no pictures.
- Downloads groups everything by where it’s got to: downloading, waiting, needs
attention, ready for review, and (press
i) imported. It shows speed and time left. - Review lists finished downloads with a verdict (clean, tracks to check, replaces files) and the full report, then the albums imported lately.
Downloading a release you already have asks first. Downloading one you have part of gets only the missing songs. People without download permission are offered to ask an admin instead.
Keys
Press ? for this list in the client.
| Where | Key | |
|---|---|---|
| Anywhere | F1 F2 F3, Alt+1 Alt+2 Alt+3 | Search, Downloads, Review |
Ctrl+C | Quit | |
| Outside the search box | 1 2 3 | Search, Downloads, Review |
/ | Search box | |
? | Keys | |
q | Quit | |
| Search box | type, Enter | Search (links work too); the results take the keys |
Ctrl+W, Ctrl+U | Delete a word, clear | |
↓ or Tab | Into the lists | |
Esc | Clear, then back to the lists (it never quits) | |
| Lists | ↑ ↓ (or k j), PgUp PgDn, g G | Move |
Tab, ← → | Between artists and albums and releases | |
| Releases | Enter or o | Open it: the person’s files and the cover |
d | Download (or ask an admin) | |
i | The album’s tracklist | |
a | Open the artist | |
| An open release | Space | Tick or untick a track |
t | Tick all, or none | |
d or Enter | Download the ticked tracks | |
i, a, s | Album tracklist, artist, search again | |
Esc | Back | |
| Album or artist | Enter | Open the selected album |
d | Download the copy the album was opened from | |
s | Search Soulseek for the album | |
a | Open the album’s artist | |
Esc | Back | |
| Downloads | s or Space | Stop or resume |
p | Download next (when waiting for a turn) | |
f | Find another copy | |
x | Remove (asks first) | |
Enter | Review a finished download | |
i | Show or hide imported albums | |
| Review | i | Import (asks first) |
x | Discard (asks first) | |
f | Find another copy | |
J K | Scroll the report | |
R | Reload the report |
Confirmations take y or Enter. Each screen shows its main keys at the bottom.
Configuration
delune reads its settings from three places. Command-line flags and environment
variables win; config.toml fills in the rest; everything else lives in delune’s
database and is changed from the web app. Connections set by flags or variables show
as fixed in Settings → Connections.
delune serve
| Flag | Environment variable | Default | |
|---|---|---|---|
--bind | DELUNE_BIND | 0.0.0.0:7474 | Address for the web app and API |
--data-dir | DELUNE_DATA_DIR | delune-data | Where delune keeps its data (what’s in it) |
--library-dir | DELUNE_LIBRARY_DIR | — | The music folder Navidrome scans; imports go here |
--naming-template | DELUNE_NAMING_TEMPLATE | {album_artist}/[{year} - ]{album}/{track} - {title} | Until naming is saved in the web app |
--slsk-username | DELUNE_SLSK_USERNAME | — | Soulseek account (with the password) |
--slsk-password | DELUNE_SLSK_PASSWORD | — | |
--slsk-port | DELUNE_SLSK_PORT | 2234 | Port other Soulseek users connect to |
--navidrome-url | DELUNE_NAVIDROME_URL | — | Turns on sign-in, library checks and rescans (with the next two) |
--navidrome-username | DELUNE_NAVIDROME_USERNAME | — | A Navidrome admin |
--navidrome-password | DELUNE_NAVIDROME_PASSWORD | — |
| Environment variable | |
|---|---|
DELUNE_LOG | Log filter, e.g. debug or info,delune_soulseek=debug (default info) |
Prefer environment variables (or config.toml) for passwords: flags end up in shell
history and process listings.
config.toml
Written by delune setup and by Settings → Connections, in the data folder, readable
only by its owner:
library_dir = "/srv/music"
[soulseek]
username = "your-name"
password = "…"
port = 2234
[navidrome]
url = "http://127.0.0.1:4533"
username = "delune"
password = "…"
Every part is optional. Changing connections in the web app saves this file and restarts delune in place.
delune setup
| Flag | Environment variable | |
|---|---|---|
--data-dir | DELUNE_DATA_DIR | Where the configuration goes (asked for too) |
delune-tui
| Argument / flag | Environment variable | |
|---|---|---|
SERVER | DELUNE_SERVER | delune’s address, its host, or Navidrome’s address |
--username | DELUNE_USERNAME | Navidrome username |
--password | DELUNE_PASSWORD | Navidrome password (prefer the prompt) |
--forget | Forget the saved server and session |
Settings in the web app
Everything else is set in the web app and saved in the database: appearance, naming and import options, automation, sharing and speed limits, people and permissions, the fetch command, Bandcamp links, follows and favourites.
NixOS module options
All options live under services.delune.
| Option | Type | Default | |
|---|---|---|---|
enable | bool | false | Run delune |
package | package | the flake’s delune | |
listen | string | "127.0.0.1:7474" | Address for the web app and API. Use 0.0.0.0:<port> in container mode. |
dataDir | path | "/var/lib/delune" | Downloads under review, accounts and settings |
libraryDir | null or path | null | The music folder Navidrome scans |
namingTemplate | null or string | null | Import naming, until saved in the web app |
user | string | "delune" | User for the service (created if delune) |
group | string | "delune" | Group for the service; use Navidrome’s to write to its folder |
soulseek.username | null or string | null | Soulseek account |
soulseek.port | port | 2234 | Soulseek listening port |
navidrome.url | null or string | null | Navidrome’s address (set with navidrome.username) |
navidrome.username | null or string | null | A Navidrome admin |
environmentFile | null or path | null | File with DELUNE_SLSK_PASSWORD= and DELUNE_NAVIDROME_PASSWORD= (any DELUNE_* works) |
openFirewall | bool | false | Open soulseek.port |
vpn.container | null or string | null | Run delune as a container in this container’s network (e.g. "gluetun") |
vpn.namespace | null or string | null | Run delune in /run/netns/<name> |
vpn.user | string | "0:0" | uid:gid in container mode; match the music folder’s owner |
vpn.volumes | list of strings | [ ] | Extra mounts in container mode |
What it sets up
- Normal mode: a
delunesystemd service, sandboxed (read-only system, no new privileges, a system-call filter), with write access only todataDirandlibraryDir, restarting on failure. vpn.namespace: the same service withNetworkNamespacePath.vpn.container: anoci-containerscontainer built from the package (with CA certificates), started after the VPN container, mountingdataDirandlibraryDir(withrslave), waiting forlibraryDir’s mount and restarting every 30 s until it works.
Data folder
Everything delune keeps is in its data folder (DELUNE_DATA_DIR).
delune.db (+ -wal, -shm) | SQLite: accounts and sessions, downloads, reviews, wishlist, follows, requests, notifications, settings, chat, favourites and their saved share lists, Bandcamp links, peer history. Readable only by delune. |
config.toml | Connections from delune setup or the web app. Readable only by delune. |
staging/<job>/ | Downloads waiting for review. Removed after import or discard; finished jobs are forgotten after 60 days and leftover folders are cleared at startup. |
avatars/ | Profile pictures |
share-cache.json | The index of what you share, so a restart doesn’t rescan the library |
Older versions kept *.json files instead of delune.db. They’re imported the first
time the database opens and renamed to *.json.imported.
Backing up
Back up the whole folder. For a consistent copy of the database while delune runs:
sqlite3 /var/lib/delune/delune.db ".backup '/backups/delune.db'"
staging/ can be left out: it only holds downloads nobody has reviewed yet.
Naming tokens
Use these in the naming template as {token}. Wrap parts that may be empty in [ ]:
the whole bracket disappears when a token inside it has no value. {{ and }} are
literal braces.
| Token | |
|---|---|
{title} | Track title |
{artist} | Track artist(s) |
{album_artist} | Album artist |
{album} | Album title |
{edition} | Edition or version, e.g. Deluxe |
{year} | Original release year |
{track} | Track number (with the padding and multi-disc settings) |
{disc} | Disc number |
{disc_count} | Number of discs |
{genre} | Primary genre |
{composer} | Composer |
{label} | Record label |
{catalog} | Catalogue number |
{isrc} | ISRC |
{codec} | Codec, e.g. FLAC |
{quality} | Quality label, e.g. FLAC 24/96 |
{bit_depth} | Bit depth, e.g. 24 |
{sample_rate} | Sample rate in kHz, e.g. 96 |
{bitrate} | Bitrate in kbps |
{source} | Where it came from, e.g. Soulseek |
{mbid} | MusicBrainz release ID |
Examples
| Template | Result |
|---|---|
{album_artist}/[{year} - ]{album}/{track} - {title} | Radiohead/1997 - OK Computer/02 - Paranoid Android.flac |
{album_artist}/{album}[ ({year})]/{track} {title} | Radiohead/OK Computer (1997)/02 Paranoid Android.flac |
{album_artist}/{album}/{track} - {artist} - {title} | Radiohead/OK Computer/02 - Radiohead - Paranoid Android.flac |
{album}/[[{year}]] {title} | OK Computer/[1997] Paranoid Android.flac |
[ ] marks an optional part; write [[ and ]] for literal brackets. The preview in
Settings shows exactly what your template produces, including a second disc and
characters that aren’t allowed in file names.
The extension is added for you. Characters that aren’t allowed in file names are replaced (Settings → Naming → options).
HTTP API
Everything the web app and the terminal client do goes through delune’s HTTP API, so anything they can do, a script can too.
- Description: OpenAPI 3.1 at
/api/v1/openapi.json. A test makes sure every route is in it. - Base path:
/api/v1. - Errors: JSON
{"code": "…", "message": "…"}with a matching status.
Signing in
curl -s -X POST https://delune.example.com/api/v1/session \
-H 'content-type: application/json' \
-d '{"username": "you", "password": "…", "token": true}'
# → { "username": "you", …, "token": "…" }
curl -s -H "Authorization: Bearer $TOKEN" https://delune.example.com/api/v1/downloads
The web app uses the delune_session cookie instead. GET /api/v1/session answers
null when nobody is signed in. Sessions end after 30 days without use, or with
DELETE /api/v1/session.
Live updates
GET /api/v1/events is a Server-Sent Events stream. Each event’s data is the name of
what changed (downloads, progress, requests, notifications, wishlist,
follows, sharing, people, session, soulseek, chat, favourites,
bandcamp, soundcloud, automation, import-options, external, or all after a
gap); fetch that part again.
Some useful routes
GET /health | Liveness, no sign-in needed |
GET /search?q=… | Soulseek search, as a Server-Sent Events stream |
GET /classify?q=… | What a pasted link points at |
POST /downloads | Start a download |
GET /downloads/{id}/review | The review report |
POST /downloads/{id}/import | Import into the library |
GET/POST /wishlist | The wishlist |
GET /music/artist, /music/album, /music/lyrics | Artist, album and lyrics lookups |
GET /soulseek | Soulseek connection status |
The TypeScript types for every request and response are generated from the Rust types
(web/src/lib/api.generated.ts).
Upgrades and backups
Upgrading
| Installed with | Upgrade with |
|---|---|
| Install script | Run it again. It skips a version that’s already installed and restarts your user service. |
| NixOS | nix flake lock --update-input delune, then nixos-rebuild switch |
| Docker Compose | git pull && docker compose up -d --build |
| Arch / Homebrew | your package manager |
| Source | git pull, rebuild the web app and the binaries, restart |
The database upgrades itself on start. Downloads in progress pick up where they left off; a fetch (command or Bandcamp) that was running is marked failed, to be fetched again.
Backups
Back up the data folder. With restic, borg or similar,
include delune.db* and config.toml; staging/ is optional.
Your music is in Navidrome’s folder, not delune’s, and your listening history is in Navidrome’s database.
Logs
| systemd (user) | journalctl --user -u delune -f |
| systemd (system) | journalctl -u delune -f |
| NixOS container mode | podman logs -f delune |
| Docker | docker compose logs -f delune |
DELUNE_LOG=debug (or info,delune_soulseek=debug) shows more.
Health
GET /api/v1/health answers without sign-in:
curl -s http://127.0.0.1:7474/api/v1/health
# {"name":"delune","version":"0.1.0","status":"ok"}
Troubleshooting
Start with Settings → Health. It checks the Soulseek login, whether your port is
reachable, the search network, sharing, Navidrome, the library folder, free disk space,
recent failed downloads and the music catalogue, and says what to do about anything
that’s wrong. (GET /api/v1/diagnostics returns the same for scripts.)
Signing in
“Wrong username or password”, but Navidrome accepts it. delune signs you in against
Navidrome at DELUNE_NAVIDROME_URL. Check that address works from where delune runs
(see Can delune reach it?).
Too many attempts. After five failed sign-ins, delune waits a minute.
Someone’s admin rights didn’t change. They follow Navidrome within about 20 seconds; the page updates on its own.
Navidrome
Albums don’t show up after import. delune asks Navidrome to scan and retries for a few minutes. If Navidrome was down longer, its own scheduled scan picks them up. Check the delune account is an admin in Navidrome.
delune in a container can’t reach Navidrome. 127.0.0.1 inside a container is the
container. Use the bridge address, and let Navidrome listen on it.
Soulseek
Soulseek keeps going offline. One account can only be signed in once. Stop the other client (slskd, Nicotine+, another delune), or use another name. When another client takes the account, delune signs back in by itself after ten minutes.
An artist never finds anything. The Soulseek server gives every client a list of phrases (usually at a rights holder’s request) that they shouldn’t answer searches for, so nobody replies. delune tells you when a search is one of them, and doesn’t answer such searches from its own shares either. Look for the music where the artist sells it.
Search stops working after the VPN restarts. With services.delune.vpn.container,
delune restarts along with the VPN container, since it can’t use the old container’s
network. Elsewhere, restart delune after recreating its VPN container.
Few results, downloads that never start. Other users can’t reach you. Forward the listening port (or turn on UPnP), and check Settings → Connections says reachable. Behind a VPN, the VPN has to forward it too.
Results but downloads fail. Try Find another copy; peers go offline. The release view shows how downloads from each person have gone before.
Imports
“Import stopped: permission denied”. delune can’t write to the music folder. Run it as the folder’s owner or group (for a NAS share, the share’s uid and gid).
A NAS folder is empty inside a container, or delune fails to start after a reboot.
The share wasn’t mounted yet. Mount it with rslave propagation, and start delune after
the mount (the NixOS module does both).
Conflicts. A file with the same destination already exists. Change the naming template, or remove the old copy in Navidrome first.
NixOS
Unknown lockfile version or a hash mismatch building delune. Your flake makes
delune follow an older nixpkgs. Remove inputs.delune.inputs.nixpkgs.follows.
secret … cannot be found. A sops secret the configuration references isn’t in
your secrets file yet. Add it, then rebuild.
The web app
Something went wrong. Reload. If it keeps happening, open the browser console and open an issue with the message.
Nothing updates live behind a proxy. The proxy is buffering the event stream; see Reverse proxy.
On iPhone, the bottom bar floats after typing. Update delune: current versions hide the bar while the keyboard is up and re-lay out the page when it closes.
Lyrics
“No words for this one”. LRCLIB doesn’t have them, or the song’s length doesn’t match any entry within ten seconds. delune also searches by title alone when the artist isn’t known.
Still stuck
Run with DELUNE_LOG=debug and open an issue
with what you did, what happened, and the log around it (passwords are never logged).
Security
What delune protects
- Passwords are never stored for sign-in: Navidrome checks them. Session tokens are stored hashed, and sessions end after 30 days without use. Failed sign-ins are rate-limited.
- Connection passwords (Soulseek, Navidrome) live in
config.tomlor your environment/secrets, both readable only by delune. The database is too. - Bandcamp logins are stored only in the database, redacted from logs, never sent to the browser, and only ever sent to Bandcamp.
- The session cookie is
HttpOnly,SameSite, andSecurebehind HTTPS. - Imports can’t write outside the music folder, and downloaded zips can’t unpack outside their staging folder.
- The fetch command runs a program directly, without a shell, substituting only the link and the folder, and is disabled without sign-in.
What you should do
- Keep Navidrome sign-in on for anything reachable beyond your machine. Open mode (no Navidrome) makes everyone who can reach delune an admin.
- Put delune behind HTTPS, and bind it to
127.0.0.1behind your proxy. - Give delune its own Navidrome admin account, so you can change or remove it independently.
- Share your library knowingly: it makes those files downloadable by anyone on Soulseek.
- Consider a VPN: Soulseek shows peers your IP address.
Reporting a problem
See SECURITY.md. Please report vulnerabilities privately, not in public issues.
Architecture
This document explains how delune is put together and where to find things. It’s
meant to be read top to bottom once; after that, the crate-level docs
(cargo doc --open) are the detailed reference.
The shape of the system
flowchart TB
subgraph host["Navidrome host"]
direction TB
server["delune serve<br>(axum HTTP API + embedded web UI)"]
slsk["Soulseek client<br>delune-soulseek"]
lib["Library<br>delune-library"]
db[("Data folder<br>jobs · accounts · settings (JSON)")]
music[/"Music folder"/]
nd["Navidrome"]
server --> slsk
server --> lib
server --> db
lib --> music
nd --> music
server -- "startScan (Subsonic API)" --> nd
end
web["Web UI<br>(browser)"] -- HTTP + SSE --> server
tui["delune-tui<br>(any machine)"] -- HTTP + SSE --> server
slsk <-- "TCP" --> network(("Soulseek<br>network"))
server -. "metadata" .-> mb["MusicBrainz · Deezer · iTunes · LRCLIB"]
- One server, two clients. The server owns all state and all work. The web UI and the TUI are thin: they render what the API returns and send user intent back. Nothing is possible in one client that isn’t possible over the API.
- Same host as Navidrome. Navidrome can’t accept uploads, so delune needs write access to the music folder. See ADR 0001.
- Two binaries.
deluneis the server with the compiled web UI, plusdelune setup.delune-tuiis the terminal client, small enough to install anywhere; it finds the server by its address, its host, or the Navidrome address next to it.
Crates
Dependencies point downward; nothing depends on delune-server except the binary.
flowchart TB
bin[delune] --> server[delune-server]
bin -- "setup wizard" --> tui[delune-tui]
client[delune-tui binary] --> tui
server --> bandcamp[delune-bandcamp]
server --> soundcloud[delune-soundcloud]
server --> resolve[delune-resolve]
server --> library[delune-library]
server --> soulseek[delune-soulseek]
server --> navidrome[delune-navidrome]
tui --> core[delune-core]
resolve --> core
library --> core
soulseek --> core
navidrome --> core
server --> core
| Crate | Responsibility | I/O? |
|---|---|---|
delune-core | Shared vocabulary: Quality, Provider, SourcePolicy, Release, API types | None |
delune-resolve | Understand pasted links; match a release across services | HTTP (resolution) |
delune-soulseek | Speak the Soulseek protocol: server session, peers, search, transfers, sharing | TCP |
delune-navidrome | Subsonic API: auth, ownership checks, scans | HTTP |
delune-library | Naming templates, layout detection, tagging, verification, import | Filesystem |
delune-server | HTTP API, job orchestration, persistence, web UI assets | All of the above |
delune-bandcamp | Bandcamp search, release pages, a fan’s purchases and downloads | HTTP |
delune-soundcloud | SoundCloud profiles, RSS feeds and track pages (no API keys) | HTTP |
delune-tui | Terminal client, and the screens of delune setup | HTTP to the server |
Keeping pure logic in delune-core and at the bottom of each crate (for example
delune-soulseek::wire, delune-library::naming) means most behaviour is tested
without networks or disks.
The main flow
What happens between typing into the search bar and a new album appearing in Navidrome.
- Classify input:
delune_resolve::classifydecides whether the input is a link or text. Clients callGET /api/v1/classifyas the user types so both UIs show the same “Spotify album” hint. - Resolve: links become a release or track from each service’s public metadata
(
delune_resolve::Resolver: Deezer and iTunes APIs, Spotify’s embed page, oEmbed, JSON-LD, Open Graph, MusicBrainz), cached for an hour. Playlists become a list of songs to put on the wishlist. See ADR 0003. - Search:
SourcePolicy::search_order()decides which sources are asked, in what order; Soulseek is always first, enforced in one tested function.GET /api/v1/searchstreams aresolvedevent for links, then results over Server-Sent Events as peers answer. - Rank: responses are grouped into one candidate per folder and sorted by
lossless-first, completeness,
Quality::rankand availability (Candidate::rank). For links, the web client ranks folders holding the linked tracklist first. See ADR 0004. Library state (/api/v1/library/album) marks what Navidrome already has. - Download:
POST /api/v1/downloadscreates a job for a folder (or some of its files); every file is queued with the peer at once throughdelune-soulseek::transferinto<data dir>/staging/<job>/, resuming from.partfiles. Jobs survive restarts (saved indelune.db, see below) and can be stopped and resumed. - Verify and plan: every file is decoded end to end and checked for transcodes
(
delune_library::verify); tags and the naming template decide where each file goes (delune_library::import::plan), and conflicts are found before anything moves. - Review: the job waits for its requester; with approval required, for an admin. See ADR 0005.
- Import: files move into the library (copying across filesystems), then in the
background artwork is embedded, lyrics are fetched from LRCLIB, and Navidrome
rescans (
delune_server::finishing).
Beside the main flow: the wishlist repeats searches on Soulseek’s wishlist interval; automation follows artists and looks for quality upgrades; sharing indexes the library, answers searches (including from the distributed network) and serves uploads; chat keeps private messages and rooms.
Storage
Everything delune keeps lives in its data directory:
delune.db: one SQLite database (WAL mode, readable only by delune). Each part of the server saves its state as a JSON document under its own key (jobs,accounts,wishlist,requests,notifications, settings…), written in one transaction. Apeerstable records how downloads from each Soulseek user went, which breaks ties when ranking results. JSON files from before the database are imported on first start and kept as*.json.imported.config.toml: connections set from the web UI (library folder, Navidrome, Soulseek); flags andDELUNE_*variables override it.staging/<job>/: downloads waiting for review.avatars/,share-cache.json: profile pictures, and a cache of audio properties that makes re-indexing the library for sharing fast.
API
- REST under
/api/v1, JSON bodies, versioned by path. - Search and chat stream over Server-Sent Events; downloads and uploads are polled.
- Sign-in checks Navidrome credentials and issues delune’s own session: an
HttpOnlycookie for browsers, a bearer token for the TUI. See ADR 0006. Every route except health and the session needs one; permissions are checked per handler. GET /api/v1/openapi.jsondescribes every route (OpenAPI 3.1, from#[utoipa::path]annotations on the handlers); a test fails if a registered route isn’t described.web/src/lib/api.generated.tsis generated fromdelune-core::api(scripts/generate-types.sh), andapi-contract.tschecks the hand-written web types against it at compile time. CI fails if the generated file is stale.
Web UI
React 19, Vite, TanStack Query, Tailwind CSS v4 and shadcn/ui components on Base UI.
Built into web/dist and embedded into the binary with rust-embed; unknown paths
fall back to index.html for client-side routing, unknown /api/* paths return 404.
Design rules (full detail in design/web-ui.md): dense and keyboard-first (⌘K / / focus search), one accent colour
taken from album artwork, IBM Plex Sans with tabular figures, designed
empty and error states, and correct at 400px wide.
Terminal UI
ratatui with the standard single-state model: App holds state, ui::draw is a
pure function of it, and background tasks talk to the event loop over a channel
instead of touching the terminal. Three screens (Search, Downloads, Review) cover
the whole flow; destructive actions ask for confirmation. Album art via
ratatui-image is planned.
Testing
- Unit tests live next to the code (
#[cfg(test)]). Protocol and parser code is tested against byte-level fixtures and published test vectors. - Server routes are tested in-process with
tower::ServiceExt::oneshot. - The Soulseek client is tested end to end against fake servers and peers over real sockets: searching, downloading and resuming, browsing, chat, uploads, answering searches and joining the distributed network.
- Live tests (
cargo test -p delune-resolve --test live -- --ignored) check link resolution against the real services. nix flake checkboots a NixOS VM with the module and checks the service answers.- CI runs
cargo fmt --check,clippy -D warnings,cargo test, rustdoc with warnings denied, and the web typecheck, lint and build on every push.
delune — product decisions (2026-09-15)
Decided with Aidan after research (see research/README.md). Each will become a proper ADR in docs/adr/ when implementation starts.
Identity
- Name:
delune(after Clair de Lune). Binariesdelune(delune serve,delune setup) anddelune-tui, the terminal client. - Repo:
github.com/PndaMan/delune, public, AGPL-3.0.
Shape
- Runs on the same host as Navidrome. Server owns the library folder, Soulseek client, queue, and review inbox. Web UI and TUI are thin clients of the server API. Goal: replace slskd + antra with one more automated tool.
- Works with any Navidrome instance (generic self-hosting, not tied to one homelab).
- Stack: Rust core (axum, tokio, sqlx/SQLite, utoipa OpenAPI) · ratatui + ratatui-image TUI · React 19 + Vite + TanStack + shadcn/ui (Base UI) web, embedded in the binary. Shared DTCG design tokens → CSS vars + Rust theme.
Sources
- Soulseek is always searched first. Native Soulseek protocol client in Rust (no slskd dependency). Must be a well-behaved client: shares the Navidrome library read-only, configurable upload slots/speed, search rate limiting to avoid server bans.
- Streaming providers exist as opt-in only (off by default, used only when the user enables them): Qobuz, Tidal, Deezer, YouTube Music, SoundCloud, Bandcamp. Implemented behind a provider trait.
- Link resolution: own resolver (Odesli is dead) — MusicBrainz URL lookup → ISRC/UPC bridge → provider metadata.
Flow
- Search text or paste any provider link → canonical release → “already in library / upgrade available” via Navidrome → Soulseek results streamed live and ranked → optional streaming sources → default highest quality, user may pick format.
- Always review. Nothing enters the library without approval.
- Approvals: requester reviews their own download; admins can turn on a required admin approval gate.
- Fake-FLAC detection: flagged in review with spectrogram; next-best source suggested.
Automation (v1)
| Feature | Default |
|---|---|
| Playlist import (Spotify/Apple/Deezer links → missing tracks) | on |
| Watchlist (keep retrying not-found items) | on |
| Artist follow + new releases | following an artist is the opt-in (Deezer releases, SoundCloud feeds) |
| Quality upgrades (find better versions of owned albums) | off, configurable |
Accounts
- Log in with Navidrome credentials. Navidrome admins = delune admins. Friends/family multi-user.
Library
- Default layout: detect and match the existing library’s folder/file naming, with very detailed settings to change it (template tokens, live preview, multi-disc, padding, illegal chars, conflicts). Full antra settings parity (lyrics synced/embedded/sidecar, artwork, MP3/AAC bitrate, sidecars, theming).
Design
- Linear/Raycast-dense, keyboard-first, ⌘K palette; cover art sets accent color (Plexamp-style). Designed empty/loading/error states. Mobile-width correct.
Distribution
- Docker image + compose · NixOS module + flake · static binaries on GitHub Releases · AUR + Homebrew (TUI client).
install.sh(checksum-verified release download) →delune setupTUI wizard → config.toml + systemd unit.delune-tuiis its own binary (2026-09-16): a client for any machine, finding the server from its address, host or the Navidrome next to it.
Bandcamp and SoundCloud (2026-09-16)
- Bandcamp: buy links only (payment stays on Bandcamp). Each person links their own account with their
identitycookie; purchases sync and download for review. - SoundCloud: public pages and SoundCloud’s RSS feeds only. No use of SoundCloud’s private web client id, so delune never fetches audio from SoundCloud itself; free downloads open the artist’s link or go through the admin’s opt-in fetch command.
v0.1 milestone — vertical slice
Paste link / search → Soulseek (native) → rank → download → verify → review → import → targeted Navidrome scan, working in both web and TUI.
Roadmap
Plans change; this file tracks the current intent. Product decisions behind it are in DECISIONS.md and adr/.
v0.1 — the whole path, once
Goal: paste a link or search, get an album from Soulseek, review it, and see it in Navidrome, from both the web UI and the TUI.
Foundations
- Workspace, CI, docs, ADRs
- Domain model: quality ranking, providers, Soulseek-first source policy
- Single binary serving API and embedded web UI
- TUI and web UI shells connected to the server
- TUI vertical slice: search, download for review, downloads (stop, resume, remove), review and import
- Configuration file and first-run setup (Navidrome URL, music folder, Soulseek account), checked before saving
- SQLite persistence (settings, jobs, peer history), importing the JSON files from before
- Login with Navidrome credentials; sessions
- TypeScript types generated from the API types, checked against the web UI at compile time and in CI
- OpenAPI description of the routes, served at
/api/v1/openapi.jsonand checked for completeness
Soulseek
- Wire primitives, server messages, compressed search responses
- Server session: login, keepalive, reconnect with backoff
- Listening port, peer connections, firewall piercing
- Search with rate limiting; results streamed to clients (API + TUI)
- Transfers: queueing, download, resume, retry, cancel
- NAT traversal help: UPnP port forwarding (opt-in), and whether the port is actually reachable
- Share index of the music folder; answering searches; uploads with slot limits
Finding the right release
- Link parsing for 9 services
- Link resolution from each service’s public metadata (Deezer, iTunes, Spotify embed, oEmbed, JSON-LD, MusicBrainz), cached
- Cross-service matching by UPC and ISRC through MusicBrainz (exact name as a fallback), without holding up the search
- Library check against Navidrome
- Folder grouping and quality/availability ranking
- Tracklist matching against the resolved release
- Automatic retry with fewer words when the network returns nothing (noise, filler words, then for links the title alone)
Downloads
- Download jobs: one folder, every file queued at once, staged under the data directory
- Persist jobs across restarts
Getting it into the library
- Naming template engine
- Layout detection from an existing library; naming settings saved from the web UI
- Decode verification and fake-FLAC detection
- Tags (lofty), embedded artwork, synced lyrics (LRCLIB)
- Review inbox
- Admin approval gate
- Import with conflict checks, then a Navidrome scan
Shipping
- Docker image (multi-arch) and compose example
- NixOS module and flake, with a VM test
- Release binaries for Linux and macOS
- [~] AUR and Homebrew packages: PKGBUILD, formula and systemd unit written; publishing them waits for the first release
Next: web UI overhaul
- Complete visual redesign of the web UI (identity, typography, colour, motion)
- Live search results with artwork, release view, settings with naming editor
- Downloads screen with live progress; ready jobs listed in Review
- Review screen: verification results, planned paths, import and discard
Requested next (from Aidan, 2026-09-15)
Accounts and requests: “Overseerr for Soulseek and streaming”
- Sign in with Navidrome credentials; sessions; sign out (web and TUI)
- Roles and permissions: admin (from Navidrome admin), and per-person rights to search, download, skip approval and manage; an “imports need approval” switch
- Revoke sessions: your devices, sign out elsewhere; admins sign someone out everywhere
- Other sources: a downloader of your own that delune runs for a link (opt-in, admins only). delune has no built-in streaming downloads: those need getting around copy protection and terms, so they’re out (decided with Aidan, 2026-09-16).
- Requests: people ask for an album; admins or permitted users approve; request status visible to the requester (pending, approved, downloading, in library, declined)
- Profile menu at the bottom of the rail (avatar, role, connection status, sign out) replacing the moon indicator
- Per-user history and notifications (requests, reviews ready, failed downloads, imports)
Soulseek: everything slskd does, simpler (before any streaming service)
- Share the music library: index, answer searches, upload with slot and speed limits (opt-in)
- Uploads page: who’s downloading from you, queue, speeds, cancel, block, per-user limits, refuse people who share nothing
- Browse a user’s shared folders; download any folder from there
- User profiles: info, picture, shared counts, free slots, speed
- Private messages and chat rooms (for people who manage delune; rooms rejoin on reconnect)
- Wishlist: saved searches re-run on a schedule, with auto-download (for review) when a good enough match appears
- Search filters (words and -exclusions, format, quality tier, free slot) and hidden people
- Transfer controls: stop, resume, retry elsewhere, global upload and download speed limits
- Speed schedule: different upload and download limits between two times of day
- Limit albums downloading at once; the rest wait in line and can be moved to the front
- Distributed search network: joins so shared files are found, and (opt-in) relays searches to child clients
- Statistics: shared, running, transferred (kept across restarts)
Links
- Resolve pasted Spotify, Apple Music, YouTube Music, Deezer, Tidal, Qobuz, SoundCloud, Bandcamp and MusicBrainz links to a release, then search Soulseek for it
- Rank folders by how much of the linked tracklist they hold; track links download just that track
- Playlist links from Spotify and Deezer: pick songs, add them (or their albums) to the wishlist, searched straight away
Profiles and look
- Profile pictures: upload your own (rail, profile menu, people list); Soulseek users’ pictures on their pages
- Themes: System, Night, Blue hour, Midnight (OLED), Forest (green); six accents; saved to each account
Mobile
- Phone layouts for search results, filters, release view (sticky action), downloads
- Phone layouts for review (sticky import bar) and settings (section links)
- Installable web app (manifest, icons, offline shell)
Search and release view
- Release view fits the screen with no scrollbars (adaptive, multi-column tracklist)
- Results show “Downloading” / “In review” / “Imported” when that folder or album is already in progress
- “In library” and “N missing” from Navidrome, per-track marks in the release view, a warning before downloading an album you already have
- Cover overlays: greyed with a progress ring while downloading, tinted once in the library
After v0.1
- Settings screens with full naming, lyrics, artwork and transcoding options
- Cover-art accent colours; theme presets; shared design tokens for web and TUI
- Playlist import and watchlist
- Followed artists and automatic quality upgrades (off by default)
- Opt-in streaming providers: Qobuz, Tidal, Deezer, YouTube Music, SoundCloud, Bandcamp
- Album art in the TUI (kitty, sixel, iTerm2)
Ideas (2026-09-16)
Proposed after an app-wide review; not yet scheduled.
Listening and discovery
- Listen before you import: play any track from Review (and from a peer’s folder) in the browser.
- Release radar: a calendar of upcoming and new releases from followed artists.
- Complete an artist: see which albums the library is missing and want them all in one tap.
- Recommendations from what people actually play (Navidrome play counts, ListenBrainz).
- Keep playlists in sync: a Spotify, Deezer or YouTube playlist that’s re-checked for new songs.
Library care
- Library health: duplicates, lossy albums, missing artwork or lyrics, odd tags — each with a fix.
- Quality ladder: upgrade an album automatically when a better copy turns up, with a spectrogram in review.
- Tagging against MusicBrainz releases, choosing the edition, before import.
Staying in touch
- Push notifications for the installed app (Web Push), plus ntfy, Discord and email.
- An activity log: who asked for, approved and imported what.
- A diagnostics page: Soulseek, Navidrome, port reachability and VPN address, with what to do about each.
Polish
- A command palette (⌘K / long-press) and keyboard shortcuts across the app.
- Request comments, and per-person request limits.
- Backup and restore of delune’s database from Settings.
- SOCKS5 proxy support for Soulseek, for VPNs that aren’t a container or namespace.
- Several libraries (Navidrome music folders) with a choice at import.
Contributing to delune
Thanks for helping. This guide covers setup, how the code is organised, and what a good pull request looks like.
Setup
Requirements: Rust 1.90 or newer, Bun 1.3+, and optionally Chromium for UI screenshots.
(cd web && bun install && bun run build) # web UI, embedded into the binary
cargo test --workspace # all Rust tests
cargo run -- serve # server on :7474
cargo run -- tui # terminal UI
(cd web && bun run dev) # web UI with hot reload, proxies /api to :7474
Before you start
- Read docs/ARCHITECTURE.md — ten minutes that save hours.
- Check the roadmap and open issues. For anything bigger than a bug fix, open an issue first so we can agree on the approach.
- Decisions that change the architecture need an ADR in docs/adr/.
Rules that aren’t negotiable
These come from the product’s core promises. A pull request that breaks one won’t be merged, however good it is otherwise.
- Soulseek is searched first. Source ordering goes through
SourcePolicy::search_order(); never build a source list anywhere else. - Streaming providers stay opt-in. Nothing may enable one by default.
- Nothing enters the library without review.
- Be a good network citizen. Soulseek changes must keep search rate limiting, sharing and slot limits intact, and need tests.
- Both clients use the API. No feature exists only in the web UI or only in the TUI because of a shortcut around the server.
Code style
Rust
cargo fmtandcargo clippy --workspace --all-targets -- -D warningsmust pass. Clippy’spedanticgroup is on; allow a lint locally with a comment saying why.- Keep pure logic separate from I/O so it can be unit-tested (see
delune-soulseek::wireanddelune-library::naming). - Every public item gets a doc comment that says what it’s for, not just what it is.
- Errors use
thiserrorin libraries andanyhowonly in the binary and TUI. - No
unsafe. The workspace forbids it.
TypeScript
bun run typecheck,bun run lintandbun run buildmust pass.- Components from
src/components/uicome from shadcn; add new ones withbunx shadcn@latest add <name>rather than hand-writing primitives. - Use theme tokens (
bg-card,text-muted-foreground,border), never raw colours. - Every async view needs designed loading, empty and error states.
- Check your change at 400px wide and with keyboard only.
Writing in the UI
- Sentence case, plain verbs, name things the way a user would.
- Errors say what happened and what to do next.
Commits and pull requests
- Conventional commit messages:
feat(soulseek): …,fix(web): …,docs: …. - One logical change per pull request, with tests for behaviour changes.
- UI changes include before/after screenshots.
- CI must be green.
Licensing
delune is AGPL-3.0-or-later. By contributing you agree that your contribution is licensed the same way. Don’t copy code from projects with incompatible licences; in particular, antra’s source is under the Elastic License 2.0 and can’t be used.