Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

40 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

slsk-relay

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.

External requirements

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.

Accounts

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).

Public deployment

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.json can never silently expose the server. Never run a public deployment with ALLOW_OPEN_MODE=true.
  • Set TRUST_PROXY=true behind 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, default sftp_hostkey) and a change is refused. Set SFTP_HOST_KEY_SHA256 to pin explicitly, or SFTP_INSECURE_HOST_KEY=true to 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, s and password parameters.
  • Keep users.json (plaintext passwords, required for token auth) and userdata.json out of any web root and out of git (both are gitignored and chmod 600).

Caching

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.

How streaming works

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_FOLDER is a local path;
  • remote over SFTP: set SFTP_HOST and SFTP_USER, authenticate with SFTP_PASSWORD and/or SFTP_KEY_FILE (+ SFTP_KEY_PASSPHRASE for encrypted keys), optionally pin the host key with SFTP_HOST_KEY_SHA256, and SLSK_DOWNLOADS_FOLDER becomes 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.

Per-user data

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).

Lyrics

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.

Transcoding

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.

Implemented endpoints

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.

Tests

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.

Known limitations

  • No filesystem library: getArtists/getIndexes and 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.
  • getAlbumList2 orders recent/frequent by your play history (recorded via scrobble), highest by your ratings, byGenre by the starred artists' Spotify genres, and newest/starred by star date.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages