Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. works out your system (Linux or macOS, Intel or ARM),
  2. downloads the latest release and checks it against its published SHA-256,
  3. installs delune and delune-tui (to ~/.local/bin, or /usr/local/bin as root),
  4. 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 itSign-in, library checks and rescans
Write access to Navidrome’s music folderImports move files there
A Soulseek accountSearching and downloading (a new name is registered on first sign-in)
Optionally, a forwarded portOther 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

ScreenWhat it asksWhat it checks
Foldersdelune’s data folder, and your music folderThe data folder can be created; the music folder exists and is writable
NavidromeAddress, admin username and password (empty skips it)Signs in, and confirms the account is an admin
SoulseekUsername, password, listening port (empty skips it)Signs in to the Soulseek network
NetworkThe web address to listen on, and whether to open the port with UPnP—
Keep it runningA user service, a system service, or nothing; whether to start now—
FinishA 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 with systemctl --user. To keep it running while you’re logged out: loginctl enable-linger $USER.
    • System service (as root): /etc/systemd/system/delune.service. Under sudo, it runs as the user who ran sudo, not root.
    • On NixOS it prints the module configuration instead, since /etc is managed by your configuration.

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:

  1. detects the platform: Linux or macOS, x86_64 or aarch64 (and the native build under Rosetta);
  2. finds the latest release, or the one you ask for;
  3. downloads it with retries and checks it against the release’s published SHA-256 (it refuses to install anything that doesn’t match);
  4. makes sure the programs run on this machine before replacing anything;
  5. installs delune and delune-tui by copying next to the target and renaming, so an interrupted install never leaves half a program;
  6. starts delune setup on 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
OptionEnvironment variableWhat it does
--version <tag>DELUNE_VERSIONInstall this release (v0.1.0 or 0.1.0). Default: the latest.
--bin-dir <dir>DELUNE_BIN_DIRInstall here. Default: ~/.local/bin (or $XDG_BIN_HOME), /usr/local/bin as root.
--no-setupDELUNE_NO_SETUP=1Don’t start delune setup afterwards.
--forceDELUNE_FORCE=1Reinstall even if this version is already there.
-y, --yesDELUNE_YES=1Don’t ask questions (use sudo without asking if the folder needs it).
--mirror <url>DELUNE_DOWNLOAD_BASEDownload the release files from here instead of GitHub.
--uninstallRemove the programs and your user service. Your data is kept.
--dry-runShow what would happen without changing anything.
-h, --helpShow 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 (add extra_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/delune and /usr/bin/delune-tui
  • a systemd service running as the delune user
  • /etc/delune/delune.env for 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 atUse
on the same host127.0.0.1:4533http://127.0.0.1:4533
in a container, Navidrome on the hosthost loopback onlyNot reachable. Make Navidrome listen on the container bridge too (see below).
in the same Compose projecta service called navidromehttp://navidrome:4533
inside a VPN container’s networkhost loopback onlyThe 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 group option) — files are written group-writable (UMask=0002);
  • on a NAS share, run delune as the share’s owner (services.delune.vpn.user = "1024:100", or user: 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.1 as 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:

  1. 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).
  2. Tell gluetun: FIREWALL_VPN_INPUT_PORTS=50300.
  3. Set delune’s soulseek.port / DELUNE_SLSK_PORT to 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 marked Secure. 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 serve really 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. Check ss -tln and 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.

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:

  1. lossless before lossy, hi-res first;
  2. complete before partial;
  3. 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.
MembersTheir downloads wait in Review for them to import.
Members, with approval onThey 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 .lrc file 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 become Artist/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-trash in 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:

ButtonWatchesPuts on the wishlist
Follow artistthe artisttheir next albums and EPs
Keep album completethis album onlyits 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:

PermissionAllows
SearchSearching Soulseek, opening releases, browsing people
DownloadStarting downloads, the wishlist, following artists
RequestAsking for albums for someone to approve
Skip approvalImporting their own downloads when imports need approval
ManageEveryone’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:

  1. Sign in at bandcamp.com on a computer.
  2. Open the developer tools (F12) → Application (Chrome) or Storage (Firefox) → Cookies → https://bandcamp.com.
  3. Copy the value of identity and 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’s PATH, 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:

  1. exactly what you typed;
  2. the same host on delune’s port, 7474;
  3. 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 press i for 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.

