docs: tell the whole daemon story, and stop denying it exists
An audit of the README and the site against the implementation found 38 places the docs were missing, stale, or wrong about daemon mode. Two were outright false: the community page still answered "want a queue that outlives the command?" with "slskd has that", and the home page promised "no daemon, no API to stand up" fifty lines before the section that describes both. The download-box walkthrough now tells the truth about its sharp edges: a pushed folder or share list is live-only on a remote daemon, whose own config.toml wins at restart; `shares add` validates the path on the machine you type it on; and a `get` that reaches its `--timeout` cancels the daemon-side transfer. Its shell example also runs as written now — the old block put a second command after a foreground daemon that never returns. The reference material catches up with the feature: `daemon` and its subcommands join the command list, the config table gains the daemon/daemon_token/--no-daemon rows, nine settings become eleven, `daemon status` and `daemon token` get documented record shapes, exit 3 covers daemons, `queued` joins the serve statuses, the credential-free count goes 13 to 19, and the action-command count 4 to 6. And the surprises are written down rather than discovered: the daemon does not sweep the wishlist on its own; Windows has no socket and so no auto-attach; the attached TUI's settings form edits the daemon's folders and rewrites this machine's config.toml; `serve` monitors a daemon's uploads instead of starting a second server; `whoami` reports the daemon's world, with --no-daemon as the way out. The OpenRPC section names the six event methods and the session-wide download.set_dir rule. Site typechecks, lints, and builds through prerender; every claim was verified against the cited code before it was written.
This commit is contained in:
@@ -46,6 +46,8 @@ does, how to install it, and every `config.toml` setting.
|
|||||||
- **TUI and CLI**: a full terminal interface, plus one-shot commands for
|
- **TUI and CLI**: a full terminal interface, plus one-shot commands for
|
||||||
scripts and agents: every feature reachable without a terminal, stdout
|
scripts and agents: every feature reachable without a terminal, stdout
|
||||||
carrying records, the exit code saying what happened
|
carrying records, the exit code saying what happened
|
||||||
|
- **Daemon mode**: one login several clients share, on this machine or over
|
||||||
|
the network, so a download outlives the command that queued it
|
||||||
|
|
||||||
## Project Goals
|
## Project Goals
|
||||||
|
|
||||||
@@ -56,10 +58,6 @@ programming and reverse engineering.
|
|||||||
The library stays lean on dependencies and has none today; the client takes
|
The library stays lean on dependencies and has none today; the client takes
|
||||||
them freely.
|
them freely.
|
||||||
|
|
||||||
## Planned Features
|
|
||||||
|
|
||||||
- [x] Headless daemon mode with remote control
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
A Cargo workspace with two crates:
|
A Cargo workspace with two crates:
|
||||||
@@ -137,11 +135,19 @@ attached. If you drive this from a script or an agent, start one.
|
|||||||
|
|
||||||
Commands find it by themselves. It listens on a Unix socket only your user can
|
Commands find it by themselves. It listens on a Unix socket only your user can
|
||||||
open, so there is nothing to configure and no secret to pass around. Docker and
|
open, so there is nothing to configure and no secret to pass around. Docker and
|
||||||
ssh-agent make the same arrangement. With no daemon running, everything
|
ssh-agent make the same arrangement. The socket is `daemon.sock` in the state
|
||||||
behaves exactly as it did before; `--no-daemon` forces that for one run.
|
directory (`~/.local/share/soulseek-rs/state`, or wherever `SOULSEEK_STATE_DIR`
|
||||||
|
points), with the token remote clients need beside it in `daemon.token`. With
|
||||||
|
no daemon running, everything behaves exactly as it did before; `--no-daemon`
|
||||||
|
forces that for one run.
|
||||||
|
|
||||||
|
Windows has no Unix socket. A daemon there needs `--bind ADDR`, and every
|
||||||
|
client needs `--daemon ADDR` and the token even on the same machine.
|
||||||
|
|
||||||
From a script, start it and *wait* for it: it takes a second or two to log in,
|
From a script, start it and *wait* for it: it takes a second or two to log in,
|
||||||
and a command that arrives early opens a session of its own instead.
|
and the socket is there before the login finishes. A command that arrives early
|
||||||
|
attaches to it and then waits on the handshake, up to 30 seconds. Poll
|
||||||
|
`daemon status` until it answers first.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
soulseek-rs daemon status --json >/dev/null 2>&1 || {
|
soulseek-rs daemon status --json >/dev/null 2>&1 || {
|
||||||
@@ -163,15 +169,29 @@ second TUI and it shows the searches and transfers already running, including
|
|||||||
ones the first started; queue something in either and both see it. The status
|
ones the first started; queue something in either and both see it. The status
|
||||||
bar names the daemon it is attached to, so it is obvious which you are looking
|
bar names the daemon it is attached to, so it is obvious which you are looking
|
||||||
at. Closing a window leaves its downloads running, because they belong to the
|
at. Closing a window leaves its downloads running, because they belong to the
|
||||||
daemon.
|
daemon. The settings popup edits the daemon's folders while you are attached:
|
||||||
|
the paths are the daemon's, they are validated there, and nothing is created on
|
||||||
|
this machine. Applying them also writes those paths into this machine's
|
||||||
|
`config.toml`, which becomes your local download folder the next time you run
|
||||||
|
without a daemon.
|
||||||
|
|
||||||
Everything that touches the network goes through it. `config`, `skills`,
|
Everything that touches the network goes through it. `config`, `skills`,
|
||||||
`completions` and `portmap` stay local, because they are about this machine
|
`completions` and `portmap` stay local, because they are about this machine
|
||||||
rather than a session. `shares add`/`remove` and `config set download_dir` do
|
rather than a session. `shares add`/`remove`, `config set download_dir` and
|
||||||
both: they write the config file *and* update a running daemon, so a folder is
|
`config set shared_dirs` do both: they write the config file *and* update a
|
||||||
served — or downloaded into — straight away rather than at the daemon's next
|
running daemon, so a folder is served — or downloaded into — straight away
|
||||||
start. Files land on the *daemon's* filesystem, and `daemon status` reports
|
rather than at the daemon's next start. Files land on the *daemon's*
|
||||||
where.
|
filesystem, and `daemon status` reports where.
|
||||||
|
|
||||||
|
It does not sweep the wishlist. Standing searches re-run inside
|
||||||
|
`serve --wishlist` or a one-off `wish run`, both of which use the daemon's
|
||||||
|
session when one is up, so put either on a schedule if you want the list worked
|
||||||
|
through. With a remote daemon the list itself lives in the client's
|
||||||
|
`config.toml`, so `wish run` from the laptop drives the daemon against the
|
||||||
|
laptop's wishes.
|
||||||
|
|
||||||
|
`soulseek-rs daemon --upload-slots N` sizes the upload queue for the session it
|
||||||
|
holds: 1 to 1000, where `serve` stops at 64.
|
||||||
|
|
||||||
#### A download box you drive from your laptop
|
#### A download box you drive from your laptop
|
||||||
|
|
||||||
@@ -184,11 +204,17 @@ network:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
soulseek-rs daemon --bind 0.0.0.0:5030
|
soulseek-rs daemon --bind 0.0.0.0:5030
|
||||||
soulseek-rs daemon token # copy this, you need it once
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
It stays in the foreground and prints the token on stderr as it starts, with
|
||||||
|
the two `config set` lines to paste on the other machine. `daemon token` prints
|
||||||
|
it again, and always *this* machine's token, so run it on the box rather than
|
||||||
|
on the laptop.
|
||||||
|
|
||||||
It needs no terminal and no one logged in; hand it to systemd or launchd and
|
It needs no terminal and no one logged in; hand it to systemd or launchd and
|
||||||
forget about it.
|
forget about it. Run the unit as the user who types the commands, or export the
|
||||||
|
same `SOULSEEK_STATE_DIR` in both, because a client looks for the socket under
|
||||||
|
its own state directory and finds nothing under someone else's.
|
||||||
|
|
||||||
**On your laptop**, say once where it lives:
|
**On your laptop**, say once where it lives:
|
||||||
|
|
||||||
@@ -206,12 +232,26 @@ soulseek-rs get 'selected ambient works'
|
|||||||
soulseek-rs daemon status # what it's doing right now
|
soulseek-rs daemon status # what it's doing right now
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`shares add` resolves the folder against the filesystem of the machine you type
|
||||||
|
it on, so `shares add /srv/music` from the laptop exits 2 unless the laptop has
|
||||||
|
that path too. Add the box's folders on the box, or over `ssh`.
|
||||||
|
|
||||||
|
A share list or download folder pushed to a remote daemon changes its live
|
||||||
|
session, and the config file the command wrote is the laptop's. The daemon
|
||||||
|
reads its own `config.toml` at the next start, so put the change there as well,
|
||||||
|
or push it again after a restart.
|
||||||
|
|
||||||
Then close the lid. **The downloads keep going**, because they belong to the
|
Then close the lid. **The downloads keep going**, because they belong to the
|
||||||
daemon, not to the command that asked for them or to the laptop that ran it.
|
daemon, not to the command that asked for them or to the laptop that ran it.
|
||||||
Come back tomorrow, run `soulseek-rs daemon status` again, and pick up where
|
Come back tomorrow, run `soulseek-rs daemon status` again, and pick up where
|
||||||
you left off. The files are on the download box, which is where you wanted
|
you left off. The files are on the download box, which is where you wanted
|
||||||
them.
|
them.
|
||||||
|
|
||||||
|
A transfer outlives the command being killed and the laptop going away, but not
|
||||||
|
the command's own deadline. A `download` or `get` that reaches `--timeout`
|
||||||
|
(300s by default) tells the daemon to drop the transfer before it exits 5.
|
||||||
|
Raise `--timeout` for a big file, or queue it from the attached TUI and quit.
|
||||||
|
|
||||||
When you are finished with it:
|
When you are finished with it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -241,6 +281,11 @@ rooms, private messages and shares are all on the same interface this CLI
|
|||||||
uses. There is no private back channel it keeps for itself, and live updates
|
uses. There is no private back channel it keeps for itself, and live updates
|
||||||
are pushed to every connected client rather than polled for.
|
are pushed to every connected client rather than polled for.
|
||||||
|
|
||||||
|
The handshake refuses a client and a daemon on different protocol versions, so
|
||||||
|
keep both ends on the same release. A destination is session-wide:
|
||||||
|
`download.set_dir` moves it for every transfer, and `download.start` ignores
|
||||||
|
any directory the caller names.
|
||||||
|
|
||||||
[`docs/daemon-protocol.md`](docs/daemon-protocol.md) covers the parts a schema
|
[`docs/daemon-protocol.md`](docs/daemon-protocol.md) covers the parts a schema
|
||||||
cannot state: the framing, the handshake, and how pushed events behave.
|
cannot state: the framing, the handshake, and how pushed events behave.
|
||||||
|
|
||||||
@@ -254,16 +299,16 @@ The one-shot commands follow three rules, so they compose with other tools:
|
|||||||
records are tab-separated fields with no header and no decoration.
|
records are tab-separated fields with no header and no decoration.
|
||||||
- **The exit code is the verdict.** No output plus exit 0 never means failure.
|
- **The exit code is the verdict.** No output plus exit 0 never means failure.
|
||||||
|
|
||||||
| Code | Meaning |
|
| Code | Meaning |
|
||||||
| ---- | ---------------------------------------------------------- |
|
| ---- | ----------------------------------------------------------------- |
|
||||||
| 0 | success |
|
| 0 | success |
|
||||||
| 1 | unexpected error |
|
| 1 | unexpected error |
|
||||||
| 2 | bad arguments, missing credentials, unusable configuration |
|
| 2 | bad arguments, missing credentials, unusable configuration |
|
||||||
| 3 | could not reach the server, or the login was rejected |
|
| 3 | could not reach the server or a daemon, or the login was rejected |
|
||||||
| 4 | the command worked but found nothing |
|
| 4 | the command worked but found nothing |
|
||||||
| 5 | timed out waiting for a response or a transfer |
|
| 5 | timed out waiting for a response or a transfer |
|
||||||
| 6 | a transfer started but did not finish |
|
| 6 | a transfer started but did not finish |
|
||||||
| 7 | the session ended mid-command; nothing it saw is reliable |
|
| 7 | the session ended mid-command; nothing it saw is reliable |
|
||||||
|
|
||||||
#### Commands
|
#### Commands
|
||||||
|
|
||||||
@@ -282,6 +327,8 @@ soulseek-rs message send <USER> <TEXT> # send a private message
|
|||||||
soulseek-rs message read # stream incoming private messages
|
soulseek-rs message read # stream incoming private messages
|
||||||
soulseek-rs room users <ROOM> # who is in a room
|
soulseek-rs room users <ROOM> # who is in a room
|
||||||
soulseek-rs serve [--follow] # stay online sharing, stream uploads
|
soulseek-rs serve [--follow] # stay online sharing, stream uploads
|
||||||
|
soulseek-rs daemon [--bind ADDR] # run as a service others share
|
||||||
|
soulseek-rs daemon token|status|stop # control a running one
|
||||||
soulseek-rs whoami # confirm credentials and connection
|
soulseek-rs whoami # confirm credentials and connection
|
||||||
soulseek-rs user <NAME> # a peer's status and share counts
|
soulseek-rs user <NAME> # a peer's status and share counts
|
||||||
soulseek-rs shares list|add|remove|status|reindex
|
soulseek-rs shares list|add|remove|status|reindex
|
||||||
@@ -295,17 +342,27 @@ soulseek-rs completions install|uninstall # tab completion for bash/zsh/fish
|
|||||||
first: the config commands need no account at all, and `whoami` answers
|
first: the config commands need no account at all, and `whoami` answers
|
||||||
"are these credentials good and what am I offering" in one call.
|
"are these credentials good and what am I offering" in one call.
|
||||||
|
|
||||||
Thirteen of these need no credentials, because they never touch the network:
|
With a daemon attached, every command reports the daemon's world. The run
|
||||||
`config path|list|get|set`, `shares list|add|remove`, `portmap`,
|
borrows its session, so `--username` and `--password` go unread and `whoami`
|
||||||
`skills install|uninstall|list` and `completions install|uninstall`.
|
names the daemon's account, server and download folder; pass `--no-daemon` when
|
||||||
`shares status` and `shares reindex` are not among them: they report what the
|
you want to test this machine's own login. `shares list` stays local either
|
||||||
network will see, which means logging in and scanning the folders.
|
way, because it reads the config file, while `shares status` and
|
||||||
|
`shares reindex` ask the session and so report what peers can see.
|
||||||
|
|
||||||
Every command emits records except the four that perform an action.
|
Nineteen of these need no credentials: `config path|list|get|set`,
|
||||||
`room say`, `message send`, `shares add` and `shares remove` say nothing on
|
`shares list|add|remove`, `wish add|remove|list`, `portmap`,
|
||||||
stdout and answer with their exit code alone. `room listen`, `message read`
|
`skills install|uninstall|list`, `completions install|uninstall` and
|
||||||
and `serve` stream records as they arrive, until `--duration` seconds pass or,
|
`daemon token|status|stop`. Most never touch the network at all;
|
||||||
with `--follow`, until they are interrupted.
|
`daemon status` and `daemon stop` talk to a daemon that is already logged in,
|
||||||
|
so a machine with no Soulseek account of its own can still ask whether one is
|
||||||
|
running. `shares status` and `shares reindex` are not among them: they report
|
||||||
|
what the network will see, which means logging in and scanning the folders.
|
||||||
|
|
||||||
|
Every command emits records except the six that perform an action.
|
||||||
|
`room say`, `message send`, `shares add`, `shares remove`, `wish remove` and
|
||||||
|
`daemon stop` say nothing on stdout and answer with their exit code alone.
|
||||||
|
`room listen`, `message read` and `serve` stream records as they arrive, until
|
||||||
|
`--duration` seconds pass or, with `--follow`, until they are interrupted.
|
||||||
|
|
||||||
#### Record shapes
|
#### Record shapes
|
||||||
|
|
||||||
@@ -322,6 +379,8 @@ room users user
|
|||||||
room listen type room user message
|
room listen type room user message
|
||||||
message read timestamp user message
|
message read timestamp user message
|
||||||
serve status user sent/size path
|
serve status user sent/size path
|
||||||
|
daemon status user server clients uptime-secs
|
||||||
|
daemon token token
|
||||||
user user status average-speed shared-files
|
user user status average-speed shared-files
|
||||||
whoami user server shared-folders shared-files
|
whoami user server shared-folders shared-files
|
||||||
shares list ok|missing directory
|
shares list ok|missing directory
|
||||||
@@ -331,18 +390,21 @@ portmap ok|failed backend external
|
|||||||
```
|
```
|
||||||
|
|
||||||
`room listen`'s `type` is `message`, `join` or `leave`; a join or leave leaves
|
`room listen`'s `type` is `message`, `join` or `leave`; a join or leave leaves
|
||||||
the message field empty. `serve`'s `status` is `uploading`, `completed`,
|
the message field empty. `serve`'s `status` is `queued`, `uploading`,
|
||||||
`cancelled` or `failed`. `user`'s `status` is `online`, `away` or `offline`,
|
`completed`, `cancelled` or `failed`. `user`'s `status` is `online`, `away` or
|
||||||
and any field the server did not answer prints as `-`.
|
`offline`, and any field the server did not answer prints as `-`.
|
||||||
|
|
||||||
`--json` carries more than text does: `search` adds `duration`, `slots`,
|
`--json` carries more than text does: `search` adds `duration`, `slots`,
|
||||||
`speed` and `free_slot`; `browse` adds `directory`; `download` adds the `user`,
|
`speed` and `free_slot`; `browse` adds `directory`; `download` adds the `user`,
|
||||||
remote `path` and `size` alongside the local `file`; `whoami` adds `listening`,
|
remote `path` and `size` alongside the local `file`; `whoami` adds `listening`,
|
||||||
`listen_port` and `download_dir`; `user` adds `privileged` and
|
`listen_port`, `download_dir` and `privilege_seconds`; `user` adds `privileged`
|
||||||
`shared_folders`; `shares status` adds the `directories` array; `serve` adds
|
and `shared_folders`; `shares status` adds the `directories` array; `serve`
|
||||||
`bytes_sent`, `size`, `speed` and a `reason` on failure; `portmap` adds
|
adds `bytes_sent`, `size`, `speed` and a `reason` carrying the queue place or
|
||||||
`port`. Fields the server never answered are `null` rather than absent, so a
|
the failure; `daemon status` adds `version`, `protocol`, `listening`,
|
||||||
missing reply cannot be misread as a zero.
|
`listen_port`, `shared_folders`, `shared_files`, `download_dir` and
|
||||||
|
`session_loss` (`displaced`, `disconnected`, or `null` while the session
|
||||||
|
holds); `portmap` adds `port`. Fields the server never answered are `null`
|
||||||
|
rather than absent, so a missing reply cannot be misread as a zero.
|
||||||
|
|
||||||
`download --stdin` reads back either shape it emits: a JSON object per line,
|
`download --stdin` reads back either shape it emits: a JSON object per line,
|
||||||
or tab-separated text whose first field is the user and last field is the
|
or tab-separated text whose first field is the user and last field is the
|
||||||
@@ -455,8 +517,9 @@ to act on one shell rather than the ones detected.
|
|||||||
`serve` is the mode that makes this client a source: it stays logged in, keeps
|
`serve` is the mode that makes this client a source: it stays logged in, keeps
|
||||||
the listener and the share index alive, answers searches and browse requests
|
the listener and the share index alive, answers searches and browse requests
|
||||||
from the network, and prints one record per upload as it changes state
|
from the network, and prints one record per upload as it changes state
|
||||||
(`uploading`, `completed`, `cancelled`, `failed`). It ends after `--duration`
|
(`queued`, `uploading`, `completed`, `cancelled`, `failed`). It ends after
|
||||||
seconds (an hour by default), or runs until interrupted with `--follow`.
|
`--duration` seconds (an hour by default), or runs until interrupted with
|
||||||
|
`--follow`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
soulseek-rs shares add ~/Music # remembered in config.toml
|
soulseek-rs shares add ~/Music # remembered in config.toml
|
||||||
@@ -470,6 +533,11 @@ short-lived with nothing to serve, so there is no `uploads list`. Managing
|
|||||||
transfers across commands (pause, resume, a queue that outlives one
|
transfers across commands (pause, resume, a queue that outlives one
|
||||||
invocation) is what [daemon mode](#daemon-mode) is for.
|
invocation) is what [daemon mode](#daemon-mode) is for.
|
||||||
|
|
||||||
|
With a daemon running, `serve` starts no second server. It attaches and streams
|
||||||
|
the daemon's uploads, `--upload-slots` re-sizes the daemon's queue, and the
|
||||||
|
local "nothing to share" and "needs the listener" checks are skipped, because
|
||||||
|
the daemon's shares and listener are the ones peers reach.
|
||||||
|
|
||||||
Downloads run under a deadline (`--timeout`, 300s by default) and
|
Downloads run under a deadline (`--timeout`, 300s by default) and
|
||||||
`--max-concurrent-downloads` at a time, so an unattended run always ends.
|
`--max-concurrent-downloads` at a time, so an unattended run always ends.
|
||||||
|
|
||||||
@@ -496,6 +564,9 @@ file, in that order of precedence. Flags work before or after the subcommand.
|
|||||||
| `--no-listener` / `--listener` | `SOULSEEK_NO_LISTENER` | `disable_listener` | listener on |
|
| `--no-listener` / `--listener` | `SOULSEEK_NO_LISTENER` | `disable_listener` | listener on |
|
||||||
| `--max-concurrent-downloads` | `SOULSEEK_MAX_CONCURRENT_DOWNLOADS` | `max_concurrent_downloads` | `20` |
|
| `--max-concurrent-downloads` | `SOULSEEK_MAX_CONCURRENT_DOWNLOADS` | `max_concurrent_downloads` | `20` |
|
||||||
| `--search-timeout` | `SOULSEEK_SEARCH_TIMEOUT` | `search_timeout` | `10` |
|
| `--search-timeout` | `SOULSEEK_SEARCH_TIMEOUT` | `search_timeout` | `10` |
|
||||||
|
| `--daemon ADDR` | `SOULSEEK_DAEMON` | `daemon` | local socket if one is up |
|
||||||
|
| `--daemon-token` | `SOULSEEK_DAEMON_TOKEN` | `daemon_token` | unset (TCP only) |
|
||||||
|
| `--no-daemon` | — | — | attach if one is running |
|
||||||
| `--log-file` | `SOULSEEK_LOG_FILE` | not a config key | stderr |
|
| `--log-file` | `SOULSEEK_LOG_FILE` | not a config key | stderr |
|
||||||
|
|
||||||
Boolean environment variables accept the usual `1`/`0`/`true`/`false`/`yes`/`no`.
|
Boolean environment variables accept the usual `1`/`0`/`true`/`false`/`yes`/`no`.
|
||||||
@@ -507,10 +578,11 @@ config and state directories wholesale. Every variable above is also read from
|
|||||||
a `.env` file in the working directory, which is often the tidiest way to hand
|
a `.env` file in the working directory, which is often the tidiest way to hand
|
||||||
a container its credentials.
|
a container its credentials.
|
||||||
|
|
||||||
`config get` and `config set` cover the nine settings the file can hold
|
`config get` and `config set` cover the eleven settings the file can hold
|
||||||
(`username`, `server`, `listener_port`, `disable_listener`, `download_dir`,
|
(`username`, `server`, `listener_port`, `disable_listener`, `download_dir`,
|
||||||
`shared_dirs`, `max_concurrent_downloads`, `search_timeout`, `password_cmd`),
|
`shared_dirs`, `max_concurrent_downloads`, `search_timeout`, `password_cmd`,
|
||||||
and name the lot back at you when you ask for something else. Setting a key to
|
`daemon`, `daemon_token`), and name the lot back at you when you ask for
|
||||||
|
something else. Setting a key to
|
||||||
an empty string clears it, and `shared_dirs` takes a comma-separated list.
|
an empty string clears it, and `shared_dirs` takes a comma-separated list.
|
||||||
Every wait expressed in seconds (`--search-timeout`, each `--timeout`, each
|
Every wait expressed in seconds (`--search-timeout`, each `--timeout`, each
|
||||||
`--duration`) is bounded to one day, so a mistyped flag is rejected rather than
|
`--duration`) is bounded to one day, so a mistyped flag is rejected rather than
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ const ALTERNATIVES: readonly Alternative[] = [
|
|||||||
question: 'Want a queue that outlives the command?',
|
question: 'Want a queue that outlives the command?',
|
||||||
answer: (
|
answer: (
|
||||||
<>
|
<>
|
||||||
Every command runs one-shot and exits: JSON on stdout, an exit code, nothing left
|
<Code>soulseek-rs daemon</Code> holds one: pause, resume and remove act on it from
|
||||||
running. No resident queue to pause, resume, or drive remotely. slskd has that.
|
the TUI or over JSON-RPC, and <Code>--bind</Code> lets another machine drive it.
|
||||||
|
slskd still has the web interface and accounts for several people.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
links: [{ label: 'slskd', href: LINKS.slskd }],
|
links: [{ label: 'slskd', href: LINKS.slskd }],
|
||||||
|
|||||||
+63
-12
@@ -89,7 +89,9 @@ const FirstRun = () => (
|
|||||||
<Prose>
|
<Prose>
|
||||||
<p>
|
<p>
|
||||||
On first run you get a login screen: sign in with an existing Soulseek account or
|
On first run you get a login screen: sign in with an existing Soulseek account or
|
||||||
register a new one. Your password goes to the OS keychain, not a plain-text file.
|
register a new one, unless a daemon is already running, in which case the TUI
|
||||||
|
attaches to it and asks for nothing. Your password goes to the OS keychain, not a
|
||||||
|
plain-text file.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Downloads land in a conventional folder, and TUI state (searches, downloads,
|
Downloads land in a conventional folder, and TUI state (searches, downloads,
|
||||||
@@ -158,6 +160,8 @@ const FromTheShell = () => (
|
|||||||
{ t: 'cm', text: '# search, pick the best, download it' },
|
{ t: 'cm', text: '# search, pick the best, download it' },
|
||||||
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --min-bitrate 320' },
|
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --min-bitrate 320' },
|
||||||
{ t: 'cmd', text: 'soulseek-rs browse <username> --json' },
|
{ t: 'cmd', text: 'soulseek-rs browse <username> --json' },
|
||||||
|
{ t: 'cmd', text: 'soulseek-rs wish add "rare pressing"' },
|
||||||
|
{ t: 'cm', text: '# a standing search, re-run by serve --wishlist' },
|
||||||
{ t: 'cmd', text: 'soulseek-rs room list' },
|
{ t: 'cmd', text: 'soulseek-rs room list' },
|
||||||
{ t: 'cm', text: '# public rooms, busiest first' },
|
{ t: 'cm', text: '# public rooms, busiest first' },
|
||||||
{ t: 'cmd', text: 'soulseek-rs room say <room> "hello room"' },
|
{ t: 'cmd', text: 'soulseek-rs room say <room> "hello room"' },
|
||||||
@@ -171,16 +175,17 @@ const FromTheShell = () => (
|
|||||||
<Prose>
|
<Prose>
|
||||||
<p>
|
<p>
|
||||||
The command groups: <Code>search</Code>, <Code>get</Code>, <Code>download</Code>,{' '}
|
The command groups: <Code>search</Code>, <Code>get</Code>, <Code>download</Code>,{' '}
|
||||||
<Code>browse</Code>, <Code>room</Code>, <Code>message</Code>, <Code>serve</Code>,{' '}
|
<Code>browse</Code>, <Code>wish</Code>, <Code>room</Code>, <Code>message</Code>,{' '}
|
||||||
<Code>user</Code>, <Code>whoami</Code>, <Code>shares</Code>, <Code>daemon</Code>,{' '}
|
<Code>serve</Code>, <Code>user</Code>, <Code>whoami</Code>, <Code>shares</Code>,{' '}
|
||||||
<Code>config</Code>, <Code>portmap</Code>, <Code>skills</Code>,{' '}
|
<Code>daemon</Code>, <Code>config</Code>, <Code>portmap</Code>,{' '}
|
||||||
<Code>completions</Code>. Records are
|
<Code>skills</Code>, <Code>completions</Code>. Records are tab-separated fields,
|
||||||
tab-separated fields, one per line, or NDJSON with <Code>--json</Code>.
|
one per line, or NDJSON with <Code>--json</Code>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
No TTY required. <Code>search --json</Code> pipes through <Code>jq</Code> into{' '}
|
No TTY required. <Code>search --json</Code> pipes through <Code>jq</Code> into{' '}
|
||||||
<Code>download --stdin</Code>. Every ending has its own exit code: 4 found
|
<Code>download --stdin</Code>. Every ending has its own exit code: 4 found
|
||||||
nothing, 3 could not reach the server, 6 the transfer died.
|
nothing, 3 could not reach the server, 6 the transfer died, 7 another login took
|
||||||
|
the session away, which is the one a daemon spares you.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<Code>completions install</Code> gives bash, zsh and fish tab completion for all
|
<Code>completions install</Code> gives bash, zsh and fish tab completion for all
|
||||||
@@ -238,6 +243,8 @@ const Daemon = () => (
|
|||||||
{ t: 'cm', text: '# once; it holds the session' },
|
{ t: 'cm', text: '# once; it holds the session' },
|
||||||
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --json' },
|
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --json' },
|
||||||
{ t: 'cm', text: '# no login, no flags; it finds the daemon' },
|
{ t: 'cm', text: '# no login, no flags; it finds the daemon' },
|
||||||
|
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --no-daemon' },
|
||||||
|
{ t: 'cm', text: "# opt out; open this run's own session" },
|
||||||
{ t: 'cmd', text: 'soulseek-rs' },
|
{ t: 'cmd', text: 'soulseek-rs' },
|
||||||
{ t: 'cm', text: '# the TUI attaches to the same session' },
|
{ t: 'cm', text: '# the TUI attaches to the same session' },
|
||||||
{ t: 'cmd', text: 'soulseek-rs daemon status' },
|
{ t: 'cmd', text: 'soulseek-rs daemon status' },
|
||||||
@@ -248,13 +255,29 @@ const Daemon = () => (
|
|||||||
<p>
|
<p>
|
||||||
Commands find it themselves: it listens on a Unix socket only your user can
|
Commands find it themselves: it listens on a Unix socket only your user can
|
||||||
open, so there is no address to pass and no token to handle. Without one
|
open, so there is no address to pass and no token to handle. Without one
|
||||||
running, everything behaves exactly as it did before.
|
running, everything behaves exactly as it did before, and{' '}
|
||||||
|
<Code>--no-daemon</Code> gets that back for a single command.{' '}
|
||||||
|
<Code>daemon status</Code> exits 3 when none is running, which is how a script
|
||||||
|
decides whether to start one. That socket is a Unix one: on Windows, start the
|
||||||
|
daemon with <Code>--bind 127.0.0.1:5030</Code> and point commands at it with{' '}
|
||||||
|
<Code>config set daemon</Code> and <Code>config set daemon_token</Code>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Every window is a view of the same session. Open a second TUI and it shows the
|
Every window is a view of the same session. Open a second TUI and it shows the
|
||||||
searches and transfers already running; queue something in either and both see
|
searches and transfers already running; queue something in either and both see
|
||||||
it. The status bar names the daemon, and closing a window leaves its downloads
|
it. There is no login screen, because the daemon is already signed in, and the
|
||||||
going, because they belong to the daemon rather than the window.
|
inbox opens on the private messages it collected while nobody was watching. The
|
||||||
|
settings form edits the daemon’s download folder and share list, so the
|
||||||
|
paths you type there name its filesystem rather than this machine’s. The
|
||||||
|
status bar names the daemon, and closing a window leaves its downloads going,
|
||||||
|
because they belong to the daemon rather than the window.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Attach one and commands report its world: <Code>whoami</Code> names its account
|
||||||
|
and server, <Code>download</Code> prints the path on its disk, and{' '}
|
||||||
|
<Code>serve --follow</Code> watches its uploads instead of starting a second
|
||||||
|
server. It holds the session rather than the wishlist: standing searches re-run
|
||||||
|
under <Code>serve --wishlist</Code>, or when you call <Code>wish run</Code>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Why bother: the server permits a single session per account. Two one-off
|
Why bother: the server permits a single session per account. Two one-off
|
||||||
@@ -305,6 +328,12 @@ const DownloadBox = () => (
|
|||||||
tomorrow, run <Code>daemon status</Code>, and pick up where you left off. The
|
tomorrow, run <Code>daemon status</Code>, and pick up where you left off. The
|
||||||
files are on the box, which is where you wanted them.
|
files are on the box, which is where you wanted them.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
The two <Code>config set</Code> lines are the one-time version;{' '}
|
||||||
|
<Code>--daemon ADDR</Code> and <Code>SOULSEEK_DAEMON</Code> point a single
|
||||||
|
command at a different box. On the box itself,{' '}
|
||||||
|
<Code>daemon --upload-slots N</Code> sets how many uploads it serves at once.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
There is no encryption on that port. On a home network that is usually fine.
|
There is no encryption on that port. On a home network that is usually fine.
|
||||||
Over the internet, reach it through SSH instead:{' '}
|
Over the internet, reach it through SSH instead:{' '}
|
||||||
@@ -312,6 +341,18 @@ const DownloadBox = () => (
|
|||||||
</p>
|
</p>
|
||||||
</Prose>
|
</Prose>
|
||||||
</Cols>
|
</Cols>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Callout tone="warn" title="pushed settings are live only">
|
||||||
|
<p>
|
||||||
|
<Code>config set download_dir</Code> and <Code>shares add</Code> reach the
|
||||||
|
running daemon and take effect at once, but the box reads its own{' '}
|
||||||
|
<Code>config.toml</Code> when it restarts, so make the lasting changes there.{' '}
|
||||||
|
<Code>shares add</Code> also checks the folder on the machine you type it on, so
|
||||||
|
a path that exists only on the box fails from your laptop: add shares on the box,
|
||||||
|
or in the attached TUI’s settings form, which sends paths through as typed.
|
||||||
|
</p>
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -327,7 +368,12 @@ const OpenSchema = () => (
|
|||||||
It speaks newline-delimited JSON-RPC 2.0, described by an{' '}
|
It speaks newline-delimited JSON-RPC 2.0, described by an{' '}
|
||||||
<ExtLink href="https://open-rpc.org">OpenRPC</ExtLink> document. The daemon
|
<ExtLink href="https://open-rpc.org">OpenRPC</ExtLink> document. The daemon
|
||||||
serves that document itself through <Code>rpc.discover</Code>, so a client can
|
serves that document itself through <Code>rpc.discover</Code>, so a client can
|
||||||
ask a running daemon what it can do.
|
ask a running daemon what it can do. The same document is checked in at{' '}
|
||||||
|
<ExtLink href={`${LINKS.gh}/blob/master/docs/openrpc.json`}>
|
||||||
|
docs/openrpc.json
|
||||||
|
</ExtLink>{' '}
|
||||||
|
on GitHub. <Code>auth</Code> answers with the protocol version, 1 today, so a
|
||||||
|
client written against an older shape learns it at connect time.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
OpenRPC is the JSON-RPC counterpart to OpenAPI: point a generator at it and you
|
OpenRPC is the JSON-RPC counterpart to OpenAPI: point a generator at it and you
|
||||||
@@ -338,7 +384,12 @@ const OpenSchema = () => (
|
|||||||
<p>
|
<p>
|
||||||
Searches, transfers, rooms, private messages and shares are all on the same
|
Searches, transfers, rooms, private messages and shares are all on the same
|
||||||
interface this CLI uses. There is no private back channel it keeps for itself,
|
interface this CLI uses. There is no private back channel it keeps for itself,
|
||||||
and live updates are pushed to every connected client rather than polled for.
|
and live updates are pushed to every connected client rather than polled for:{' '}
|
||||||
|
<Code>event.room</Code>, <Code>event.message</Code>, <Code>event.upload</Code>,{' '}
|
||||||
|
<Code>event.download_status</Code>, <Code>event.browse</Code> and{' '}
|
||||||
|
<Code>event.session_loss</Code>. One rule to write against: the download folder
|
||||||
|
belongs to the daemon, so <Code>download.set_dir</Code> moves it for every
|
||||||
|
transfer and a per-download destination is ignored.
|
||||||
</p>
|
</p>
|
||||||
</Prose>
|
</Prose>
|
||||||
<Terminal
|
<Terminal
|
||||||
|
|||||||
@@ -232,8 +232,10 @@ const About = () => (
|
|||||||
<Callout title="agents and automation">
|
<Callout title="agents and automation">
|
||||||
<p>
|
<p>
|
||||||
Every command runs as a one-off from the shell. Point cron, a script, or an
|
Every command runs as a one-off from the shell. Point cron, a script, or an
|
||||||
agent at them and read JSON records back off stdout. No daemon, no API to stand
|
agent at them and read JSON records back off stdout. Nothing to stand up first;{' '}
|
||||||
up. <Code>skills install</Code> teaches the agent the surface for you.{' '}
|
<Code>soulseek-rs daemon</Code> is one command when a batch of them should share
|
||||||
|
a single login, and the agent skill starts one for you.{' '}
|
||||||
|
<Code>skills install</Code> teaches the agent the surface.{' '}
|
||||||
<Link to="/docs" className="text-link hover:text-link-hover">
|
<Link to="/docs" className="text-link hover:text-link-hover">
|
||||||
See the commands
|
See the commands
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ const PLATFORMS: readonly PlatformNote[] = [
|
|||||||
config: '%APPDATA%\\soulseek-rs\\config\\config.toml',
|
config: '%APPDATA%\\soulseek-rs\\config\\config.toml',
|
||||||
state: '%APPDATA%\\soulseek-rs\\data\\state',
|
state: '%APPDATA%\\soulseek-rs\\data\\state',
|
||||||
downloads: '%USERPROFILE%\\Downloads\\Soulseek',
|
downloads: '%USERPROFILE%\\Downloads\\Soulseek',
|
||||||
note: 'Uses the standard known-folder locations for roaming app data.',
|
note: 'Uses the standard known-folder locations for roaming app data. Without Unix sockets, a daemon here needs --bind 127.0.0.1:5030, and clients reach it with config set daemon plus config set daemon_token; that token file carries no permissions of its own.',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -207,6 +207,26 @@ const SETTINGS: readonly Setting[] = [
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'daemon',
|
||||||
|
fallback: 'the local socket, if one is running',
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
A daemon to drive instead of logging in here, as <code>host:port</code> or a Unix
|
||||||
|
socket path.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'daemon_token',
|
||||||
|
fallback: 'unset',
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
The secret a remote daemon asks for, printed by <code>daemon token</code> on the
|
||||||
|
machine running it. A local socket needs none.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const CONFIG_EXAMPLE: TermLine[] = [
|
const CONFIG_EXAMPLE: TermLine[] = [
|
||||||
@@ -431,14 +451,17 @@ const Platforms = () => (
|
|||||||
<code>config.toml</code> is layered: flags beat environment variables, which
|
<code>config.toml</code> is layered: flags beat environment variables, which
|
||||||
beat the file, which beats the defaults. State (searches, downloads, rooms) is
|
beat the file, which beats the defaults. State (searches, downloads, rooms) is
|
||||||
versioned JSON and restores on restart; soulseek-rs sets anything unreadable
|
versioned JSON and restores on restart; soulseek-rs sets anything unreadable
|
||||||
aside as <code>.bak</code> rather than overwriting it.
|
aside as <code>.bak</code> rather than overwriting it. Attached to a daemon, that
|
||||||
|
state is the daemon’s, and the state folder also holds its{' '}
|
||||||
|
<code>daemon.sock</code> and <code>daemon.token</code>, both 0600 on macOS and
|
||||||
|
Linux.
|
||||||
</FeatureCard>
|
</FeatureCard>
|
||||||
</Cols>
|
</Cols>
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
const Config = () => (
|
const Config = () => (
|
||||||
<Section id="config">
|
<Section id="config">
|
||||||
<SectionHead eyebrow="config.toml" title="Nine settings, all optional.">
|
<SectionHead eyebrow="config.toml" title="Eleven settings, all optional.">
|
||||||
Every one has a working default, so an empty file, or no file at all, is valid.{' '}
|
Every one has a working default, so an empty file, or no file at all, is valid.{' '}
|
||||||
<Code>config list</Code> prints the effective value of each.
|
<Code>config list</Code> prints the effective value of each.
|
||||||
</SectionHead>
|
</SectionHead>
|
||||||
@@ -471,7 +494,9 @@ const Config = () => (
|
|||||||
reads one setting and <Code>config set</Code> writes one. It rejects
|
reads one setting and <Code>config set</Code> writes one. It rejects
|
||||||
unknown keys, so a typo fails instead of doing nothing.{' '}
|
unknown keys, so a typo fails instead of doing nothing.{' '}
|
||||||
<Code>--no-config</Code> ignores the file entirely and{' '}
|
<Code>--no-config</Code> ignores the file entirely and{' '}
|
||||||
<Code>--config FILE</Code> points at another one.
|
<Code>--config FILE</Code> points at another one. <Code>wishlist</Code> is in
|
||||||
|
the file too, written by <Code>wish add</Code> rather than{' '}
|
||||||
|
<Code>config set</Code>, since a standing search may itself contain a comma.
|
||||||
</p>
|
</p>
|
||||||
</Callout>
|
</Callout>
|
||||||
</div>
|
</div>
|
||||||
@@ -497,7 +522,8 @@ const Uninstall = () => (
|
|||||||
{ t: 'cmd', text: 'cargo uninstall soulseek-rs' },
|
{ t: 'cmd', text: 'cargo uninstall soulseek-rs' },
|
||||||
{ t: 'cm', text: '# brew only: drop the tap it came from' },
|
{ t: 'cm', text: '# brew only: drop the tap it came from' },
|
||||||
{ t: 'cmd', text: 'brew untap michel/tap' },
|
{ t: 'cmd', text: 'brew untap michel/tap' },
|
||||||
{ t: 'cm', text: '# config and state: searches, downloads, rooms, messages' },
|
{ t: 'cm', text: '# config and state: searches, downloads, rooms,' },
|
||||||
|
{ t: 'cm', text: '# messages, the daemon token' },
|
||||||
{
|
{
|
||||||
t: 'cmd',
|
t: 'cmd',
|
||||||
text: 'rm -rf ~/.config/soulseek-rs ~/.local/share/soulseek-rs',
|
text: 'rm -rf ~/.config/soulseek-rs ~/.local/share/soulseek-rs',
|
||||||
|
|||||||
Reference in New Issue
Block a user