forked from Keelrleek/SoulStream
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.
100 lines
3.2 KiB
Rust
100 lines
3.2 KiB
Rust
//! Responder: answers other peers' requests — searches, browse, user-info,
|
|
//! folder contents. Search replies open a fresh P connection to the
|
|
//! searcher; the rest reply on the connection the request arrived on.
|
|
|
|
use std::sync::atomic::Ordering;
|
|
use std::sync::Arc;
|
|
|
|
use soulseek_proto::peer;
|
|
|
|
use crate::{conn, Event, Inner, PeerHandle};
|
|
|
|
/// Max files we return in a single search reply.
|
|
const MAX_SEARCH_RESULTS: usize = 100;
|
|
|
|
/// Match a relayed search against our shares and, if we have hits, send a
|
|
/// PeerSearchResponse back to `username`. Runs on its own task.
|
|
pub(crate) fn respond_to_search(inner: Arc<Inner>, username: String, token: u32, query: String) {
|
|
inner.searches_seen.fetch_add(1, Ordering::Relaxed);
|
|
// Never answer our own searches; de-dupe repeated relays of a token.
|
|
if username == inner.own_username() {
|
|
return;
|
|
}
|
|
{
|
|
let mut answered = inner.answered_searches.lock().unwrap();
|
|
if answered.contains(&token) {
|
|
return;
|
|
}
|
|
answered.insert(token);
|
|
// Bound the set so it can't grow without limit.
|
|
if answered.len() > 20_000 {
|
|
answered.clear();
|
|
}
|
|
}
|
|
|
|
if inner.shares.file_count() == 0 {
|
|
return;
|
|
}
|
|
|
|
tokio::spawn(async move {
|
|
let excluded = inner.excluded();
|
|
let files = inner.shares.search(&query, &excluded, MAX_SEARCH_RESULTS);
|
|
if files.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let frame = peer::search_response(
|
|
&inner.own_username(),
|
|
token,
|
|
&files,
|
|
inner.has_free_upload_slot(),
|
|
inner.upload_speed.load(Ordering::Relaxed),
|
|
inner.upload_queue.lock().unwrap().len() as u32,
|
|
);
|
|
|
|
// Search replies use a normal P connection to the searcher.
|
|
match conn::get_or_connect_peer(&inner, &username).await {
|
|
Ok(handle) => {
|
|
if handle.write_tx.send(frame).is_ok() {
|
|
let _ = inner.event_tx.send(Event::RespondedToSearch {
|
|
username: username.clone(),
|
|
files: files.len(),
|
|
});
|
|
}
|
|
}
|
|
Err(e) => log::debug!("couldn't reply to search from {username}: {e}"),
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Answer a BrowseRequest on the same connection.
|
|
pub(crate) fn respond_browse(inner: &Arc<Inner>, handle: &PeerHandle) {
|
|
let dirs = inner.shares.browse();
|
|
let _ = handle.write_tx.send(peer::browse_response(&dirs));
|
|
}
|
|
|
|
/// Answer a UserInfoRequest.
|
|
pub(crate) fn respond_user_info(inner: &Arc<Inner>, handle: &PeerHandle) {
|
|
let frame = peer::user_info_response(
|
|
&inner.description,
|
|
inner.upload_slots,
|
|
inner.upload_queue.lock().unwrap().len() as u32,
|
|
inner.has_free_upload_slot(),
|
|
);
|
|
let _ = handle.write_tx.send(frame);
|
|
}
|
|
|
|
/// Answer a FolderContentsRequest for one folder.
|
|
pub(crate) fn respond_folder_contents(
|
|
inner: &Arc<Inner>,
|
|
handle: &PeerHandle,
|
|
token: u32,
|
|
folder: &str,
|
|
) {
|
|
if let Some(dir) = inner.shares.folder(folder) {
|
|
let _ = handle
|
|
.write_tx
|
|
.send(peer::folder_contents_response(token, folder, &dir));
|
|
}
|
|
}
|