`Client::set_download_speed_limit(bytes_per_second)` puts one ceiling on every download at once. Zero lifts it and is the default. It can be changed at any time, and a change reaches transfers already running within one 100 ms tick. The limiter works the way Soulseek.NET's does, which slskd uses for its own download limit: one token bucket shared by all downloads, refilled to a tenth of a second's worth every 100 ms, handing out partial grants and taking back what a short read or a timeout did not use. A download is paced by reading less from the socket. The unread bytes stay in the kernel's receive buffer and TCP flow control slows the uploader; no protocol message is involved, as in Nicotine+ too. With no cap set, each read costs one atomic load. A same-hour stress run scored develop 88.98/88.98 and this branch 89.00/88.99/88.97, at about 133 MiB/s both. An upload ceiling is left out, as before.
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.
Usage
Add it to your Cargo.toml:
cargo add soulseek-rs-lib
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(())
}
ClientOperation is #[non_exhaustive]: every protocol message the client
learns to handle adds a variant, so match it with a wildcard arm. SharedFile
is too, so build it with the .. syntax rather than listing every field.