Four places told people to write `soulseek-rs-lib = "8"` (RELEASE.md said "6"), and every one of them had been left behind by ten majors or more. The failure is silent rather than loud: 6.x and 8.0.0 are both real published versions, so cargo resolves the snippet happily and hands over a library from August instead of refusing it, and one of the four is soulseek-rs-lib's own README, which is the page crates.io renders. `cargo add` writes whatever version is current at the time it runs, so there is no longer a number in the docs to go stale. The manifest pin in soulseek-rs/Cargo.toml stays as it is — release-plz owns that one and keeps it in step with the release.
120 lines
3.2 KiB
Markdown
120 lines
3.2 KiB
Markdown
# 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`:
|
|
|
|
```bash
|
|
cargo add soulseek-rs-lib
|
|
```
|
|
|
|
## Example
|
|
|
|
### Simple Usage
|
|
|
|
```rust,no_run
|
|
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
|
|
|
|
```rust,no_run
|
|
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.
|
|
|
|
```rust,no_run
|
|
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.
|