feat!: wishlist, upload-queue privilege recognition, and search filters (#20)
Closes the last two gaps in Soulseek's list of features an alternative client must implement to be tolerated, and adds the result filters Nicotine+ has had all along. Wishlist: server code 103 is sent and code 104's announced interval is kept rather than logged and discarded, so the re-search timer runs off the schedule the server set. `wish add|remove|list|run` plus `serve --wishlist`; wishes persist in config.toml as a TOML array so a comma inside a query survives. `wish run` prints search-shaped records, so it pipes into `download --stdin`. Privilege recognition: the code-69 user list is parsed instead of counted and dropped, and code 92 answers `whoami` as `privilege_seconds`. Uploads now go through a queue with a slot cap (`--upload-slots`, default 2) that serves privileged users first — the cap is what makes the ordering mean anything, so they are one change. `PlaceInQueueRequest` (peer code 51) is answered, where it previously had no handler at all, and a departed peer's slot is released so a stalled peer cannot wedge uploads shut. Search filters: `--exclude`, `--extension`, `--min-size`, `--max-size` beside the existing `--min-bitrate` and `--free-slots`, in one flattened arg group shared by `search`, `get` and `wish run`. Also serialises the library e2e suite behind the same gate `cli_e2e` already uses: the new queue tests hold peer sockets open on purpose, which starved concurrently-spawned soulfinds and failed unrelated download tests with `login: Timeout`. BREAKING CHANGE: `UploadStatus` gains a `Queued(u32)` variant, so an exhaustive match on it no longer compiles, and `cli::Commands` gains `Wish`.
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
use super::uploads::downloader_of;
|
||||
use super::{
|
||||
Arc, BROKER_CONNECT_TIMEOUT, Client, ClientContext, ClientOperation,
|
||||
ConnectionType, Download, DownloadPeer, DownloadStatus, Peer, PeerMessage,
|
||||
PeerRegistry, Receiver, RwLock, RwLockExt, ServerMessage, UploadJob,
|
||||
build_search_response, debug, error, info, next_connect_token,
|
||||
next_upload_token, sleep, thread, trace, warn,
|
||||
PeerRegistry, Receiver, RwLock, RwLockExt, ServerMessage,
|
||||
build_search_response, debug, error, info, next_connect_token, sleep,
|
||||
thread, trace, warn,
|
||||
};
|
||||
use crate::message::server::MessageFactory;
|
||||
|
||||
impl Client {
|
||||
pub(crate) fn listen_to_client_operations(
|
||||
@@ -102,6 +104,14 @@ impl Client {
|
||||
None,
|
||||
);
|
||||
}
|
||||
// A peer that leaves must not keep occupying an
|
||||
// upload slot: with a slot cap, a couple of
|
||||
// departed peers would otherwise wedge uploads
|
||||
// shut for the rest of the session.
|
||||
Self::release_upload_slots(
|
||||
&client_context,
|
||||
&username,
|
||||
);
|
||||
}
|
||||
ClientOperation::PierceFireWall(peer) => {
|
||||
Self::pierce_firewall(
|
||||
@@ -531,6 +541,12 @@ impl Client {
|
||||
),
|
||||
}
|
||||
}
|
||||
ClientOperation::WishlistInterval(seconds) => {
|
||||
if let Ok(mut ctx) = client_context.write_safe()
|
||||
{
|
||||
ctx.wishlist_interval = Some(seconds);
|
||||
}
|
||||
}
|
||||
ClientOperation::PeerConnected(username) => {
|
||||
// An outbound control connection just handshook.
|
||||
// Flush any downloads that were queued for this
|
||||
@@ -655,16 +671,10 @@ impl Client {
|
||||
requester_key,
|
||||
filename,
|
||||
} => {
|
||||
// A peer queued one of our shared files. Look it
|
||||
// up, mint an upload token, and offer it.
|
||||
let downloader = requester_key
|
||||
.strip_suffix(":direct")
|
||||
.unwrap_or(&requester_key)
|
||||
.to_string();
|
||||
let token = next_upload_token();
|
||||
let (registry, size) = match client_context
|
||||
.write_safe()
|
||||
{
|
||||
// The peer served next may not be this one.
|
||||
let downloader =
|
||||
downloader_of(&requester_key).to_string();
|
||||
match client_context.write_safe() {
|
||||
Ok(mut ctx) => {
|
||||
let Some(file) =
|
||||
ctx.shares.get(&filename)
|
||||
@@ -677,16 +687,13 @@ impl Client {
|
||||
};
|
||||
let size = file.size;
|
||||
let real_path = file.real_path.clone();
|
||||
ctx.uploads.insert(
|
||||
token,
|
||||
UploadJob {
|
||||
downloader: downloader.clone(),
|
||||
real_path,
|
||||
virtual_path: filename.clone(),
|
||||
size,
|
||||
},
|
||||
ctx.enqueue_upload(
|
||||
&requester_key,
|
||||
&downloader,
|
||||
&filename,
|
||||
real_path,
|
||||
size,
|
||||
);
|
||||
(ctx.peer_registry.clone(), size)
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
@@ -695,18 +702,59 @@ impl Client {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
Self::pump_upload_queue(&client_context);
|
||||
}
|
||||
ClientOperation::PlaceInQueueRequested {
|
||||
requester_key,
|
||||
filename,
|
||||
} => {
|
||||
let downloader = downloader_of(&requester_key);
|
||||
let (registry, place) =
|
||||
match client_context.read_safe() {
|
||||
Ok(ctx) => (
|
||||
ctx.peer_registry.clone(),
|
||||
ctx.place_in_queue(
|
||||
downloader, &filename,
|
||||
),
|
||||
),
|
||||
Err(_) => continue,
|
||||
};
|
||||
// Silence would leave the peer guessing; a file
|
||||
// no longer queued is being served, which is
|
||||
// place 0 by the same convention Nicotine+ uses.
|
||||
if let Some(registry) = registry {
|
||||
let _ = registry.send_to_peer(
|
||||
&requester_key,
|
||||
PeerMessage::ServeUpload {
|
||||
token,
|
||||
filename,
|
||||
size,
|
||||
},
|
||||
PeerMessage::SendMessage(
|
||||
MessageFactory::build_place_in_queue_response(
|
||||
&filename,
|
||||
place.unwrap_or(0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
ClientOperation::PrivilegedUsers(users) => {
|
||||
if let Ok(mut ctx) = client_context.write_safe()
|
||||
{
|
||||
debug!(
|
||||
"[client] {} privileged users listed",
|
||||
users.len()
|
||||
);
|
||||
ctx.set_privileged_users(users);
|
||||
}
|
||||
// Someone already waiting may have just been
|
||||
// outranked, but a slot that is free should
|
||||
// still be filled.
|
||||
Self::pump_upload_queue(&client_context);
|
||||
}
|
||||
ClientOperation::OwnPrivileges(seconds) => {
|
||||
if let Ok(mut ctx) = client_context.write_safe()
|
||||
{
|
||||
ctx.own_privileges = Some(seconds);
|
||||
}
|
||||
}
|
||||
ClientOperation::StartUpload { token } => {
|
||||
// The peer accepted our offer: resolve their
|
||||
// address (from the code-9 GetPeerAddress) and
|
||||
|
||||
Reference in New Issue
Block a user