//! Live-network download smoke test: search, pick the best folder, //! download its files. //! //! cargo run -p soulseek-client --example download -- "search terms" [max_files] use std::collections::HashMap; use soulseek_client::{Client, Config, DownloadState, Event}; #[tokio::main] async fn main() { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); let query = std::env::args() .nth(1) .unwrap_or_else(|| "daft punk discovery".into()); let max_files: usize = std::env::args() .nth(2) .and_then(|s| s.parse().ok()) .unwrap_or(2); let suffix = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .subsec_nanos() % 100_000; let username = format!("ssplusdev{suffix}"); let mut config = Config::default(); config.downloads_dir = std::env::temp_dir().join("ssplus-download-test"); let (client, mut events) = Client::connect(config).await.expect("connect"); client .login(&username, "ssplusdev-throwaway") .await .expect("login"); println!("logged in as {username}"); let token = client.search(&query).expect("search"); println!("searching {query:?}…"); // Collect responses for 12s, pick the best free-slot folder. let mut best: Option<(u64, soulseek_proto::types::SearchResponse)> = None; let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(12); loop { let ev = tokio::select! { ev = events.recv() => ev, _ = tokio::time::sleep_until(deadline) => break, }; if let Some(Event::SearchResult { response, .. }) = ev { if !response.has_free_upload_slot || response.files.len() < 3 { continue; } let score = response.upload_speed as u64; if best.as_ref().map(|(s, _)| score > *s).unwrap_or(true) { best = Some((score, response)); } } } client.stop_search(token); let Some((_, pick)) = best else { println!("RESULT: no suitable candidates"); return; }; println!( "picked {} ({} files, speed {})", pick.username, pick.files.len(), pick.upload_speed ); let mut pending: HashMap = HashMap::new(); for f in pick.files.iter().take(max_files) { let id = client.download(&pick.username, &f.filename, f.size); println!( " queued #{id}: {}", f.filename.rsplit('\\').next().unwrap() ); pending.insert(id, f.filename.clone()); } let overall = tokio::time::Instant::now() + std::time::Duration::from_secs(180); let mut ok = 0; while !pending.is_empty() { let ev = tokio::select! { ev = events.recv() => ev, _ = tokio::time::sleep_until(overall) => { println!("TIMEOUT"); break; } }; match ev { Some(Event::Download(u)) => match &u.state { DownloadState::Complete { local_path } => { println!(" ✔ #{} complete → {local_path}", u.id); pending.remove(&u.id); ok += 1; } DownloadState::Failed { reason } => { println!(" ✘ #{} failed: {reason}", u.id); pending.remove(&u.id); } DownloadState::Transferring if u.total > 0 => { println!( " … #{} {:.0}% ({}/{} bytes, {} KB/s)", u.id, u.received as f64 / u.total as f64 * 100.0, u.received, u.total, u.speed_bps / 1000 ); } s => println!(" · #{} {:?}", u.id, s), }, Some(Event::Disconnected { reason }) => { println!("disconnected: {reason}"); break; } Some(_) => {} None => break, } } println!("RESULT: {ok} downloads completed"); client.shutdown(); }