The message-set coverage is the crate's strongest claim, so say it where people look first: the README features list and About, the website home and community pages, each pointing at the coverage table. The table now carries a section naming what the message set does not reach, so the claim has something behind it rather than standing on its own. The upload streaming test still asserted the uploader was blocked waiting for the downloader's close — the wait that came out when it started costing the upload slot. It passed only while the thread had yet to be scheduled, and failed whenever it had.
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 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(())
}
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.