Two verbs that rewrite what the queue already holds instead of adding to it: DedupQueue and SortQueue, on the playback loop, for every client. Client-side was the tempting shortcut and the wrong one — a client-computed Remove races the resolve stream, and "sorting" as a Replace of paths re-resolves everything through providers and restarts playback at the head. A duplicate is the same provider item id *scoped to its provider* (the content store keys content the same way, so unscoped ids let two providers' numeric ids collide), or the whole path when a provider reports no id. Deliberately not artist+title: identical metadata is routinely a different recording, and a wrong merge is unrecoverable queue state. Within a group the playing entry survives, else the earliest — "keep the first" stops the music whenever the playing copy is a later one — and the removal itself goes through remove_tracks, so one code path maintains play_order. Sorting permutes `tracks` and then treats the play order by mode: with shuffle off it rebuilds it as the identity with the cursor on the current track (the sort decides what plays next), with shuffle on it remaps it through the permutation so the shuffled sequence and the position in it survive. Five stable strategies (artist→album, album, title, duration, reverse), keys built once per track, unknown sorting last in both directions, and UNSPECIFIED refused as InvalidArgument. DedupQueue answers with a count, alone among the queue verbs: 0 cannot be recovered from the new snapshot and is what a user needs to hear. It crosses as a number — the wording is each client's. TUI: `u` and a modal `S` sort menu whose unclaimed keys are swallowed, plus the count in the queue pane title. Web: the same keys, clickable menu rows, toolbar buttons, count in a toast. CLI: `queue dedup` and `queue sort <key> [--desc]` with the strategy as a ValueEnum. Full dev-flow: architecture/queue-order.md, quality/queue-order.md, plan/queue-order.md, plan/summary.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| .gitignore | ||
| Cargo.toml | ||
| README.md | ||
| Trunk.toml | ||
| index.html | ||
| style.css | ||
README.md
cbd-web — the browser client
A Leptos client-side WASM app with the same
functionality as cbd-tui, served by crabidy-server itself.
How it works
- Transport: gRPC-web (
tonic-web-wasm-client) over the same generated client and proto types the TUI uses (crabidy-core). No second API surface — feature parity is structural. The server wraps its existing gRPC service intonic-web, so the browser and the TUI hit identical/crabidy.v1.CrabidyService/…paths, and the same role auth layer gates both. - Serving: the built bundle (
cbd-web/dist) is embedded intocrabidy-serverat compile time behind the default-onweb-uifeature and served as the fallback route on port 50051. gRPC and static assets share one origin, so there is no CORS story. - Local-first: pure client-side rendering, every asset in the
bundle (no CDN, no external fonts), library listings cached in memory
like the TUI, credentials and theme in
localStorage, and the update stream reconnects with backoff when the server disappears. There is no CRDT layer — this is a remote control for one live server state, not an offline-editing app (a deliberate departure from theweb_client_example_workspacetemplate that informed the toolchain).
Functionality
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/queuetabs in the top bar switch panes and show which one the keys go to — whatTabdoes, 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
(owner / queue-owner / queue-appender) and password; they are
stored in localStorage and sent as the gRPC-web authorization
header on every request.
Building
The WASM toolchain (trunk, wasm-bindgen, the wasm32-unknown-unknown
target) is provided by devenv. From the repo root:
devenv shell -- build-web # release bundle → cbd-web/dist
cargo build -p crabidy-server # embeds cbd-web/dist
build-web clears RUSTFLAGS first: the native toolchain sets the
mold linker, which rust-lld (the wasm linker) cannot parse.
Building crabidy-server without a cbd-web/dist present is fine — it
embeds a placeholder page telling you to run build-web. To drop the web
client (and the tonic-web layer) entirely, build the server without its
web-ui cargo feature — e.g. --no-default-features --features all-providers,opus,spectrum; see docs/src/build-features.md.
Dev loop
Run a server, then a live-reloading trunk server that proxies gRPC-web to it:
cargo run -p crabidy-server # or `cbd`
devenv shell -- serve-web # trunk serve on http://127.0.0.1:8080
Trunk.toml proxies /crabidy.v1.CrabidyService to 127.0.0.1:50051,
so the app behaves as if served from the server.
Tests
The DOM-free logic (pane/selection state machines, the keymap, capture
progress formatting) lives in src/state.rs and src/keymap.rs and is
unit-tested on the native target:
cargo test -p cbd-web
Components in src/app.rs stay thin over that logic. The server-side
serving and the gRPC-web + auth routing are tested in crabidy-server
(src/web.rs, tests/web_server.rs).