WhereKey
AnywhereF1 F2 F3, Alt+1 Alt+2 Alt+3Search, Downloads, Review
Ctrl+CQuit
Outside the search box1 2 3Search, Downloads, Review
/Search box
?Keys
qQuit
Search boxtype, EnterSearch (links work too); the results take the keys
Ctrl+W, Ctrl+UDelete a word, clear
↓ or TabInto the lists
EscClear, then back to the lists (it never quits)
Lists↑ ↓ (or k j), PgUp PgDn, g GMove
Tab, ← →Between artists and albums and releases
ReleasesEnter or oOpen it: the person’s files and the cover
dDownload (or ask an admin)
iThe album’s tracklist
aOpen the artist
An open releaseSpaceTick or untick a track
tTick all, or none
d or EnterDownload the ticked tracks
i, a, sAlbum tracklist, artist, search again
EscBack
Album or artistEnterOpen the selected album
dDownload the copy the album was opened from
sSearch Soulseek for the album
aOpen the album’s artist
EscBack
Downloadss or SpaceStop or resume
pDownload next (when waiting for a turn)
fFind another copy
xRemove (asks first)
EnterReview a finished download
iShow or hide imported albums
ReviewiImport (asks first)
xDiscard (asks first)
fFind another copy
J KScroll the report
RReload 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

FlagEnvironment variableDefault
--bindDELUNE_BIND0.0.0.0:7474Address for the web app and API
--data-dirDELUNE_DATA_DIRdelune-dataWhere delune keeps its data (what’s in it)
--library-dirDELUNE_LIBRARY_DIR—The music folder Navidrome scans; imports go here
--naming-templateDELUNE_NAMING_TEMPLATE{album_artist}/[{year} - ]{album}/{track} - {title}Until naming is saved in the web app
--slsk-usernameDELUNE_SLSK_USERNAME—Soulseek account (with the password)
--slsk-passwordDELUNE_SLSK_PASSWORD—
--slsk-portDELUNE_SLSK_PORT2234Port other Soulseek users connect to
--navidrome-urlDELUNE_NAVIDROME_URL—Turns on sign-in, library checks and rescans (with the next two)
--navidrome-usernameDELUNE_NAVIDROME_USERNAME—A Navidrome admin
--navidrome-passwordDELUNE_NAVIDROME_PASSWORD—
Environment variable
DELUNE_LOGLog 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

FlagEnvironment variable
--data-dirDELUNE_DATA_DIRWhere the configuration goes (asked for too)

delune-tui

Argument / flagEnvironment variable
SERVERDELUNE_SERVERdelune’s address, its host, or Navidrome’s address
--usernameDELUNE_USERNAMENavidrome username
--passwordDELUNE_PASSWORDNavidrome password (prefer the prompt)
--forgetForget 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.

OptionTypeDefault
enableboolfalseRun delune
packagepackagethe flake’s delune
listenstring"127.0.0.1:7474"Address for the web app and API. Use 0.0.0.0:<port> in container mode.
dataDirpath"/var/lib/delune"Downloads under review, accounts and settings
libraryDirnull or pathnullThe music folder Navidrome scans
namingTemplatenull or stringnullImport naming, until saved in the web app
userstring"delune"User for the service (created if delune)
groupstring"delune"Group for the service; use Navidrome’s to write to its folder
soulseek.usernamenull or stringnullSoulseek account
soulseek.portport2234Soulseek listening port
navidrome.urlnull or stringnullNavidrome’s address (set with navidrome.username)
navidrome.usernamenull or stringnullA Navidrome admin
environmentFilenull or pathnullFile with DELUNE_SLSK_PASSWORD= and DELUNE_NAVIDROME_PASSWORD= (any DELUNE_* works)
openFirewallboolfalseOpen soulseek.port
vpn.containernull or stringnullRun delune as a container in this container’s network (e.g. "gluetun")
vpn.namespacenull or stringnullRun delune in /run/netns/<name>
vpn.userstring"0:0"uid:gid in container mode; match the music folder’s owner
vpn.volumeslist of strings[ ]Extra mounts in container mode

