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:
@@ -22,7 +22,7 @@ use crate::{
|
||||
utils::{lock::RwLockExt, md5, thread_pool::ThreadPool},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
net::TcpStream,
|
||||
sync::{
|
||||
RwLock,
|
||||
@@ -35,10 +35,24 @@ use std::{
|
||||
sync::{Arc, mpsc},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use upload_queue::QueuedUpload;
|
||||
|
||||
use crate::{debug, error, info, trace, warn};
|
||||
const DEFAULT_LISTEN_PORT: u16 = 2234;
|
||||
|
||||
/// What to assume between wishlist searches until the server announces its own.
|
||||
///
|
||||
/// Twelve minutes is what Soulseek servers give an unprivileged account (code
|
||||
/// 104); guessing lower only gets the searches dropped.
|
||||
pub const DEFAULT_WISHLIST_INTERVAL: Duration = Duration::from_mins(12);
|
||||
|
||||
/// How many uploads run at once by default.
|
||||
///
|
||||
/// Two is what makes the queue real: with unlimited slots there is nothing for
|
||||
/// a privileged peer to jump, and a home connection split across every peer
|
||||
/// that asks serves all of them badly.
|
||||
pub const DEFAULT_UPLOAD_SLOTS: usize = 2;
|
||||
|
||||
/// How long to wait for a server-brokered (firewalled) peer to connect back
|
||||
/// before giving up and failing the download. Matches the direct-dial timeout.
|
||||
const BROKER_CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
@@ -248,6 +262,19 @@ pub enum ClientOperation {
|
||||
/// Something happened in the chat-room subsystem (list refreshed, a room
|
||||
/// joined/left, a message said, a member joined/left).
|
||||
RoomEvent(RoomEvent),
|
||||
/// The server announced how many seconds must pass between wishlist
|
||||
/// searches.
|
||||
WishlistInterval(u32),
|
||||
/// Everyone the server counts as privileged; they sort ahead of others in
|
||||
/// our upload queue.
|
||||
PrivilegedUsers(Vec<String>),
|
||||
/// Seconds of our own privileges left.
|
||||
OwnPrivileges(u32),
|
||||
/// A peer asked where their queued file sits.
|
||||
PlaceInQueueRequested {
|
||||
requester_key: String,
|
||||
filename: String,
|
||||
},
|
||||
}
|
||||
pub struct ClientContext {
|
||||
pub peer_registry: Option<PeerRegistry>,
|
||||
@@ -284,6 +311,20 @@ pub struct ClientContext {
|
||||
/// What the server has told us about other users, merged across the
|
||||
/// separate status and statistics replies.
|
||||
user_info: HashMap<String, UserInfo>,
|
||||
/// Seconds the server wants between wishlist searches (code 104), once it
|
||||
/// has told us.
|
||||
wishlist_interval: Option<u32>,
|
||||
/// Everyone the server listed as privileged (code 69). They sort ahead of
|
||||
/// other peers in [`Self::upload_queue`].
|
||||
privileged_users: HashSet<String>,
|
||||
/// Seconds of our own privileges left (code 92), once we have asked.
|
||||
own_privileges: Option<u32>,
|
||||
/// Peers waiting for one of our upload slots.
|
||||
upload_queue: Vec<QueuedUpload>,
|
||||
/// Arrival counter for the queue's first-come tie-break.
|
||||
upload_seq: u64,
|
||||
/// How many uploads may be in flight at once.
|
||||
upload_slots: usize,
|
||||
actor_system: Arc<ActorSystem>,
|
||||
}
|
||||
impl Default for ClientContext {
|
||||
@@ -580,6 +621,12 @@ impl ClientContext {
|
||||
room_events: Vec::new(),
|
||||
room_members: HashMap::new(),
|
||||
user_info: HashMap::new(),
|
||||
wishlist_interval: None,
|
||||
privileged_users: HashSet::new(),
|
||||
own_privileges: None,
|
||||
upload_queue: Vec::new(),
|
||||
upload_seq: 0,
|
||||
upload_slots: DEFAULT_UPLOAD_SLOTS,
|
||||
downloads: DownloadStore::new(),
|
||||
actor_system,
|
||||
}
|
||||
@@ -820,8 +867,9 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
/// Snapshot of the uploads served this session (active and finished),
|
||||
/// most recent last.
|
||||
/// Every upload this session knows about: the ones streaming or finished,
|
||||
/// followed by the peers still waiting for a slot, in the order they will
|
||||
/// be served.
|
||||
#[must_use]
|
||||
pub fn uploads(&self) -> Vec<crate::types::UploadInfo> {
|
||||
self.context.read_safe().map_or_else(
|
||||
@@ -829,7 +877,7 @@ impl Client {
|
||||
|ctx| {
|
||||
let mut tokens: Vec<&u32> = ctx.active_uploads.keys().collect();
|
||||
tokens.sort_unstable();
|
||||
tokens
|
||||
let mut all: Vec<crate::types::UploadInfo> = tokens
|
||||
.into_iter()
|
||||
.map(|token| {
|
||||
let upload = &ctx.active_uploads[token];
|
||||
@@ -849,7 +897,9 @@ impl Client {
|
||||
status: upload.status.clone(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
all.extend(ctx.queued_uploads());
|
||||
all
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -919,6 +969,7 @@ mod downloads;
|
||||
mod operations;
|
||||
mod rooms;
|
||||
mod search;
|
||||
mod upload_queue;
|
||||
mod uploads;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user