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:
Michel de Graaf
2026-07-26 14:20:07 +02:00
committed by GitHub
parent eda762d067
commit 9b87bd6a0f
29 changed files with 2991 additions and 145 deletions
@@ -92,6 +92,9 @@ fn build_upload_info_lines(upload: &UploadInfo) -> Vec<Line<'static>> {
lines.push(label_value("Size", &format_bytes(upload.size)));
let (status_text, status_style) = match &upload.status {
UploadStatus::Queued(place) => {
(format!("Queued (#{place})"), inactive_style())
}
UploadStatus::InProgress => {
("In progress".to_string(), warning_style())
}
@@ -132,7 +135,9 @@ fn build_upload_info_lines(upload: &UploadInfo) -> Vec<Line<'static>> {
)));
lines.push(Line::from(Span::styled(reason.clone(), error_style())));
}
UploadStatus::Completed | UploadStatus::Cancelled => {}
UploadStatus::Queued(_)
| UploadStatus::Completed
| UploadStatus::Cancelled => {}
}
lines