diff --git a/README.md b/README.md index 6d9ea0f..30e57bf 100644 --- a/README.md +++ b/README.md @@ -137,13 +137,26 @@ 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 = 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 -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: +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 @@ -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` 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://:50051/`) — same navigation, -same keys (`j`/`k`/`h`/`l`, `%`, `e`, `d`, `w`, `W`, queue and playback -controls, `?` for help), plus clickable equivalents and 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). +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` diff --git a/cbd-web/README.md b/cbd-web/README.md index bde0b79..170bdbd 100644 --- a/cbd-web/README.md +++ b/cbd-web/README.md @@ -25,12 +25,25 @@ functionality as `cbd-tui`, served by `crabidy-server` itself. ## Functionality -Everything the TUI does: browse the library (`j`/`k`/`h`/`l`, click), -marks, create/rename/delete nodes (`%`/`e`/`d`), bookmark and capture -(`w`/`W`, with live progress lines and skipped-track marking), the full queue -and playback controls, volume, shuffle/repeat, and a `?` help overlay listing -the keys. Keys mirror the TUI; every key also has a clickable control. A -light/dark theme follows the OS and can be toggled (persisted). The accent +Everything the TUI does: browse the library (`j`/`k`/`h`/`l`, click), marks +and visual mode (`s`, `v`/`V`) in both panes, the one-slot register (`y` +yanks, `d`/`c`/`C` fill it as they remove, `p`/`P` paste), create/rename/ +delete nodes (`%`/`e`/`d`), bookmark and capture (`w`/`W`, with live progress +lines and skipped-track marking), the full queue and playback controls, +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. When the server requires credentials, a login form collects the role diff --git a/crabidy-server/src/auth.rs b/crabidy-server/src/auth.rs index b26cb63..dd1df67 100644 --- a/crabidy-server/src/auth.rs +++ b/crabidy-server/src/auth.rs @@ -75,7 +75,7 @@ pub fn minimum_role(grpc_path: &str) -> Role { // Every other queue and playback verb. "Queue" | "Replace" | "Remove" | "Insert" | "ClearQueue" | "SetCurrent" | "ToggleShuffle" | "ToggleRepeat" | "TogglePlay" | "Stop" | "ChangeVolume" - | "ToggleMute" | "Next" | "Prev" | "RestartTrack" => Role::QueueOwner, + | "ToggleMute" | "Next" | "Prev" | "RestartTrack" | "Seek" => Role::QueueOwner, // Library writes (CaptureLibraryNode, SaveQueue, // RenameLibraryNode, DeleteLibraryNode) and anything unmapped. _ => 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 /// (argon2id, default parameters, fresh random salt). Backs the -/// `crabidy-server hash-password` helper. +/// `crabidy-server guard` subcommand. pub fn hash_password(password: &str) -> Result { use argon2::password_hash::{rand_core::OsRng, SaltString}; use argon2::PasswordHasher; @@ -355,6 +355,7 @@ mod tests { "Next", "Prev", "RestartTrack", + "Seek", ]; let owner = [ "CaptureLibraryNode", @@ -362,10 +363,28 @@ mod tests { "RenameLibraryNode", "DeleteLibraryNode", ]; - // The full service, from crabidy.proto — 24 methods. A new RPC - // must be added to exactly one list (and the layer keeps it - // owner-only until then). - assert_eq!(appender.len() + queue_owner.len() + owner.len(), 24); + // Every RPC must appear in exactly one list above. Taken from the + // proto itself rather than a count copied out of it: a hardcoded + // total let `Seek` be added to the service and land on the + // 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 { let path = format!("{SERVICE_PREFIX}{method}"); assert_eq!(minimum_role(&path), Role::QueueAppender, "{method}"); diff --git a/docs/src/auth.md b/docs/src/auth.md index 581f995..2365ed4 100644 --- a/docs/src/auth.md +++ b/docs/src/auth.md @@ -22,8 +22,8 @@ below it (owner ⊃ queue-owner ⊃ queue-appender): - **owner** — the normal user: everything, including all library writes. - **queue-owner** — anything on the queue and playback (append, remove, - reorder, clear, shuffle, repeat, play/stop, next/prev, volume, mute, - …), but **no library writes**: no bookmarks (`w`), no captures (`W`), + reorder, clear, shuffle, repeat, play/stop, next/prev, seek, volume, + mute), but **no library writes**: no bookmarks (`w`), no captures (`W`), no saving, renaming, or deleting. - **queue-appender** — may browse and search the library and **append** tracks to the queue; nothing else. (Searching is allowed because diff --git a/docs/src/build-features.md b/docs/src/build-features.md index 8a265bb..18411fc 100644 --- a/docs/src/build-features.md +++ b/docs/src/build-features.md @@ -36,12 +36,13 @@ The same list goes into the server's startup log, so a support question | `abs` | the `/abs` audiobookshelf provider | | `soundcloud` | the `/soundcloud` provider | | `jamendo` | the `/jamendo` provider | +| `rss` | the `/rss` podcast-subscription provider | | `fs` | local files **and persistent state** — see below | | `opus` | Ogg-Opus decoding (`symphonia` + a bundled libopus C build) | | `spectrum` | the server-side FFT feeding clients' spectrum bars | | `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 `notifications` for the TUI's desktop "now playing" popups (`notify-rust`, which on Linux pulls a D-Bus stack). diff --git a/docs/src/clients.md b/docs/src/clients.md index 366c27e..7da40f2 100644 --- a/docs/src/clients.md +++ b/docs/src/clients.md @@ -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 **commands** (browse the library, change the queue, control playback) and subscribes to the **update stream** that pushes the current queue, -play state, track position, capture progress, and the frequency -spectrum as they change. Nothing is polled — each client redraws from +play state, track position, output level and mute, capture progress, and +the frequency spectrum as they change. Nothing is polled — each client redraws from the pushed updates, so several clients driving one server always agree on what is playing. diff --git a/docs/src/intro.md b/docs/src/intro.md index e9ed7cc..0cba6e0 100644 --- a/docs/src/intro.md +++ b/docs/src/intro.md @@ -64,6 +64,7 @@ segment is a **provider** mounted as a subtree: ├── fs a local music folder ├── fyyd podcast search ├── jamendo Creative-Commons music +├── rss podcast subscriptions ├── soundcloud SoundCloud search, likes, and playlists ├── tidal Tidal streaming ├── youtube YouTube search & playlists diff --git a/docs/src/providers.md b/docs/src/providers.md index 530f7c8..613c9f3 100644 --- a/docs/src/providers.md +++ b/docs/src/providers.md @@ -55,11 +55,11 @@ independent things decide it: 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 - 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. 2. **Is it enabled?** The `providers` list in `crabidy-server.toml` turns 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. ```admonish note diff --git a/plan/summary.md b/plan/summary.md index e822514..1e5a0e0 100644 --- a/plan/summary.md +++ b/plan/summary.md @@ -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 pure appearance with no right answer: each one is a question that came back 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. diff --git a/rssdy/README.md b/rssdy/README.md index 70ff1e9..16ce0ad 100644 --- a/rssdy/README.md +++ b/rssdy/README.md @@ -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, 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 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