A dynamic Subsonic / OpenSubsonic API server with no local library: metadata comes live from Spotify, audio comes on demand from Soulseek (via slskd). Point any Subsonic client (DSub, Symfonium, Substreamer, Sonixd, …) at it and search/browse/stream as if it were a normal server.
| Dependency | Purpose | Setup |
|---|---|---|
| slskd | Searches Soulseek and downloads the audio that gets streamed | Run slskd with a Soulseek account, enable its API, set SLSK_URL + SLSK_KEY and point SLSK_DOWNLOADS_FOLDER at its download directory |
| Spotify app | All metadata: search, artists, albums, tracks, cover art, new releases | Create an app at developer.spotify.com, set SPOTIFY_ID + SPOTIFY_SECRET (client-credentials flow, no user login needed) |
| Last.fm API key | Artist biographies and similar artists (getArtistInfo/getArtistInfo2) |
Create a key at last.fm/api, set LASTFM_API_KEY |
Copy .env.example to .env, fill it in, then:
go run .The server listens on :4533 by default.
Users live in USERS_FILE (default users.json). Secure by default: while no accounts exist, every request is rejected. Bootstrap either by setting SUBSONIC_USER/SUBSONIC_PASSWORD (seeded as an admin account), or by starting once with ALLOW_OPEN_MODE=true — in open mode any username/password works and every requester is an implicit admin, so you can create the first real account and then remove the flag:
curl "http://localhost:4533/rest/createUser?u=x&p=x&username=me&password=secret&adminRole=true"From then on clients must log in with a real account (plain, enc:, and token auth are all supported). The full Subsonic user API is implemented: getUser, getUsers, createUser, updateUser, deleteUser, changePassword (admin role required for managing other users). SUBSONIC_USER/SUBSONIC_PASSWORD still work and are seeded as an admin account. Note: passwords are stored in plaintext in the users file because Subsonic token auth requires the server to know them — keep the file private (it is chmod 600 and gitignored).
Before exposing the relay on a public domain:
- HTTPS is mandatory. The Subsonic protocol sends credentials in query parameters, so plain HTTP leaks them to every network hop. Put the relay behind a TLS reverse proxy (Caddy, nginx, Traefik) and only expose the proxy.
- Auth is closed by default. An empty account store rejects everything — a lost or mispathed
users.jsoncan never silently expose the server. Never run a public deployment withALLOW_OPEN_MODE=true. - Set
TRUST_PROXY=truebehind a reverse proxy so login rate limiting buckets by real client address instead of the proxy's. Leave it unset when the relay is directly exposed, or clients could forge the header. - The SFTP host key is pinned on first connect (stored in
SFTP_KNOWN_HOSTS, defaultsftp_hostkey) and a change is refused. SetSFTP_HOST_KEY_SHA256to pin explicitly, orSFTP_INSECURE_HOST_KEY=trueto disable verification (not recommended with password auth). - Failed logins are rate-limited: 10 failures per client address per minute, then further attempts are rejected. Request logs redact the
p,t,sandpasswordparameters. - Keep
users.json(plaintext passwords, required for token auth) anduserdata.jsonout of any web root and out of git (both are gitignored and chmod 600).
All external API responses are cached in memory: Spotify metadata 24h, searches 30min, album lists 6h, Last.fm artist info 24h, cover art images 24h. Repeated browsing hits Spotify/Last.fm only once per TTL, and the Spotify client-credentials token renews automatically on expiry.
stream?id=spotify:track:… resolves the track on Spotify, searches Soulseek for artist title, queues the best unlocked mp3/flac match, then tails the growing file from the slskd download folder to the client while it downloads.
The relay never joins the Soulseek network itself — only slskd does. The download folder can be:
- local (default): relay runs on the same host as slskd,
SLSK_DOWNLOADS_FOLDERis a local path; - remote over SFTP: set
SFTP_HOSTandSFTP_USER, authenticate withSFTP_PASSWORDand/orSFTP_KEY_FILE(+SFTP_KEY_PASSPHRASEfor encrypted keys), optionally pin the host key withSFTP_HOST_KEY_SHA256, andSLSK_DOWNLOADS_FOLDERbecomes the path on that host. This lets the relay run anywhere — e.g. slskd on a seedbox, relay at home — with no VPN and no network mount.
Playlists (createPlaylist/updatePlaylist/deletePlaylist/getPlaylist(s), with public/private visibility), starred items (star/unstar/getStarred(2)), ratings (setRating, surfaced as userRating on songs), bookmarks (createBookmark/deleteBookmark/getBookmarks) and the play queue (savePlayQueue/getPlayQueue) are persisted per user in USERDATA_FILE (default userdata.json).
getLyrics (by artist/title) and the OpenSubsonic getLyricsBySongId extension are backed by LRCLIB — no API key needed. Synced (LRC) lyrics are returned as structured lines with millisecond offsets when available.
If ffmpeg is on PATH (or set via FFMPEG_PATH), stream honors maxBitRate (kbps), format (mp3, opus, or raw) and timeOffset (seconds) — including while the Soulseek download is still running. Without ffmpeg, streams always pass through raw.
System: ping, getLicense, getOpenSubsonicExtensions ·
Users: getUser(s), createUser, updateUser, deleteUser, changePassword ·
Browsing: getMusicFolders, getIndexes, getMusicDirectory, getArtists, getArtist, getAlbum, getSong, getGenres, getArtistInfo(2), getAlbumInfo(2) (Last.fm notes), getTopSongs (Last.fm top tracks resolved to Spotify), getSimilarSongs(2), getNowPlaying, getSongsByGenre ·
Lists: getAlbumList(2), getRandomSongs, getStarred(2) (all from the user's starred collection) ·
Search: search2, search3 ·
Playlists: full CRUD ·
Media: stream, download, getCoverArt, getLyrics ·
Annotation: star, unstar, setRating, scrobble, getPlayQueue, savePlayQueue, getBookmarks, createBookmark, deleteBookmark ·
Misc: getScanStatus, startScan, getShares, getPodcasts, getInternetRadioStations
All endpoints answer in JSON (default) or XML (f=xml / .view), always HTTP 200 with Subsonic error codes in the body, and carry the OpenSubsonic type/serverVersion/openSubsonic fields.
go test ./... runs the regression suite: REST handlers against a fake Spotify/Last.fm/slskd (auth lifecycle, playlists, starred, play queue, search shapes, streaming, error formats, XML), the slskd client against an emulated slskd server (including the fast-completion and extension-derivation regressions), storage backends against an in-process SFTP server, and the cache/accounts/userdata stores.
- No filesystem library:
getArtists/getIndexesand empty-query search serve the user's starred items — star artists/albums/songs to build your library; discovery happens through search and album lists. - Podcasts, internet radio, shares, jukebox, chat and video endpoints return valid but empty responses (out of scope for this design).
- First play of a track waits for the Soulseek search and download to start (a few seconds); replays are served from the download cache instantly.
getAlbumList2ordersrecent/frequentby your play history (recorded viascrobble),highestby your ratings,byGenreby the starred artists' Spotify genres, andnewest/starredby star date.