What it sets up

  • Normal mode: a delune systemd service, sandboxed (read-only system, no new privileges, a system-call filter), with write access only to dataDir and libraryDir, restarting on failure.
  • vpn.namespace: the same service with NetworkNamespacePath.
  • vpn.container: an oci-containers container built from the package (with CA certificates), started after the VPN container, mounting dataDir and libraryDir (with rslave), waiting for libraryDir’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.tomlConnections 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.jsonThe 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

TemplateResult
{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 /healthLiveness, 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 /downloadsStart a download
GET /downloads/{id}/reviewThe review report
POST /downloads/{id}/importImport into the library
GET/POST /wishlistThe wishlist
GET /music/artist, /music/album, /music/lyricsArtist, album and lyrics lookups
GET /soulseekSoulseek 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 withUpgrade with
Install scriptRun it again. It skips a version that’s already installed and restarts your user service.
NixOSnix flake lock --update-input delune, then nixos-rebuild switch
Docker Composegit pull && docker compose up -d --build
Arch / Homebrewyour package manager
Sourcegit 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 modepodman logs -f delune
Dockerdocker 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.

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.toml or 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, and Secure behind 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.1 behind 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. delune is the server with the compiled web UI, plus delune setup. delune-tui is 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
CrateResponsibilityI/O?
delune-coreShared vocabulary: Quality, Provider, SourcePolicy, Release, API typesNone
delune-resolveUnderstand pasted links; match a release across servicesHTTP (resolution)
delune-soulseekSpeak the Soulseek protocol: server session, peers, search, transfers, sharingTCP
delune-navidromeSubsonic API: auth, ownership checks, scansHTTP
delune-libraryNaming templates, layout detection, tagging, verification, importFilesystem
delune-serverHTTP API, job orchestration, persistence, web UI assetsAll of the above
delune-bandcampBandcamp search, release pages, a fan’s purchases and downloadsHTTP
delune-soundcloudSoundCloud profiles, RSS feeds and track pages (no API keys)HTTP
delune-tuiTerminal client, and the screens of delune setupHTTP 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.

  1. Classify input: delune_resolve::classify decides whether the input is a link or text. Clients call GET /api/v1/classify as the user types so both UIs show the same “Spotify album” hint.
  2. 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.
  3. Search: SourcePolicy::search_order() decides which sources are asked, in what order; Soulseek is always first, enforced in one tested function. GET /api/v1/search streams a resolved event for links, then results over Server-Sent Events as peers answer.
  4. Rank: responses are grouped into one candidate per folder and sorted by lossless-first, completeness, Quality::rank and 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.
  5. Download: POST /api/v1/downloads creates a job for a folder (or some of its files); every file is queued with the peer at once through delune-soulseek::transfer into <data dir>/staging/<job>/, resuming from .part files. Jobs survive restarts (saved in delune.db, see below) and can be stopped and resumed.
  6. 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.
  7. Review: the job waits for its requester; with approval required, for an admin. See ADR 0005.
  8. 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. A peers table 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 and DELUNE_* 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 HttpOnly cookie 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.json describes 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.ts is generated from delune-core::api (scripts/generate-types.sh), and api-contract.ts checks 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 check boots 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). Binaries delune (delune serve, delune setup) and delune-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)

FeatureDefault
Playlist import (Spotify/Apple/Deezer links → missing tracks)on
Watchlist (keep retrying not-found items)on
Artist follow + new releasesfollowing 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 setup TUI wizard → config.toml + systemd unit.
  • delune-tui is 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 identity cookie; 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.json and 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.

  1. Soulseek is searched first. Source ordering goes through SourcePolicy::search_order(); never build a source list anywhere else.
  2. Streaming providers stay opt-in. Nothing may enable one by default.
  3. Nothing enters the library without review.
  4. Be a good network citizen. Soulseek changes must keep search rate limiting, sharing and slot limits intact, and need tests.
  5. 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 fmt and cargo clippy --workspace --all-targets -- -D warnings must pass. Clippy’s pedantic group 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::wire and delune-library::naming).
  • Every public item gets a doc comment that says what it’s for, not just what it is.
  • Errors use thiserror in libraries and anyhow only in the binary and TUI.
  • No unsafe. The workspace forbids it.

TypeScript

  • bun run typecheck, bun run lint and bun run build must pass.
  • Components from src/components/ui come from shadcn; add new ones with bunx 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.