Skip to content

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:

  1. Gets the active Session (returns error if disconnected or dead).
  2. Generates a new token = next_token().
  3. Calls transfer::download_and_stream(session, username, filepath, filesize, token).
  4. Stores the ActiveStream { temp_path, download_abort, http_abort } in SoulSeekState.streams keyed by token.to_string().
  5. Returns { url, token }.

soulseek_release_stream command

await invoke("soulseek_release_stream", { token: string });

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:

  • OPTIONS204 No Content with CORS headers (preflight for cross-origin from WebView).
  • GET without Range → 200 OK with Content-Length: file_size, Accept-Ranges: bytes.
  • GET with Range → 206 Partial Content with correct Content-Range header.

Seek behaviour

When the audio element seeks, it sends a Range request for the new byte offset. The HTTP server:

  1. Checks downloaded.load(Acquire) — the number of bytes already in the temp file.
  2. If downloaded > range_start, serve from the temp file immediately.
  3. If not, spin in an 80 ms poll loop until downloaded > range_start or complete = true.
  4. 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.