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
|
||||
scripts and agents: every feature reachable without a terminal, stdout
|
||||
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
|
||||
|
||||
@@ -56,10 +58,6 @@ programming and reverse engineering.
|
||||
The library stays lean on dependencies and has none today; the client takes
|
||||
them freely.
|
||||
|
||||
## Planned Features
|
||||
|
||||
- [x] Headless daemon mode with remote control
|
||||
|
||||
## Project Structure
|
||||
|
||||
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
|
||||
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
|
||||
behaves exactly as it did before; `--no-daemon` forces that for one run.
|
||||
ssh-agent make the same arrangement. The socket is `daemon.sock` in the state
|
||||
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,
|
||||
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
|
||||
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
|
||||
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
|
||||
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`,
|
||||
`completions` and `portmap` stay local, because they are about this machine
|
||||
rather than a session. `shares add`/`remove` and `config set download_dir` do
|
||||
both: they write the config file *and* update a running daemon, so a folder is
|
||||
served — or downloaded into — straight away rather than at the daemon's next
|
||||
start. Files land on the *daemon's* filesystem, and `daemon status` reports
|
||||
where.
|
||||
rather than a session. `shares add`/`remove`, `config set download_dir` and
|
||||
`config set shared_dirs` do both: they write the config file *and* update a
|
||||
running daemon, so a folder is served — or downloaded into — straight away
|
||||
rather than at the daemon's next start. Files land on the *daemon's*
|
||||
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
|
||||
|
||||
@@ -184,11 +204,17 @@ network:
|
||||
|
||||
```bash
|
||||
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
|
||||
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:
|
||||
|
||||
@@ -206,12 +232,26 @@ soulseek-rs get 'selected ambient works'
|
||||
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
|
||||
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
|
||||
you left off. The files are on the download box, which is where you wanted
|
||||
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:
|
||||
|
||||
```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
|
||||
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
|
||||
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.
|
||||
- **The exit code is the verdict.** No output plus exit 0 never means failure.
|
||||
|
||||
| Code | Meaning |
|
||||
| ---- | ---------------------------------------------------------- |
|
||||
| 0 | success |
|
||||
| 1 | unexpected error |
|
||||
| 2 | bad arguments, missing credentials, unusable configuration |
|
||||
| 3 | could not reach the server, or the login was rejected |
|
||||
| 4 | the command worked but found nothing |
|
||||
| 5 | timed out waiting for a response or a transfer |
|
||||
| 6 | a transfer started but did not finish |
|
||||
| 7 | the session ended mid-command; nothing it saw is reliable |
|
||||
| Code | Meaning |
|
||||
| ---- | ----------------------------------------------------------------- |
|
||||
| 0 | success |
|
||||
| 1 | unexpected error |
|
||||
| 2 | bad arguments, missing credentials, unusable configuration |
|
||||
| 3 | could not reach the server or a daemon, or the login was rejected |
|
||||
| 4 | the command worked but found nothing |
|
||||
| 5 | timed out waiting for a response or a transfer |
|
||||
| 6 | a transfer started but did not finish |
|
||||
| 7 | the session ended mid-command; nothing it saw is reliable |
|
||||
|
||||
#### 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 room users <ROOM> # who is in a room
|
||||
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 user <NAME> # a peer's status and share counts
|
||||
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
|
||||
"are these credentials good and what am I offering" in one call.
|
||||
|
||||
Thirteen of these need no credentials, because they never touch the network:
|
||||
`config path|list|get|set`, `shares list|add|remove`, `portmap`,
|
||||
`skills install|uninstall|list` and `completions install|uninstall`.
|
||||
`shares status` and `shares reindex` are not among them: they report what the
|
||||
network will see, which means logging in and scanning the folders.
|
||||
With a daemon attached, every command reports the daemon's world. The run
|
||||
borrows its session, so `--username` and `--password` go unread and `whoami`
|
||||
names the daemon's account, server and download folder; pass `--no-daemon` when
|
||||
you want to test this machine's own login. `shares list` stays local either
|
||||
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.
|
||||
`room say`, `message send`, `shares add` and `shares remove` 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.
|
||||
Nineteen of these need no credentials: `config path|list|get|set`,
|
||||
`shares list|add|remove`, `wish add|remove|list`, `portmap`,
|
||||
`skills install|uninstall|list`, `completions install|uninstall` and
|
||||
`daemon token|status|stop`. Most never touch the network at all;
|
||||
`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
|
||||
|
||||
@@ -322,6 +379,8 @@ room users user
|
||||
room listen type room user message
|
||||
message read timestamp user message
|
||||
serve status user sent/size path
|
||||
daemon status user server clients uptime-secs
|
||||
daemon token token
|
||||
user user status average-speed shared-files
|
||||
whoami user server shared-folders shared-files
|
||||
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
|
||||
the message field empty. `serve`'s `status` is `uploading`, `completed`,
|
||||
`cancelled` or `failed`. `user`'s `status` is `online`, `away` or `offline`,
|
||||
and any field the server did not answer prints as `-`.
|
||||
the message field empty. `serve`'s `status` is `queued`, `uploading`,
|
||||
`completed`, `cancelled` or `failed`. `user`'s `status` is `online`, `away` or
|
||||
`offline`, and any field the server did not answer prints as `-`.
|
||||
|
||||
`--json` carries more than text does: `search` adds `duration`, `slots`,
|
||||
`speed` and `free_slot`; `browse` adds `directory`; `download` adds the `user`,
|
||||
remote `path` and `size` alongside the local `file`; `whoami` adds `listening`,
|
||||
`listen_port` and `download_dir`; `user` adds `privileged` and
|
||||
`shared_folders`; `shares status` adds the `directories` array; `serve` adds
|
||||
`bytes_sent`, `size`, `speed` and a `reason` on failure; `portmap` adds
|
||||
`port`. Fields the server never answered are `null` rather than absent, so a
|
||||
missing reply cannot be misread as a zero.
|
||||
`listen_port`, `download_dir` and `privilege_seconds`; `user` adds `privileged`
|
||||
and `shared_folders`; `shares status` adds the `directories` array; `serve`
|
||||
adds `bytes_sent`, `size`, `speed` and a `reason` carrying the queue place or
|
||||
the failure; `daemon status` adds `version`, `protocol`, `listening`,
|
||||
`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,
|
||||
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
|
||||
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
|
||||
(`uploading`, `completed`, `cancelled`, `failed`). It ends after `--duration`
|
||||
seconds (an hour by default), or runs until interrupted with `--follow`.
|
||||
(`queued`, `uploading`, `completed`, `cancelled`, `failed`). It ends after
|
||||
`--duration` seconds (an hour by default), or runs until interrupted with
|
||||
`--follow`.
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
`--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 |
|
||||
| `--max-concurrent-downloads` | `SOULSEEK_MAX_CONCURRENT_DOWNLOADS` | `max_concurrent_downloads` | `20` |
|
||||
| `--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 |
|
||||
|
||||
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 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`,
|
||||
`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
|
||||
`shared_dirs`, `max_concurrent_downloads`, `search_timeout`, `password_cmd`,
|
||||
`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.
|
||||
Every wait expressed in seconds (`--search-timeout`, each `--timeout`, each
|
||||
`--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?',
|
||||
answer: (
|
||||
<>
|
||||
Every command runs one-shot and exits: JSON on stdout, an exit code, nothing left
|
||||
running. No resident queue to pause, resume, or drive remotely. slskd has that.
|
||||
<Code>soulseek-rs daemon</Code> holds one: pause, resume and remove act on it from
|
||||
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 }],
|
||||
|
||||
+63
-12
@@ -89,7 +89,9 @@ const FirstRun = () => (
|
||||
<Prose>
|
||||
<p>
|
||||
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>
|
||||
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: 'cmd', text: 'soulseek-rs search "netlabel 2004" --min-bitrate 320' },
|
||||
{ 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: 'cm', text: '# public rooms, busiest first' },
|
||||
{ t: 'cmd', text: 'soulseek-rs room say <room> "hello room"' },
|
||||
@@ -171,16 +175,17 @@ const FromTheShell = () => (
|
||||
<Prose>
|
||||
<p>
|
||||
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>user</Code>, <Code>whoami</Code>, <Code>shares</Code>, <Code>daemon</Code>,{' '}
|
||||
<Code>config</Code>, <Code>portmap</Code>, <Code>skills</Code>,{' '}
|
||||
<Code>completions</Code>. Records are
|
||||
tab-separated fields, one per line, or NDJSON with <Code>--json</Code>.
|
||||
<Code>browse</Code>, <Code>wish</Code>, <Code>room</Code>, <Code>message</Code>,{' '}
|
||||
<Code>serve</Code>, <Code>user</Code>, <Code>whoami</Code>, <Code>shares</Code>,{' '}
|
||||
<Code>daemon</Code>, <Code>config</Code>, <Code>portmap</Code>,{' '}
|
||||
<Code>skills</Code>, <Code>completions</Code>. Records are tab-separated fields,
|
||||
one per line, or NDJSON with <Code>--json</Code>.
|
||||
</p>
|
||||
<p>
|
||||
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
|
||||
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>
|
||||
<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: 'cmd', text: 'soulseek-rs search "netlabel 2004" --json' },
|
||||
{ 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: 'cm', text: '# the TUI attaches to the same session' },
|
||||
{ t: 'cmd', text: 'soulseek-rs daemon status' },
|
||||
@@ -248,13 +255,29 @@ const Daemon = () => (
|
||||
<p>
|
||||
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
|
||||
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>
|
||||
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
|
||||
it. The status bar names the daemon, and closing a window leaves its downloads
|
||||
going, because they belong to the daemon rather than the window.
|
||||
it. There is no login screen, because the daemon is already signed in, and the
|
||||
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>
|
||||
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
|
||||
files are on the box, which is where you wanted them.
|
||||
</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>
|
||||
There is no encryption on that port. On a home network that is usually fine.
|
||||
Over the internet, reach it through SSH instead:{' '}
|
||||
@@ -312,6 +341,18 @@ const DownloadBox = () => (
|
||||
</p>
|
||||
</Prose>
|
||||
</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>
|
||||
)
|
||||
|
||||
@@ -327,7 +368,12 @@ const OpenSchema = () => (
|
||||
It speaks newline-delimited JSON-RPC 2.0, described by an{' '}
|
||||
<ExtLink href="https://open-rpc.org">OpenRPC</ExtLink> document. The daemon
|
||||
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>
|
||||
OpenRPC is the JSON-RPC counterpart to OpenAPI: point a generator at it and you
|
||||
@@ -338,7 +384,12 @@ const OpenSchema = () => (
|
||||
<p>
|
||||
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,
|
||||
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>
|
||||
</Prose>
|
||||
<Terminal
|
||||
|
||||
@@ -232,8 +232,10 @@ const About = () => (
|
||||
<Callout title="agents and automation">
|
||||
<p>
|
||||
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
|
||||
up. <Code>skills install</Code> teaches the agent the surface for you.{' '}
|
||||
agent at them and read JSON records back off stdout. Nothing to stand up first;{' '}
|
||||
<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">
|
||||
See the commands
|
||||
</Link>
|
||||
|
||||
@@ -125,7 +125,7 @@ const PLATFORMS: readonly PlatformNote[] = [
|
||||
config: '%APPDATA%\\soulseek-rs\\config\\config.toml',
|
||||
state: '%APPDATA%\\soulseek-rs\\data\\state',
|
||||
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[] = [
|
||||
@@ -431,14 +451,17 @@ const Platforms = () => (
|
||||
<code>config.toml</code> is layered: flags beat environment variables, which
|
||||
beat the file, which beats the defaults. State (searches, downloads, rooms) is
|
||||
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>
|
||||
</Cols>
|
||||
</Section>
|
||||
)
|
||||
const 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.{' '}
|
||||
<Code>config list</Code> prints the effective value of each.
|
||||
</SectionHead>
|
||||
@@ -471,7 +494,9 @@ const Config = () => (
|
||||
reads one setting and <Code>config set</Code> writes one. It rejects
|
||||
unknown keys, so a typo fails instead of doing nothing.{' '}
|
||||
<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>
|
||||
</Callout>
|
||||
</div>
|
||||
@@ -497,7 +522,8 @@ const Uninstall = () => (
|
||||
{ t: 'cmd', text: 'cargo uninstall soulseek-rs' },
|
||||
{ t: 'cm', text: '# brew only: drop the tap it came from' },
|
||||
{ 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',
|
||||
text: 'rm -rf ~/.config/soulseek-rs ~/.local/share/soulseek-rs',
|
||||
|
||||
Reference in New Issue
Block a user