Files
soulseek-rs-modded/soulseek-rs-lib
Michel de Graaf 0bbb559f68 fix: survive busy sessions without exhausting file descriptors
The peer listener died with "Too many open files (os error 24)" on any
long-lived non-daemon session: only the daemon raised RLIMIT_NOFILE, and
peer sockets were unbounded — no idle reaping, no deadlines on accepted
transfer streams, and one peer could hold two connections under split
registry identities.

- raise the open-file limit at the top of run(), for every entry point,
  and warn from Client::connect when the soft limit is below 1024
- reap peer control connections idle for five minutes; a clean
  disconnect no longer discards the peer's queued uploads
- give listener-supplied download streams and upload serving the same
  read/write deadlines dialled sockets already had
- register inbound peers under their plain username so the registry
  holds one connection per peer, and announce them like pierced ones
- hold the registry lock across actor spawn + insert and surface
  thread-spawn failure, so an instantly-dead peer cannot leave a
  zombie entry
2026-08-09 22:34:36 +02:00
..

soulseek-rs-lib

A Rust library for implementing the Soulseek peer-to-peer protocol.

Website: https://re-invention.nl/soulseek-rs/

About

This library provides the core functionality for interacting with the Soulseek network. It can be used to build custom Soulseek clients or bots.

Usage

Add this to your Cargo.toml:

[dependencies]
soulseek-rs-lib = "8"

Example

Simple Usage

use soulseek_rs::Client;
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create and connect to Soulseek server
    let mut client = Client::new("username", "password");

    client.connect()?;
    client.login()?;

    // Search for files
    let results = client.search("Alex Kassian lifestream", Duration::from_secs(10))?;

    // Download first available file
    if let Some(result) = results.iter().find(|r| !r.files.is_empty()) {
        let file = &result.files[0];
        let (_download, status) = client.download(
            file.name.clone(),
            file.username.clone(),
            file.size,
            "~/Downloads".to_string(),
        )?;

        // `download` returns as soon as the request is queued: the transfer is
        // finished when the channel says so, not when this call returns.
        for update in status {
            println!("{}: {update:?}", file.name);
        }
    }

    Ok(())
}

Advanced Configuration

use soulseek_rs::{Client, ClientSettings, PeerAddress};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create client with custom settings
    let settings = ClientSettings {
        server_address: PeerAddress::new("server.slsknet.org".to_string(), 2416),
        enable_listen: true,
        listen_port: 3000,
        ..ClientSettings::new("username", "password")
    };

    let mut client = Client::with_settings(settings);
    client.connect()?;
    client.login()?;

    Ok(())
}

Asking about a peer

The server-backed lookups are request/poll pairs: you ask, then read the answer once it lands. request_user_info clears any previous answer first, so the loop below observes this request rather than the last one.

use soulseek_rs::Client;
use std::thread::sleep;
use std::time::{Duration, Instant};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = Client::new("username", "password");
    client.connect()?;
    client.login()?;

    client.request_user_info("someuser")?;

    let deadline = Instant::now() + Duration::from_secs(10);
    while Instant::now() < deadline {
        if let Some(info) = client.user_info("someuser") {
            // Presence and stats are separate replies, so each is an Option:
            // a reply that never arrived is None, not "offline" or "shares nothing".
            println!("{:?} {:?}", info.presence, info.stats);
            break;
        }
        sleep(Duration::from_millis(100));
    }

    // Room membership is kept current from join and leave events.
    println!("{:?}", client.room_members("lobby"));

    Ok(())
}

ServerMessage and ClientOperation are #[non_exhaustive]: every protocol message the client learns to handle adds a variant, so match them with a wildcard arm.