crabidy/README.md

479 lines
20 KiB
Markdown

# crabidy
A client/server music player. A headless gRPC server owns the library,
the play queue, and audio output; a terminal UI connects to it over
localhost (or the network). Media comes from pluggable **providers**,
each mounted as a subtree of one library:
```text
/
├── crabidy your saves: queues, bookmarks (`w`), and captures (`W`),
│ managed by the server
├── abs audiobookshelf audiobooks (absdy/README.md)
├── fs a local music folder (fsdy/README.md)
├── fyyd podcast search (fyyd/README.md)
├── jamendo Creative-Commons music (jamendody/README.md)
├── rss podcast subscriptions (rssdy/README.md)
├── soundcloud SoundCloud (soundclouddy/README.md)
├── tidal Tidal streaming (tidaldy/README.md)
├── youtube YouTube search & playlists (ytdy/README.md)
└── orphans store audio no save references any more — rename, delete,
or queue it
```
Full documentation — every provider, how to log in to each, every config
option, and the architecture — is the **book in [`docs/`](docs/src/)**:
```sh
devenv shell -- docs # serve it locally
```
## Binaries
- `crabidy-server` — the server: providers, queue, playback, gRPC on
`0.0.0.0:50051`. Also serves the web client at that address (see
below).
- `cbd-tui` — the terminal client. Press `?` inside for all key
bindings.
- `cbd` — both in one process: starts the server, waits until it
accepts connections, then runs the TUI. Adopts an already-running
server instead of failing on an occupied port.
- `cbd-web` — the browser client (Leptos/WASM), with the same
functionality as the TUI. Not run directly: it is built to a bundle
and embedded into `crabidy-server` (see
[cbd-web/README.md](cbd-web/README.md)).
## Quick start
The toolchain is managed by [devenv](https://devenv.sh):
```sh
devenv shell # provides rust, yt-dlp, and friends
cargo run -p cbd # server + TUI in one process
```
Or run the halves separately: `cargo run -p crabidy-server` and, in
another terminal, `cargo run -p cbd-tui`.
### Tailored builds
Every provider — plus Opus decoding, the spectrum bars, the embedded web
UI, and the TUI's desktop notifications and MPRIS player — sits behind a
Cargo feature,
all on by default. Drop what you do not need and the dependencies go with
it (`opus-bundled` is the one to drop if you would rather link the system
libopus than compile the vendored copy):
```sh
# a local-files appliance: no network providers, no web UI, no FFT
cargo build --release -p crabidy-server --no-default-features --features fs,opus
```
`crabidy-server features` prints what a binary was built with. See
[docs/src/build-features.md](docs/src/build-features.md) for the full table,
what `fs` takes with it, and more examples.
**Note:** `--no-default-features` on its own drops **every** provider (it
compiles and runs, but plays nothing). Always name what you want.
## Configuration
All configuration lives in `~/.config/crabidy/` (the platform config
directory). Every file is optional; missing providers simply do not
mount. Files are created/rewritten on first start with their defaults
filled in.
| File | Configures | Login |
| --------------------- | ---------------- | ------------------ |
| `abs.toml` | audiobookshelf | URL + API key |
| `fsdy.toml` | local files | — |
| `fyyd.toml` | podcasts | none needed |
| `jamendo.toml` | Jamendo | none (key shipped) |
| `rss.toml` | podcast feeds | the feed URLs |
| `soundcloud.toml` | SoundCloud | optional token |
| `tidaly.toml` | Tidal | device login |
| `ytdy.toml` | YouTube | optional cookies |
| `cbd-tui.toml` | `cbd-tui` | server role + pw |
| `cbd.toml` | `cbd` | server role + pw |
| `crabidy-server.toml` | the server | its `[auth]` hashes|
Each provider's README — linked from the library tree at the top — explains
**how to log in** and documents every option in its file. The same material
is in the book under [`docs/src/providers/`](docs/src/providers/); the client
and server files are covered below.
Provider credentials and the client password are stored in **cleartext**
these are config files, not a keyring. Keep `~/.config/crabidy/` private.
Crabidy redacts secrets from logs, errors, and config dumps.
The server-managed `crabidy` provider does not live under `~/.config`. Its
track-file tree (saved queues, bookmarks, and captures) lives in
`~/.local/state/crabidy/` (the platform *state* directory) and the audio it
captures lives in a single content-addressed store under
`~/.local/share/crabidy/` (the *data* directory), shared and de-duplicated
across saves. Neither needs configuration; see
[docs/src/store.md](docs/src/store.md).
### `cbd-tui.toml` and `cbd.toml`
Client configuration. `cbd-tui` (the standalone terminal client) reads
`cbd-tui.toml`; `cbd` (server + TUI in one process) reads its own
`cbd.toml`. They are **separate files with the same options** so the two
can run side by side on one machine — a common setup is `cbd` playing
locally against its in-process server while `cbd-tui` points at a remote
server (e.g. a Raspberry Pi). A shared file would force one to follow
the other's `address`.
```toml
[server]
# Where to find the server. Default (both files): localhost, which is
# what cbd's own in-process server listens on. Point cbd-tui.toml at a
# remote server to use it as a remote control.
address = "http://127.0.0.1:50051"
# Credentials, when the server has [auth] configured (see below).
# `user` is the role name; leave both empty against an open server.
# The password is stored in plaintext — keep this file private.
user = ""
password = ""
# Show the frequency-spectrum bars under the track progress. Default true.
spectrum = true
# How those bars look. Defaults shown; a color is a "#rrggbb" triple, a
# color name, or a 0-255 palette index, and an unusable value warns and
# falls back. See docs/src/clients/tui.md for what each one does.
spectrum_color = "#bf616a" # the bars (the queue's playing-track red)
spectrum_gradient = true # shade them by height
spectrum_top_color = "#b48ead" # what the shading reaches at the top
spectrum_peak_color = "#81a1c1" # peak-hold shadows; "none" draws none
spectrum_peak_fill = true # false: a thin rule at the peak instead
spectrum_peak_fall = 10.0 # seconds for a full-scale shadow to fall
spectrum_bar_width = 1 # least bar width, in cells
spectrum_bar_gap = 1 # seam dividing two bars, in cells
spectrum_row_gap = 1 # line dividing two rows, in eighths
```
Every option except the `spectrum_*` appearance settings is also available
as a command-line flag before the subcommand (`cbd-tui --address ... --user
owner`, `cbd --spectrum false`); a flag overrides the file value. To write
the credentials into the config once, use the `auth` subcommand (see below)
instead of editing the file by hand:
```sh
cbd-tui auth owner 'my-password' # sets user + password
cbd-tui auth queue-owner 'pw' --address http://pi:50051
```
### `crabidy-server.toml` — providers and rights
On first start the server writes this file with every provider enabled:
```toml
providers = [
"tidal", "youtube", "fyyd", "abs", "soundcloud", "jamendo", "rss",
"fs", "crabidy", "orphans",
]
```
**Remove a name to disable that provider** — it no longer mounts and does
not appear in the library. Deleting the whole `providers` line re-enables
everything (a fresh install with no file behaves the same). Disabling
`crabidy` also drops `orphans`, which is a view over the store. A disabled
provider's own config file (`tidaly.toml`, etc.) is simply left unread.
The list can only offer what the binary was **built** with (see [Tailored
builds](#tailored-builds)): the default list names only the providers this
build has, and naming one it lacks logs a warning at startup instead of
failing.
#### Roles and rights
By default the server is open: everyone who can reach the port has
full control. Setting password hashes in `[auth]` locks the server
**from the top down** — each password you set lowers what a
no-credential caller may do, while a matching password elevates a
caller to that role:
- **owner** — everything (the normal user).
- **queue-owner** — anything on the queue and playback, but no
library writes: no bookmarks (`w`), captures (`W`), queue saving,
renames or deletes.
- **queue-appender** — may browse/search and append tracks to the
queue; nothing else.
A caller with no credentials gets the highest role you left *unguarded*:
nothing guarded → owner (the open default); guard `owner` → anonymous is
queue-owner; guard `owner` + `queue_owner` → anonymous is queue-appender;
guard all three → credentials required for everything.
```toml
[auth]
# One PHC hash per role. Guard from the top down. Generate and store a
# hash with: crabidy-server guard <role> (see the CLI section).
owner = "$argon2id$v=19$m=19456,t=2,p=1$..."
queue_owner = "$argon2id$..."
queue_appender = "$argon2id$..."
```
Clients authenticate with the role name as the basic-auth user (see
`cbd-tui.toml` above). A malformed `crabidy-server.toml` — or one that
guards a lower role while a higher one is still open — aborts server
startup rather than silently running open. Note that the transport is
plain HTTP/2: fine on a trusted home network, but anything exposed
further needs TLS termination (reverse proxy, VPN) in front.
#### Audio output device
By default the server plays to the system default output device. On a
Raspberry Pi that is often HDMI, so playback runs but you hear nothing on
the headphone jack or a USB/DAC. List the devices the server can see:
```console
$ crabidy-server audio-devices
Audio output devices (* = selected by the current config):
hdmi:CARD=vc4hdmi,DEV=0
sysdefault:CARD=Headphones
...
```
Then pin one by passing it to the same command — the value is matched
case-insensitively as a substring of the name, so a memorable fragment is
enough — and restart the server:
```console
$ crabidy-server audio-devices Headphones
Set [audio] device = "Headphones" in .../crabidy-server.toml
```
That writes `[audio] device` for you; you can also edit it by hand:
```toml
[audio]
device = "Headphones"
```
If the name matches nothing, both the command and the server warn, and the
server falls back to the system default.
## Command line
Every binary is a clap CLI: run it with `--help` (and any subcommand
with `--help`) for the full surface. Running a binary with **no
subcommand** behaves as it always has — `crabidy-server` runs the
server, `cbd-tui` runs the TUI, `cbd` runs the in-process server + TUI.
The `library`, `queue`, and `global` subcommands are available on all
three binaries and act as a remote control over gRPC (they connect to a
running server, honouring the same `[auth]` credentials as the TUI):
```sh
crabidy-server library list /tidal # browse a node
cbd-tui --address http://pi:50051 queue append /fs/album
cbd global play # toggle play/pause
cbd global volume -- -0.1 # lower the volume
```
Connection flags (`--address/--user/--password`) go **before** the
subcommand; omitted, they fall back to the client config file.
Server-only subcommands (`crabidy-server`, and `cbd`):
- `guard <role> [password]` — hash a role password (argon2id), print
the PHC string, and (unless `--no-config`) write it into
`crabidy-server.toml`'s `[auth]`. Roles: `owner`, `queue-owner`,
`queue-appender`.
- `scan <path> [--capture|--move]` — walk a folder and drop a
`.cbd-track.toml` beside every audio file so it browses under `/fs`.
`--capture` copies each file into the content store (the toml points
there); `--move` moves it instead of copying.
- `audio-devices [device]` — list the audio output devices, or pin one into
`[audio] device` (see above).
- `features` — print the build features this binary has (see [Tailored
builds](#tailored-builds)).
Client-only subcommand (`cbd-tui`, and `cbd`):
- `auth <role> [password] [--address ADDR]` — write the role name and
cleartext password (and address) into the client config.
**Password caveat.** A password given as a command-line argument is
visible in the process list (e.g. `ps`). Omit it and `guard` reads the
password from stdin instead, which keeps it out of argv and is
pipe-friendly:
```sh
printf '%s' 'my-password' | crabidy-server guard owner
```
The client config stores the password in plaintext, so keep the file
private.
### Completions and man pages
```sh
cbd-tui completions bash # print a completion script
devenv shell -- gen-cli-assets # write dist/completions + dist/man
```
`gen-cli-assets` builds the binaries with `CBD_ASSET_DIR=$PWD/dist`, so
`dist/completions/**` (bash/zsh/fish) and `dist/man/*.1` are produced
for all three binaries. Every ordinary build also emits them into the
crate's `OUT_DIR`.
## Using the library
Navigation is vim-style: `j`/`k` select, `l` enters the selected
folder, `h` goes to the parent, `Tab` switches between library and
queue, `Enter` replaces the queue with the selection. `%` creates a
node where the pane title shows `% to add` (e.g. a search term), `e`
renames, `d` deletes. `/` filters the current pane (library or queue)
live as you type — `Enter` keeps the filter, `Esc` clears it.
`s` marks the selected row; `v` (or `V`) enters **visual mode**, where
movement marks or unmarks everything you sweep over, vim-style. The sweep
is anchored where you entered it, so moving back reverses it. `Esc` (or
any non-movement key) leaves visual mode. Both panes have marks and visual
mode.
In the queue this feeds a vim-style **register**: `y` yanks the selection
into it, `d` deletes the marked rows *into* it, and `c`/`C` fill it with
whatever they clear — so an accidental clear is recoverable. `p` pastes it
after the cursor and `P` before it, which makes `d` then `P` an exact undo
and `d``p` a move. The register is per client, in memory, one slot, and
holds paths — so a paste re-resolves (a yanked album expands to its
tracks). Note `p` no longer inserts the library selection: that flow is now
`y` on the left, then `p` on the right.
- `w` saves the selection (a library subtree, or in the queue pane the
queue) as a new folder under `/crabidy/<name>` of **link** files —
needs the source provider to replay. On a name that already exists the
save is refused with a warning; delete the old folder and save again.
- `W` **captures** the selection into `/crabidy/<name>`: same as `w`, but
every track's audio is fetched into the shared content store under
`~/.local/share/crabidy/` and the saved tomls link to it — fully local
playback afterwards. Works on a library subtree and on the queue (no
need to save it first). Audio is **de-duplicated**: capturing the same
track again (from a playlist, a search, another save) reuses the stored
file instead of downloading it twice, matched first by provider id and
then by content hash. Tracks already local (from `/fs`) are copied into
the store rather than re-downloaded. A source that genuinely cannot be
captured is recorded as *skipped* (red in the UI, skipped by playback).
Download captures can take long; progress is shown in the library pane.
- Captured rows are marked with a trailing `↓` (down-arrow) at the end of
the row — visible even while browsing another provider, so you can see
what you already have.
- Inside `/crabidy`, `d` deletes a folder or track immediately (no
confirmation): it removes only the metadata toml, never the shared store
audio, which other saves may reference.
Press `?` for the full binding table.
A row of frequency-spectrum bars is drawn under the track progress
while audio plays (the server taps its own output, runs the FFT, and
streams the bars, so it works whether the server is local or remote).
Toggle it at runtime with `f`, or set the startup default with
`spectrum = false` in the client config. Servers built without the
`spectrum` feature simply never send bars.
The bars are shaded by height — red at the floor reaching purple at the top
— and divided into segments by a dim seam between bars and a thin line
between value rows. Each bar trails a blue peak-hold shadow marking where it
lately reached, falling away over a few seconds. Every part of that is
configurable, including turning it all off; see
[docs/src/clients/tui.md](docs/src/clients/tui.md).
`,` and `.` seek 15 seconds inside the playing track; `<` and `>` (or
`Ctrl-p`/`Ctrl-n`) skip a whole track. `K`/`J` change the volume and `m`
mutes; the now-playing pane shows the server's level, which tops out at
110%.
## Web client
`crabidy-server` serves a browser client with the same functionality as
the TUI at its own address (`http://<server>:50051/`) — same navigation,
same keys (`j`/`k`/`h`/`l`, `%`, `e`, `d`, `w`, `W`, marks and visual mode,
the `y`/`d`/`p` register, `,`/`.` to seek, queue and playback controls, `?`
for help), plus clickable equivalents for all of it: the progress bar seeks
where you click, and `library`/`queue` tabs in the top bar switch panes
without a keyboard. On a phone the two panes cannot sit side by side, so
only the focused one is shown and those tabs are the way between them.
There is a light/dark theme toggle. It talks gRPC-web to the same service
the TUI uses, so it honors the same `[auth]` roles (it shows a login form
when the server requires credentials). The `/` live filter is TUI-only for
now.
It is compiled to a WASM bundle and embedded into the server binary,
behind the default-on `web-ui` cargo feature. A plain `cargo build`
needs no WASM toolchain — it embeds a "not built" placeholder page until
you build the bundle:
```sh
devenv shell -- build-web # writes cbd-web/dist
cargo build -p crabidy-server # embeds it
```
For a headless, gRPC-only binary, build with every feature *except*
`web-ui` — e.g. `--no-default-features --features
all-providers,opus,spectrum`. See [cbd-web/README.md](cbd-web/README.md) for
the dev loop and details.
## Nix packages and cross-compiling
`flake.nix` (built with [crane](https://github.com/ipetkov/crane)) packages
the binaries as Nix derivations — for your own machines, and cross-compiled
for a Raspberry Pi. No Docker required.
Native — install on any machine with Nix:
```sh
nix run .#cbd-tui # run without installing
nix build .#crabidy # cbd, cbd-tui, crabidy-server → ./result/bin
nix profile install .#crabidy # or github:OWNER/crabidy once pushed
```
Raspberry Pi (or any aarch64 Linux) — a fully **static musl** binary, so it
has no glibc-version or loader dependency and runs on stock Raspberry Pi OS
(bookworm and newer):
```sh
nix build .#crabidy-server-aarch64
scp ./result/bin/crabidy-server pi:/usr/local/bin/
```
The native package links the libopus that nixpkgs ships (`opus` without
`opus-bundled`, see
[docs/src/build-features.md](docs/src/build-features.md)), so nothing is
compiled from vendored C and the binaries carry a RUNPATH — they run from a
bare login shell, not only from this repo's dev shell.
Nix cross-compiles the Rust *and* the C dependencies (ALSA, libopus, aws-lc)
hermetically on an x86_64 host — the isolated build avoids the host-linker
pitfalls of cross-compiling in a plain shell. The aarch64 server **includes
the embedded web UI**: the flake builds the `cbd-web` wasm bundle with trunk
(pinning a `wasm-bindgen` CLI that matches the crate) and stages it into the
server's `web-ui` feature. The native `.#crabidy` package stays headless; use
a normal `cargo build` there if you want the bundle.
A container-based `cross` setup also exists (`Cross.toml` + the
`*-Dockerfile`s) for building against Debian's glibc, but the flake is the
recommended path.
## Logs
`cbd` and `cbd-tui` log to `~/.local/state/crabidy/` (daily files);
`crabidy-server` logs to stderr. Stream URLs and credentials are
redacted from logs by design.
## Development
```sh
devenv shell -- bash -lc 'cargo test --workspace --exclude cbd-web'
devenv shell -- check-features # the build-feature matrix
devenv shell -- docs # serve the documentation book
```
`AGENTS.md` (and `CLAUDE.md`) carry the coding rules and the toolchain
conventions. The reference documentation for the system itself lives in
[`docs/`](docs/src/).