Files
soulseek-rs-modded/soulseek-rs
Michel de Graaf e8a48c6b06 fix!: make concurrent invocations work instead of silently seeing nothing (#22)
Running several one-shot commands at once returned "no results" for queries that
had plenty. Four defects, each of which made a dead session look like an empty
network:

- The peer listener bound its port inside a detached thread and panicked on
  EADDRINUSE, unwinding that thread alone. The process carried on with no
  listener, so no peer could deliver a search response. `Listen::bind` now takes
  the socket before login and falls back to an ephemeral port when the
  configured one is held.
- SetWaitPort advertised the configured port whether or not it was bound, so
  peers dialled a sibling process that discarded responses to tickets it never
  issued. The port the listener really holds is what gets advertised, mapped
  through UPnP, and reported by `whoami` and `serve`.
- A zero-byte read is EOF on a TCP socket, but MessageReader reported it as
  success, so a server that closed cleanly left the actor polling a dead socket.
  It is now an error, and Relogged (code 41) has a handler. The peer actor
  classifies that error: a control connection that was established and then
  closed is a clean disconnect, because peers drop idle sockets while the bytes
  move over their own connection, and reporting an error there fails every
  download queued with that peer including the one streaming. A connection that
  never established keeps the error path, which is what the server-brokered
  fallback reads to know a peer is unreachable.
- A session the server cut off - usually because another process logged in under
  the same name - exited 4, documented as "it worked and found nothing". Callers
  concluded the file was unavailable. Sessions now record why they ended and
  those verdicts become exit 7.

The server allows one session per account, so concurrency still needs a distinct
--username per run; SKILL.md documents that. The listener port needs no such
care any more.

Six e2e tests against soulfind cover it, each of which fails without the fix: a
taken listen port, a displaced session, three clients searching at once under
one configured port, the two CLI equivalents, and a peer that hangs up its
control connection while a transfer is still to come.

BREAKING CHANGE: `Listen::start` is replaced by `Listen::bind` and
`Listen::serve`, so the caller owns the socket and can read the port it actually
bound. `Client::listen_port` and `Client::session_loss` are new.
2026-07-26 14:52:31 +02:00
..
2026-07-26 14:31:00 +02:00

soulseek-rs

A Soulseek client for the terminal: a full-screen TUI, and a one-shot command surface built for scripts and agents.

Website: https://re-invention.nl/soulseek-rs/

Installation

brew install michel/tap/soulseek-rs   # macOS and Linux, prebuilt
cargo install soulseek-rs             # anywhere with a Rust toolchain

Usage

Run it with no subcommand for the interactive TUI:

soulseek-rs

Anything else is a one-shot command that runs headless. stdout carries records only, stderr carries progress, and the exit code carries the verdict:

soulseek-rs search <QUERY>                # print matching files
soulseek-rs get <QUERY>                   # search, pick the best, download
soulseek-rs download <USER> <PATH>        # fetch one known file
soulseek-rs download --stdin              # fetch files listed on stdin
soulseek-rs browse <USER>                 # list a user's shared files
soulseek-rs serve [--follow]              # stay online sharing, stream uploads
soulseek-rs whoami                        # confirm credentials and connection
soulseek-rs user <NAME>                   # a peer's status and share counts
soulseek-rs room list|say|users|listen
soulseek-rs message send|read
soulseek-rs shares list|add|remove|status|reindex
soulseek-rs config path|list|get|set
soulseek-rs portmap                       # test automatic port mapping
soulseek-rs completions install           # tab completion for bash/zsh/fish

Add --json for newline-delimited JSON instead of tab-separated text, so commands compose:

soulseek-rs search "rjd2" --json \
  | jq -c 'select(.size > 5000000 and .free_slot)' \
  | soulseek-rs download --stdin

Exit codes: 0 success, 2 usage or config, 3 connect or login, 4 nothing found, 5 timed out, 6 transfer failed, 7 session lost mid-command, 1 unexpected error.

The full README documents every command, record shape, flag, environment variable and config.toml key.

Building from Source

git clone https://github.com/michel/soulseek-rs.git
cd soulseek-rs
cargo build --release

Library

To build your own Soulseek client, use the soulseek-rs-lib crate: the protocol implementation this client is built on.