forked from Keelrleek/SoulStream
Baseline: full tree snapshot after git repository loss
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.
This commit is contained in:
commit
a667b4b28e
161 files changed
+53170
No files matched your search
@@ -0,0 +1,86 @@
|
||||
//! Live citizenship test: share our downloaded albums, join the distributed
|
||||
//! network, and report every search we answer for real peers.
|
||||
//!
|
||||
//! cargo run -p soulseek-client --example citizen
|
||||
|
||||
use soulseek_client::{Client, Config, Event};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
let suffix = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.subsec_nanos()
|
||||
% 100_000;
|
||||
let username = format!("ssplusshare{suffix}");
|
||||
|
||||
let music = dirs_music().join("SoulSeek+");
|
||||
let mut config = Config::default();
|
||||
config.shared_folders = vec![music.clone()];
|
||||
config.upload_slots = 4;
|
||||
config.description = "SoulSeek+ dev share".into();
|
||||
|
||||
println!("sharing {}…", music.display());
|
||||
let (client, mut events) = Client::connect(config).await.expect("connect");
|
||||
let (dirs, files) = client.share_counts();
|
||||
println!("indexed {files} files in {dirs} folders");
|
||||
|
||||
client
|
||||
.login(&username, "ssplusdev-throwaway")
|
||||
.await
|
||||
.expect("login");
|
||||
println!("logged in as {username}; joining network, waiting for searches to answer…\n");
|
||||
|
||||
let mut answered = 0usize;
|
||||
let mut uploads = 0usize;
|
||||
let start = tokio::time::Instant::now();
|
||||
let deadline = start + std::time::Duration::from_secs(180);
|
||||
let mut next_tick = start + std::time::Duration::from_secs(20);
|
||||
loop {
|
||||
let ev = tokio::select! {
|
||||
ev = events.recv() => ev,
|
||||
_ = tokio::time::sleep_until(next_tick) => {
|
||||
next_tick += std::time::Duration::from_secs(20);
|
||||
println!(
|
||||
" … {}s: parent={}, {} searches seen, {answered} answered",
|
||||
start.elapsed().as_secs(),
|
||||
client.has_parent(),
|
||||
client.searches_seen(),
|
||||
);
|
||||
if tokio::time::Instant::now() >= deadline { break; }
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match ev {
|
||||
Some(Event::RespondedToSearch { username, files }) => {
|
||||
answered += 1;
|
||||
if answered <= 25 {
|
||||
println!(" ✓ answered {username} with {files} file(s)");
|
||||
}
|
||||
}
|
||||
Some(Event::Upload(u)) => {
|
||||
uploads += 1;
|
||||
println!(" ⬆ upload to {}: {:?} {}", u.username, u.state, u.filename);
|
||||
}
|
||||
Some(Event::Disconnected { reason }) => {
|
||||
println!("disconnected: {reason}");
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"\nRESULT: {} searches seen, {answered} answered, {uploads} upload events",
|
||||
client.searches_seen(),
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
fn dirs_music() -> std::path::PathBuf {
|
||||
// %USERPROFILE%\Music
|
||||
let profile = std::env::var("USERPROFILE").unwrap_or_else(|_| ".".into());
|
||||
std::path::PathBuf::from(profile).join("Music")
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
//! 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();
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Smoke test against the live Soulseek network:
|
||||
//! login with a throwaway account, run one search, print results.
|
||||
//!
|
||||
//! cargo run -p soulseek-client --example probe -- "search terms"
|
||||
|
||||
use soulseek_client::{Client, Config, 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 suffix = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.subsec_nanos()
|
||||
% 100_000;
|
||||
let username = format!("ssplusdev{suffix}");
|
||||
|
||||
println!("connecting…");
|
||||
let (client, mut events) = Client::connect(Config::default()).await.expect("connect");
|
||||
println!("listener on port {}", client.listen_port());
|
||||
|
||||
match client.login(&username, "ssplusdev-throwaway").await {
|
||||
Ok(greeting) => println!("logged in as {username}: {}", greeting.trim()),
|
||||
Err(e) => {
|
||||
eprintln!("login failed: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let token = client.search(&query).expect("search");
|
||||
println!("searching for {query:?} (token {token}) — collecting for 25s…\n");
|
||||
|
||||
let mut responses = 0usize;
|
||||
let mut files = 0usize;
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(25);
|
||||
|
||||
loop {
|
||||
let ev = tokio::select! {
|
||||
ev = events.recv() => ev,
|
||||
_ = tokio::time::sleep_until(deadline) => break,
|
||||
};
|
||||
match ev {
|
||||
Some(Event::SearchResult { response, .. }) => {
|
||||
responses += 1;
|
||||
files += response.files.len();
|
||||
if responses <= 5 {
|
||||
let f = &response.files[0];
|
||||
println!(
|
||||
" [{}] {} files, e.g. {} ({} MB{}) slots={} speed={}",
|
||||
response.username,
|
||||
response.files.len(),
|
||||
f.filename.rsplit('\\').next().unwrap_or(&f.filename),
|
||||
f.size / 1_000_000,
|
||||
f.bitrate()
|
||||
.map(|b| format!(", {b} kbps"))
|
||||
.unwrap_or_default(),
|
||||
response.has_free_upload_slot,
|
||||
response.upload_speed,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(Event::Disconnected { reason }) => {
|
||||
println!("disconnected: {reason}");
|
||||
break;
|
||||
}
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nRESULT: {responses} responses, {files} files");
|
||||
client.shutdown();
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Two-instance upload test: instance A shares a known file; instance B
|
||||
//! downloads it from A. Verifies the upload responder + F-connection path.
|
||||
//!
|
||||
//! cargo run -p soulseek-client --example upload_test
|
||||
|
||||
use soulseek_client::{Client, Config, DownloadState, Event};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
// Build a share source with one real audio file.
|
||||
let src = std::env::temp_dir().join("ssplus-upload-src");
|
||||
let album = src.join("TestAlbum");
|
||||
std::fs::create_dir_all(&album).unwrap();
|
||||
let sample = find_sample().expect("need a downloaded audio file to share");
|
||||
let shared_file = album.join("sample.flac");
|
||||
std::fs::copy(&sample, &shared_file).unwrap();
|
||||
let size = std::fs::metadata(&shared_file).unwrap().len();
|
||||
let remote_path = "ssplus-upload-src\\TestAlbum\\sample.flac".to_string();
|
||||
println!("A shares {remote_path} ({size} bytes)");
|
||||
|
||||
let stamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.subsec_nanos()
|
||||
% 100_000;
|
||||
let user_a = format!("ssplusA{stamp}");
|
||||
let user_b = format!("ssplusB{stamp}");
|
||||
|
||||
// Instance A: sharer/uploader.
|
||||
let mut cfg_a = Config::default();
|
||||
cfg_a.shared_folders = vec![src.clone()];
|
||||
cfg_a.listen_port = 51300;
|
||||
let (client_a, mut events_a) = Client::connect(cfg_a).await.expect("A connect");
|
||||
client_a.login(&user_a, "pw").await.expect("A login");
|
||||
println!("A logged in as {user_a}");
|
||||
|
||||
// Spawn a task to log A's upload events.
|
||||
tokio::spawn(async move {
|
||||
while let Some(ev) = events_a.recv().await {
|
||||
if let Event::Upload(u) = ev {
|
||||
println!(
|
||||
" A upload → {}: {:?} {}/{}",
|
||||
u.username, u.state, u.sent, u.total
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Instance B: downloader. Different listen port to coexist on one host.
|
||||
let mut cfg_b = Config::default();
|
||||
cfg_b.listen_port = 52300;
|
||||
cfg_b.downloads_dir = std::env::temp_dir().join("ssplus-upload-dst");
|
||||
let _ = std::fs::remove_dir_all(&cfg_b.downloads_dir);
|
||||
let (client_b, mut events_b) = Client::connect(cfg_b).await.expect("B connect");
|
||||
client_b.login(&user_b, "pw").await.expect("B login");
|
||||
println!("B logged in as {user_b}");
|
||||
|
||||
// Both instances share this host's public IP, so the server-provided
|
||||
// address would need NAT hairpinning. Seed loopback endpoints so the P
|
||||
// and F connections go over localhost instead.
|
||||
client_b.set_peer_address(&user_a, "127.0.0.1:51300".parse().unwrap());
|
||||
client_a.set_peer_address(&user_b, "127.0.0.1:52300".parse().unwrap());
|
||||
|
||||
// Give the server a moment to register both listen ports.
|
||||
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||||
|
||||
let id = client_b.download(&user_a, &remote_path, size);
|
||||
println!("B requested download #{id} from A…\n");
|
||||
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(90);
|
||||
loop {
|
||||
let ev = tokio::select! {
|
||||
ev = events_b.recv() => ev,
|
||||
_ = tokio::time::sleep_until(deadline) => { println!("TIMEOUT"); break; }
|
||||
};
|
||||
match ev {
|
||||
Some(Event::Download(u)) => match &u.state {
|
||||
DownloadState::Complete { local_path } => {
|
||||
let got = std::fs::metadata(local_path).map(|m| m.len()).unwrap_or(0);
|
||||
println!("\nB COMPLETE: {local_path} ({got}/{size} bytes)");
|
||||
println!(
|
||||
"RESULT: {}",
|
||||
if got == size {
|
||||
"UPLOAD VERIFIED ✓"
|
||||
} else {
|
||||
"size mismatch ✗"
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
DownloadState::Failed { reason } => {
|
||||
println!("\nB FAILED: {reason}");
|
||||
println!("RESULT: upload not verified (likely NAT hairpin on same host)");
|
||||
break;
|
||||
}
|
||||
other => println!(" B: {:?} {}/{}", other, u.received, u.total),
|
||||
},
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
client_a.shutdown();
|
||||
client_b.shutdown();
|
||||
}
|
||||
|
||||
/// Find any downloaded audio file to use as the shared sample.
|
||||
fn find_sample() -> Option<PathBuf> {
|
||||
let root = PathBuf::from(std::env::var("USERPROFILE").ok()?)
|
||||
.join("Music")
|
||||
.join("SoulSeek+");
|
||||
fn walk(dir: &std::path::Path) -> Option<PathBuf> {
|
||||
for e in std::fs::read_dir(dir).ok()?.flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
if let Some(f) = walk(&p) {
|
||||
return Some(f);
|
||||
}
|
||||
} else if p.extension().and_then(|x| x.to_str()) == Some("flac") {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
walk(&root)
|
||||
}
|
||||
Reference in new issue
Block a user