docs: audit the book and the READMEs, and give Seek its role

Sweep of docs/ and every README against the code, after several features
landed since the last one.

Mechanical checks, which find what reading does not: SUMMARY covers every
page and no more; every relative link resolves (docs/src/providers.md
pointed a directory above the book, twice); every #anchor matches a real
heading; every provider crate's Settings field is documented in both its
README and its book page; the feature table matches Cargo.toml, which it
did not -- `rss` was missing and "seven provider features" is now eight.

Prose that predated recent work: the root README's client-config sample
knew only `spectrum` and still claimed every option has a flag; its
spectrum and web-client sections predated the colors, segments, shadows,
pane tabs, register and seek; cbd-web/README.md likewise; intro.md's
provider tree was missing /rss; clients.md omitted volume and mute from
what the update stream carries; rssdy/README.md did not mention that only
audio enclosures become episodes.

The audit also found a defect the docs were right about: `Seek` was never
added to `minimum_role`, so it fell through to the owner-only default
while auth.md and architecture/roles-auth.md both promise a queue-owner
may control playback. With auth configured a queue-owner could play, skip
and change the volume, but got PermissionDenied on `,`/`.`. Seek now sits
with the other playback verbs.

The test meant to prevent that -- "a new RPC must be added to exactly one
list" -- compared the role lists against a hardcoded 24, so a 25th method
kept the suite green. It now reads the method names out of crabidy.proto
and compares sets: a count copied out of a file is not a check against it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-27 21:19:59 +02:00
parent d8bd06219c
commit 87b98193d8
10 changed files with 138 additions and 29 deletions

View File

