Files
soulseek-rs-modded/README.md
T

230 lines
7.8 KiB
Markdown

# Soulseek-rs 🦀
Soulseek-rs is a Soulseek library and client built in Rust. Soulseek is a
closed-source P2P file-sharing network from the 2000s. It's still used by
music enthusiasts around the world to share niche music.
## 🎥 Demo
![CLI Demo](fixtures/demo.gif)
## Features
- **Search & download** — search the network, pick results in the TUI, and
queue downloads with pause, resume, and retry
- **Sharing** — point `--shared-dir` at a directory and your files show up in
searches; peers can browse and download them
- **Browse** — list any user's shared files and download straight from the tree
- **Chat rooms** — list, join, and talk in public rooms, several open at once
- **Private messages** — send and receive messages, with an inbox in the TUI
- **Firewalled peers** — downloads and browsing fall back to server-brokered
connections when a peer can't be reached directly
- **Automatic port mapping** — opens your listen port via UPnP-IGD and
NAT-PMP, with a `portmap` subcommand to test your router
- **TUI and CLI** — a full terminal interface, plus scriptable subcommands
(`search`, `message`, `browse`, `rooms`, `chat`, `portmap`)
- **Zero-dependency library** — the protocol crate uses only the Rust
standard library, down to its own zlib inflate and md5
## Project Goals
This project is a learning exercise to explore Rust. I've been using Soulseek
since the early 2000s, so it's close to my heart, and the Soulseek protocol is
a closed-source network protocol that provides a great opportunity to learn
about asynchronous and concurrent network programming and reverse engineering
Since it's a learning project, I have a self-imposed restriction not to use
external dependencies in the library. This means I can't use any external
crates that are not part of the Rust standard library. This is a good challenge
to learn how to build complex systems with only the standard library.
In the client crate, external dependencies are allowed for building a rich
experience. For me, this is a good balance between learning and practicality.
## Planned Features
- [ ] Headless mode daemon mode with remote control
## Project Structure
This project is organized as a Cargo workspace with two crates:
- **soulseek-rs-lib** - The core library implementing the Soulseek protocol
- **soulseek-rs** - A CLI client built on top of the library
This structure allows:
- Other developers to build custom Soulseek clients using `soulseek-rs-lib`
- Users to install the ready-made client via `cargo install soulseek-rs`
- Clean separation of concerns between protocol implementation and user interface
## Installation
### For Users
```bash
cargo install soulseek-rs
```
### For Developers
Clone and build from source:
```bash
git clone git@github.com:michel/soulseek-rs.git
cd soulseek-rs
cargo build --release
```
The binary will be available at `target/release/soulseek-rs`.
### For Library Users
To build your own Soulseek client, add to your `Cargo.toml`:
```toml
[dependencies]
soulseek-rs-lib = "5.0.0"
```
## Usage
```bash
./target/release/soulseek-rs "the weeknd Blinding Lights"
```
### Private messages
Send a private message to another user from the command line:
```bash
soulseek-rs message <username> "hello there"
```
In the interactive TUI:
- press `m` to compose — type `<recipient> <message>` and `Enter` to send;
- press `i` to open the inbox popup listing sent and received messages
(incoming messages arrive automatically while the TUI is open). The `i`
shortcut shows an unread counter, e.g. `i inbox (3)`.
### Chat rooms
From the command line:
```bash
soulseek-rs rooms # list public rooms with user counts
soulseek-rs chat <room> # join and print messages for a while
soulseek-rs chat <room> "hello room" # join, say one message, and exit
```
In the interactive TUI, press `c` to open the chat-rooms popup:
- the **room list** is browsable and `/`-filterable and shows each room's
user count (busiest first); press `Enter` to join the highlighted room;
- several rooms can be **open at once** as tabs — `Tab`/`Shift-Tab` switch
between them, `x` leaves the active room, `l` returns to the room list;
- in a room, press `Enter` to type a message and `Enter` again to send;
- the room's **member list** is selectable with `↑`/`↓`; press `b` to browse
the highlighted user's shared files or `m` to send them a private message;
- **unread messages** bold a room's tab and add a `room (n)` badge, and the
`c chat (n)` shortcut counts unread across all open rooms.
### Connectivity (being reachable)
Browsing and downloading are peer-to-peer, so at least one side must accept an
incoming connection. When the listener is enabled (the default), the client
**automatically tries to open its listen port** on your router via **UPnP-IGD**
and **NAT-PMP**, so firewalled peers and the server can connect back to you.
This is best-effort: if your router has UPnP/NAT-PMP disabled it's a no-op and
you'll see a log line suggesting you forward the port manually.
- The mapped/forwarded port is your `--listener-port` (env `LISTENER_PORT`,
default `2234`); it is renewed automatically and removed on exit.
- If auto-mapping can't get that exact port, forward **TCP 2234** (or whatever
`--listener-port` you chose) to this machine on your router.
- Pass `--disable-listener` to turn the listener (and port mapping) off.
Check whether it works on **your** network without launching the whole client:
```bash
soulseek-rs portmap
```
It tries to open the port via UPnP/NAT-PMP and prints whether your router
allowed it (and your external address), then removes the test mapping. If it
reports failure, enable UPnP on your router or forward the port manually.
If both you and a peer are behind routers with no forwarded port, browsing that
peer can't work — that's a fundamental Soulseek/peer-to-peer limitation, not a
bug.
## Development
To run the project in development mode with debug output and trace output:
```bash
RUST_LOG=trace cargo run
```
To run the tests:
```bash
cargo test
```
To run the linter:
```bash
cargo clippy
```
To run the formatter:
```bash
cargo fmt
```
### End-to-end tests
The `soulseek-rs-lib/tests/e2e.rs` suite exercises the client against a real
Soulseek server using [soulfind](https://github.com/soulfind-dev/soulfind), a
local server implementation. The tests are **server-optional**: they run when a
server is available and otherwise skip (so `cargo test` stays green everywhere).
They locate a server in this order:
1. `SOULSEEK_TEST_SERVER=host:port` — connect to an already-running server, or
2. `SOULFIND_BIN=/path/to/soulfind` (or a `soulfind/bin/soulfind` checkout in a
parent directory) — spawn soulfind on an ephemeral port with a throwaway
database.
```bash
# Build soulfind once (see its BUILDING.md), then:
SOULFIND_BIN=/path/to/soulfind/bin/soulfind \
cargo test -p soulseek-rs-lib --test e2e -- --nocapture
```
> On macOS, soulfind's `SQLITE_CONFIG_SINGLETHREAD` optimization is rejected by
> the system SQLite; building soulfind with that call made non-fatal lets it run
> locally. It is only a mutex optimization and does not affect the server.
Set `SOULSEEK_E2E_REQUIRED=1` to turn a missing server into a hard failure
instead of a skip. Continuous integration sets it so the e2e suite genuinely
runs against a freshly built soulfind rather than silently skipping.
### Continuous integration
`.github/workflows/ci.yml` runs on every push and pull request:
- **Format & Clippy** — `cargo fmt --all --check` and
`cargo clippy --workspace --all-targets -- -D warnings`.
- **Test** — builds soulfind from source (LDC + `dub build :server`), points
`SOULFIND_BIN` at it, and runs `cargo test --workspace` with
`SOULSEEK_E2E_REQUIRED=1`, so unit and end-to-end tests both run.
## License
This project is licensed under the MIT License — see the [LICENSE](./LICENSE)
file for details.