SoulSeek Streaming¶
SoulSeek audio streaming follows the same pattern as the vozduxan path from the frontend's perspective: streamUrl() returns a local HTTP URL, and the <audio> element plays from it. The difference is entirely in the backend.
See also: File Transfer for the full P2P download protocol.
Full playback flow¶
User clicks a SoulSeek track
│
▼ streamUrl(magnet, fileIdx, { source: "soulseek", slskUsername, slskFilepath, slskFilesize })
│
▼ soulseekPrepareStream(username, filepath, filesize)
│ ≡ invoke("soulseek_prepare_stream", { username, filepath, filesize })
│
▼ Rust: download_and_stream(session, username, filepath, filesize, token)
│ │
│ ├── modern path: QueueUpload → TransferRequest → TransferResponse → F connection
│ │ (falls back to legacy TransferRequest if modern fails)
│ │
│ ├── download_loop: reads from F socket → neegde_slsk_<token>.tmp
│ │
│ └── http_server_loop on 127.0.0.1:<random_port>
│
▼ Returns { url: "http://127.0.0.1:<port>/slsk/<token>", token: "<n>" }
│
▼ <audio src={url} /> begins playback
│ HTTP Range requests → http_server_loop → reads temp file, polls downloaded counter
│
▼ Track ends / user skips
invoke("soulseek_release_stream", { token })
→ abort download task + HTTP task
→ delete neegde_slsk_<token>.tmp
Key differences from the vozduxan path¶
| Aspect | vozduxan (RuTracker) | SoulSeek |
|---|---|---|
| Download source | libtorrent via DHT/trackers | Single peer P2P TCP |
| Local server | C++ HTTP server (vozduxan) | Rust HTTP server (transfer.rs) |
| Stream URL | http://127.0.0.1:<port>/stream/<token> |
http://127.0.0.1:<port>/slsk/<token> |
| Seek support | Full (piece priority window slides) | Full (Range requests, polled temp file) |
| Prefetch | Token buckets (current/next/warm) | Not implemented — SoulSeek tracks are single-peer |
| Release | torrent_release_stream → C++ join |
soulseek_release_stream → abort + file delete |
| Token type | String (vozduxan UUID) | String (u32 as string) |
soulseek_prepare_stream command¶
const ready = await invoke<{ url: string; token: string }>(
"soulseek_prepare_stream",
{ username, filepath, filesize }
);
// ready.url = "http://127.0.0.1:<port>/slsk/<token>"
// ready.token = "<u32 as string>"
The Rust handler:
- Gets the active
Session(returns error if disconnected or dead). - Generates a new
token = next_token(). - Calls
transfer::download_and_stream(session, username, filepath, filesize, token). - Stores the
ActiveStream { temp_path, download_abort, http_abort }inSoulSeekState.streamskeyed bytoken.to_string(). - Returns
{ url, token }.
soulseek_release_stream command¶
Looks up the ActiveStream by token, aborts both tasks, and deletes the temp file. This is called:
- When the player moves to the next track (releaseTorrentStreamUrl).
- When the SoulSeek session is replaced by soulseek_login (all streams are drained in soulseek_logout).
HTTP server for SoulSeek¶
The HTTP server in transfer.rs implements a subset of HTTP/1.1 needed by the Web Audio pipeline:
OPTIONS→204 No Contentwith CORS headers (preflight for cross-origin from WebView).GETwithout Range →200 OKwithContent-Length: file_size,Accept-Ranges: bytes.GETwith Range →206 Partial Contentwith correctContent-Rangeheader.
Seek behaviour¶
When the audio element seeks, it sends a Range request for the new byte offset. The HTTP server:
- Checks
downloaded.load(Acquire)— the number of bytes already in the temp file. - If
downloaded > range_start, serve from the temp file immediately. - If not, spin in an 80 ms poll loop until
downloaded > range_startorcomplete = true. - If 60 seconds pass without the data arriving, return
503 Service Unavailable.
This means backwards seeks are instant (already downloaded), but forward seeks to positions far ahead of the buffer may stall briefly.
The HTTP server reads from the temp file in spawn_blocking using std::fs::File::seek + std::fs::File::read to avoid blocking the async executor. The read granularity is HTTP_CHUNK = 32 KB.
Temp file location¶
Temp files are named neegde_slsk_<token>.tmp and placed in std::env::temp_dir(). On macOS this is /var/folders/…/T/. They are created at the start of download_loop and deleted by soulseek_release_stream. If the app crashes before release, the files are left on disk but are typically cleaned by the OS on the next restart (temp dir is on APFS with auto-purge on macOS).
Failover to alternative peers¶
The frontend stores up to 5 alternative peers in raw.alternativePeers on each SoulSeek track entity (see Peer Ranking). If soulseek_prepare_stream throws (timeout, denied, NAT), the Player automatically retries with the next alternative peer:
// In Player.vue (simplified)
for (const alt of track.alternativePeers ?? []) {
try {
url = await streamUrl(magnet, 0, {
source: "soulseek",
slskUsername: alt.slskUsername,
slskFilepath: alt.slskFilepath,
slskFilesize: alt.size,
});
break;
} catch { /* try next */ }
}
This failover is transparent to the user — the audio starts as soon as any peer accepts the request.