@ -137,13 +137,26 @@ password = ""
# Show the frequency-spectrum bars under the track progress. Default true. # Show the frequency-spectrum bars under the track progress. Default true.
spectrum = 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 = 4.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 is also available as a command-line flag before the Every option except the `spectrum_*` appearance settings is also available
subcommand (`cbd-tui --address ... --user owner`, `cbd --spectrum as a command-line flag before the subcommand (`cbd-tui --address ... --user
false`); a flag overrides the file value. To write the credentials into owner`, `cbd --spectrum false`); a flag overrides the file value. To write
the config once, use the `auth` subcommand (see below) instead of the credentials into the config once, use the `auth` subcommand (see below)
editing the file by hand: instead of editing the file by hand:
```sh ```sh
cbd-tui auth owner 'my-password' # sets user + password cbd-tui auth owner 'my-password' # sets user + password
@ -361,15 +374,32 @@ Toggle it at runtime with `f`, or set the startup default with
`spectrum = false` in the client config. Servers built without the `spectrum = false` in the client config. Servers built without the
`spectrum` feature simply never send bars. `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 ## Web client
`crabidy-server` serves a browser client with the same functionality as `crabidy-server` serves a browser client with the same functionality as
the TUI at its own address (`http://<server>:50051/`) — same navigation, the TUI at its own address (`http://<server>:50051/`) — same navigation,
same keys (`j`/`k`/`h`/`l`, `%`, `e`, `d`, `w`, `W`, queue and playback same keys (`j`/`k`/`h`/`l`, `%`, `e`, `d`, `w`, `W`, marks and visual mode,
controls, `?` for help), plus clickable equivalents and a light/dark the `y`/`d`/`p` register, `,`/`.` to seek, queue and playback controls, `?`
theme toggle. It talks gRPC-web to the same service the TUI uses, so it for help), plus clickable equivalents for all of it: the progress bar seeks
honors the same `[auth]` roles (it shows a login form when the server where you click, and `library`/`queue` tabs in the top bar switch panes
requires credentials). 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, 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` behind the default-on `web-ui` cargo feature. A plain `cargo build`

View File

@ -25,12 +25,25 @@ functionality as `cbd-tui`, served by `crabidy-server` itself.
## Functionality ## Functionality
Everything the TUI does: browse the library (`j`/`k`/`h`/`l`, click), Everything the TUI does: browse the library (`j`/`k`/`h`/`l`, click), marks
marks, create/rename/delete nodes (`%`/`e`/`d`), bookmark and capture and visual mode (`s`, `v`/`V`) in both panes, the one-slot register (`y`
(`w`/`W`, with live progress lines and skipped-track marking), the full queue yanks, `d`/`c`/`C` fill it as they remove, `p`/`P` paste), create/rename/
and playback controls, volume, shuffle/repeat, and a `?` help overlay listing delete nodes (`%`/`e`/`d`), bookmark and capture (`w`/`W`, with live progress
the keys. Keys mirror the TUI; every key also has a clickable control. A lines and skipped-track marking), the full queue and playback controls,
light/dark theme follows the OS and can be toggled (persisted). The accent seeking (`,`/`.` for 15 seconds, `<`/`>` for a whole track, or a click on the
progress bar), volume, shuffle/repeat, and a `?` help overlay listing the
keys. Keys mirror the TUI; every key also has a clickable control. The `/`
live filter is the one thing that is TUI-only so far.
Two layout details earn their own note:
- `library`/`queue` **tabs** in the top bar switch panes and show which one
the keys go to — what `Tab` does, reachable by thumb.
- Below 700px the panes cannot sit side by side, so **only the focused pane
is rendered**. It is not collapsed to a strip: a strip's truncated rows
still take taps, which sent them to the wrong pane.
A light/dark theme follows the OS and can be toggled (persisted). The accent
color is the crab orange-red. color is the crab orange-red.
When the server requires credentials, a login form collects the role When the server requires credentials, a login form collects the role

View File

@ -75,7 +75,7 @@ pub fn minimum_role(grpc_path: &str) -> Role {
// Every other queue and playback verb. // Every other queue and playback verb.
"Queue" | "Replace" | "Remove" | "Insert" | "ClearQueue" | "SetCurrent" "Queue" | "Replace" | "Remove" | "Insert" | "ClearQueue" | "SetCurrent"
| "ToggleShuffle" | "ToggleRepeat" | "TogglePlay" | "Stop" | "ChangeVolume" | "ToggleShuffle" | "ToggleRepeat" | "TogglePlay" | "Stop" | "ChangeVolume"
| "ToggleMute" | "Next" | "Prev" | "RestartTrack" => Role::QueueOwner, | "ToggleMute" | "Next" | "Prev" | "RestartTrack" | "Seek" => Role::QueueOwner,
// Library writes (CaptureLibraryNode, SaveQueue, // Library writes (CaptureLibraryNode, SaveQueue,
// RenameLibraryNode, DeleteLibraryNode) and anything unmapped. // RenameLibraryNode, DeleteLibraryNode) and anything unmapped.
_ => Role::Owner, _ => Role::Owner,
@ -84,7 +84,7 @@ pub fn minimum_role(grpc_path: &str) -> Role {
/// Hashes a password into the PHC string `crabidy-server.toml` expects /// Hashes a password into the PHC string `crabidy-server.toml` expects
/// (argon2id, default parameters, fresh random salt). Backs the /// (argon2id, default parameters, fresh random salt). Backs the
/// `crabidy-server hash-password` helper. /// `crabidy-server guard` subcommand.
pub fn hash_password(password: &str) -> Result<String, String> { pub fn hash_password(password: &str) -> Result<String, String> {
use argon2::password_hash::{rand_core::OsRng, SaltString}; use argon2::password_hash::{rand_core::OsRng, SaltString};
use argon2::PasswordHasher; use argon2::PasswordHasher;
@ -355,6 +355,7 @@ mod tests {
"Next", "Next",
"Prev", "Prev",
"RestartTrack", "RestartTrack",
"Seek",
]; ];
let owner = [ let owner = [
"CaptureLibraryNode", "CaptureLibraryNode",
@ -362,10 +363,28 @@ mod tests {
"RenameLibraryNode", "RenameLibraryNode",
"DeleteLibraryNode", "DeleteLibraryNode",
]; ];
// The full service, from crabidy.proto — 24 methods. A new RPC // Every RPC must appear in exactly one list above. Taken from the
// must be added to exactly one list (and the layer keeps it // proto itself rather than a count copied out of it: a hardcoded
// owner-only until then). // total let `Seek` be added to the service and land on the
assert_eq!(appender.len() + queue_owner.len() + owner.len(), 24); // owner-only fallthrough with the suite still green, which is the
// one thing this test exists to prevent.
let proto = include_str!("../../crabidy-core/crabidy/v1/crabidy.proto");
let service: std::collections::BTreeSet<&str> = proto
.lines()
.filter_map(|line| line.trim().strip_prefix("rpc "))
.filter_map(|rest| rest.split('(').next())
.map(str::trim)
.collect();
let mapped: std::collections::BTreeSet<&str> = appender
.iter()
.chain(queue_owner.iter())
.chain(owner.iter())
.copied()
.collect();
assert_eq!(
mapped, service,
"every RPC of the service needs a role (left: mapped, right: the proto)"
);
for method in appender { for method in appender {
let path = format!("{SERVICE_PREFIX}{method}"); let path = format!("{SERVICE_PREFIX}{method}");
assert_eq!(minimum_role(&path), Role::QueueAppender, "{method}"); assert_eq!(minimum_role(&path), Role::QueueAppender, "{method}");

View File

@ -22,8 +22,8 @@ below it (owner ⊃ queue-owner ⊃ queue-appender):
- **owner** — the normal user: everything, including all library - **owner** — the normal user: everything, including all library
writes. writes.
- **queue-owner** — anything on the queue and playback (append, remove, - **queue-owner** — anything on the queue and playback (append, remove,
reorder, clear, shuffle, repeat, play/stop, next/prev, volume, mute, reorder, clear, shuffle, repeat, play/stop, next/prev, seek, volume,
), but **no library writes**: no bookmarks (`w`), no captures (`W`), mute), but **no library writes**: no bookmarks (`w`), no captures (`W`),
no saving, renaming, or deleting. no saving, renaming, or deleting.
- **queue-appender** — may browse and search the library and **append** - **queue-appender** — may browse and search the library and **append**
tracks to the queue; nothing else. (Searching is allowed because tracks to the queue; nothing else. (Searching is allowed because

View File

@ -36,12 +36,13 @@ The same list goes into the server's startup log, so a support question
| `abs` | the `/abs` audiobookshelf provider | | `abs` | the `/abs` audiobookshelf provider |
| `soundcloud` | the `/soundcloud` provider | | `soundcloud` | the `/soundcloud` provider |
| `jamendo` | the `/jamendo` provider | | `jamendo` | the `/jamendo` provider |
| `rss` | the `/rss` podcast-subscription provider |
| `fs` | local files **and persistent state** — see below | | `fs` | local files **and persistent state** — see below |
| `opus` | Ogg-Opus decoding (`symphonia` + a bundled libopus C build) | | `opus` | Ogg-Opus decoding (`symphonia` + a bundled libopus C build) |
| `spectrum` | the server-side FFT feeding clients' spectrum bars | | `spectrum` | the server-side FFT feeding clients' spectrum bars |
| `web-ui` | the embedded web client (`tonic-web` + the wasm bundle) | | `web-ui` | the embedded web client (`tonic-web` + the wasm bundle) |
Plus two conveniences: `all-providers` enables the seven provider features Plus two conveniences: `all-providers` enables the eight provider features
at once, and `cbd` (the bundle) mirrors every feature above and adds at once, and `cbd` (the bundle) mirrors every feature above and adds
`notifications` for the TUI's desktop "now playing" popups (`notify-rust`, `notifications` for the TUI's desktop "now playing" popups (`notify-rust`,
which on Linux pulls a D-Bus stack). which on Linux pulls a D-Bus stack).

View File

@ -4,8 +4,8 @@ A client is anything that drives the server. Every client speaks the
same gRPC service (see [Architecture](./architecture.md)): it sends same gRPC service (see [Architecture](./architecture.md)): it sends
**commands** (browse the library, change the queue, control playback) **commands** (browse the library, change the queue, control playback)
and subscribes to the **update stream** that pushes the current queue, and subscribes to the **update stream** that pushes the current queue,
play state, track position, capture progress, and the frequency play state, track position, output level and mute, capture progress, and
spectrum as they change. Nothing is polled — each client redraws from the frequency spectrum as they change. Nothing is polled — each client redraws from
the pushed updates, so several clients driving one server always agree the pushed updates, so several clients driving one server always agree
on what is playing. on what is playing.

View File

@ -64,6 +64,7 @@ segment is a **provider** mounted as a subtree:
├── fs a local music folder ├── fs a local music folder
├── fyyd podcast search ├── fyyd podcast search
├── jamendo Creative-Commons music ├── jamendo Creative-Commons music
├── rss podcast subscriptions
├── soundcloud SoundCloud search, likes, and playlists ├── soundcloud SoundCloud search, likes, and playlists
├── tidal Tidal streaming ├── tidal Tidal streaming
├── youtube YouTube search & playlists ├── youtube YouTube search & playlists

View File

@ -55,11 +55,11 @@ independent things decide it:
1. **Was it built in?** Every provider sits behind a Cargo feature (all on by 1. **Was it built in?** Every provider sits behind a Cargo feature (all on by
default). A binary built without one can never mount it — see [Tailored default). A binary built without one can never mount it — see [Tailored
builds](../build-features.md), and `crabidy-server features` to see what a builds](./build-features.md), and `crabidy-server features` to see what a
binary has. binary has.
2. **Is it enabled?** The `providers` list in `crabidy-server.toml` turns 2. **Is it enabled?** The `providers` list in `crabidy-server.toml` turns
compiled-in providers on and off without a rebuild — see compiled-in providers on and off without a rebuild — see
[Configuration](../config.md#enabling-and-disabling-providers). [Configuration](./config.md#enabling-and-disabling-providers).
3. **Did it initialize?** Only a client that came up successfully is mounted. 3. **Did it initialize?** Only a client that came up successfully is mounted.
```admonish note ```admonish note

View File

@ -1774,3 +1774,41 @@ would divide by zero and freeze every shadow on screen.
Seven appearance keys is a lot of config surface, justified by these being Seven appearance keys is a lot of config surface, justified by these being
pure appearance with no right answer: each one is a question that came back pure appearance with no right answer: each one is a question that came back
with a different answer than the default assumed. with a different answer than the default assumed.
## Documentation audit (2026-07-27)
A sweep of `docs/` and every README against the code, prompted by several
features landing after the last one. Mechanical checks first — they find what
reading cannot:
- every `docs/src/**.md` appears in `SUMMARY.md` and vice versa;
- every relative link in the book and the READMEs resolves (two did not:
`docs/src/providers.md` pointed at `../build-features.md` and `../config.md`,
which from `src/providers.md` lands a directory above the book);
- every `#anchor` in an internal link matches a heading that exists;
- every provider crate's `Settings` field appears in its README and its book
page (all did, except Tidal's endpoint and token fields, which its page
deliberately delegates to `tidaldy/README.md`);
- the `crabidy-server` feature table against `Cargo.toml` (`rss` was missing,
and "seven provider features" had become eight).
Stale prose, all of it from work that landed after the previous sweep: the
root README's client-config sample knew only `spectrum`, still claimed every
option has a command-line flag, and described the spectrum before it had
colors, segments or shadows; its web section predated the pane tabs, the
register and seek; `cbd-web/README.md` predated the same; `docs/src/intro.md`'s
provider tree was missing `/rss`; `docs/src/clients.md` listed what the update
stream carries without volume and mute.
The audit also turned up a **defect the docs were right about**: `Seek` was
never added to `minimum_role`, so it fell through to the owner-only default
while `auth.md` and `architecture/roles-auth.md` both promise a queue-owner
may control playback. With auth configured, a queue-owner could play, skip and
change the volume but got `PermissionDenied` on `,`/`.`.
The test meant to prevent exactly that — "a new RPC must be added to exactly
one list" — compared the three role lists against a hardcoded 24. `Seek` made
the service 25 methods and the suite stayed green. It now reads the method
names out of `crabidy.proto` with `include_str!` and compares sets, so the
next RPC cannot be forgotten. A count copied out of a file is not a check
against that file.

View File

@ -43,6 +43,13 @@ A subscription is queueable and downloadable, so you can queue or `W`-capture
a whole feed. Episodes stream directly from the enclosure URL — no sidecar, a whole feed. Episodes stream directly from the enclosure URL — no sidecar,
no helper binary. no helper binary.
Only **audio** enclosures become episodes: an item is taken when its
enclosure type says audio, or when the type is missing or generic and the URL
looks like audio (including extensionless `/feed/mp3`-style URLs). Video and
image enclosures are skipped, so a show that publishes both plays its audio
and a blog feed does not list its featured images as tracks. See
`docs/src/providers/rss.md` for the exact rule.
**Nothing is cached.** Every visit to a subscription fetches the feed, so an **Nothing is cached.** Every visit to a subscription fetches the feed, so an
episode published a minute ago is there. One memo exists purely so that episode published a minute ago is there. One memo exists purely so that
listing a feed and then queueing its 40 episodes costs one fetch rather than listing a feed and then queueing its 40 episodes costs one fetch rather than