The previous .git directory was found gutted (only info/ remained), so history is unrecoverable locally. This commit restores version control with the tree exactly as it stands: the working app, the preview-tracks launcher on the artist hero, the scrollable-track-previews design bundle (docs + approved mockups), and the Phase 0 validation artifacts.
124 lines
4.1 KiB
Rust
124 lines
4.1 KiB
Rust
//! 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<u64, String> = 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();
|
|
}
|