Every binary is now a clap-derive CLI; no subcommand keeps the current
default (run server / TUI / both).
- cbd-cli: run_remote executes library/queue/global against a running
server (mirrors RpcClient; direct Stop; connect+request timeouts;
concise errors; human-readable listings).
- crabidy-server: guard (hash + write [auth], stdin fallback, --no-config),
scan (walk + write .cbd-track.toml; --capture/--move via new
CrabidyStore::ingest_file), ServerSettings::store; replaces hash-password.
- cbd-tui: auth writes the client config; config load+override keeps the
first-run-defaults / flag-overrides-file behavior.
- cbd: union of server + client subcommands.
- build.rs in each binary generates shell completions + man pages
(OUT_DIR, and CBD_ASSET_DIR when set); devenv gen-cli-assets → dist/.
- README CLI section; tests for parse, config writers, scan/ingest, guard.
Deviations (plan/summary.md): ClapSerde kept; connection flags top-level
(not clap-global, to avoid colliding with auth --address); Box<dyn Error>
CLI reports per existing convention.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace /queues + /bookmarks + /captures with one /crabidy fs provider
whose track tomls link into a content-addressed store that de-duplicates
audio by provider id and by content hash (architecture/crabidy-store.md).
Green-field: no data migration.
- crabidy_store.rs: CrabidyStore owns the state tree (state_dir/crabidy)
and the data store (data_dir/crabidy); StoreIndex derived from the
.cbd-store.toml sidecars. save() enumerates a source into a temp folder
and swaps it in atomically (conflict refuses); capture_track dedups
(already-stored -> provider-id -> hash -> new). Queue persistence lives
here now (persist_current/load_current/save_snapshot/spawn_persister).
- capture.rs: reduced to enumerate + Downloader::download_to + Progress;
removed the Sink/capture_into/download-to-toml machinery.
- orchestrator: one crabidy_client + crabidy_store, single crabidy_owns
routing; get_lib_node annotates captured tracks via the store index.
- rpc: capture_error_status helper; save_queue link-saves the live queue
into /crabidy. playback persists/restores via CrabidyStore.
- tidal/youtube set Track.provider_item_id (track id / video id).
- cbd-tui: /crabidy/current, captured | row marker, delete confirmation
removed (deletes never touch the store), cache-invalidation + help text.
- delete bookmark_store/capture_store/queue_store; supersede their docs.
See plan/summary.md for deviations (SaveQueue RPC kept; fs id left empty;
shallow folder marking).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 1-2 of the crabidy-store dev-flow (architecture/crabidy-store.md,
quality/, plan/): one /crabidy provider replacing queues/bookmarks/
captures, with track tomls linking into a content-addressed store that
de-duplicates by provider id and content hash.
Additive, build stays green:
- proto: Track.provider_item_id + is_captured; LibraryNode.is_captured;
LibraryNodeChild.is_captured (swept all literals).
- fsdy: Playable::Store + PlayableSpec.store, 5-way cardinality,
from_track_store, Client.with_store_root + store resolution.
- crabidy_store.rs: StoreSidecar/ProviderEntry/StoreIndex/CrabidyStore
type + method surface (bodies stubbed for the implement stage).
- supersede bookmarks/captures/capture-deletion docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three fixes so the spectrum shows for real audio: the analyzer now
takes the peak magnitude per band with single-sided (2/N) scaling
instead of the mean (averaging diluted a strong component into the
quiet bins around it, leaving near-zero bars that render as blank
spaces); the now-playing rows use a hard Length(1) so the spectrum row
cannot be squeezed out by the info block; and the server logs when it
starts/stops streaming bars so the live path is diagnosable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A row of frequency bars under the track progress, on by default and
toggleable with the client spectrum config option. Because the audio
plays on the server and clients may be remote, the spectrum is produced
server-side, not captured locally: audio-player taps its own output
into a lock-free ring on the audio thread (one store per sample, no
locks), crabidy-server runs a Hann + realfft over 2048 samples at 20fps,
folds it into log-spaced bars, and broadcasts them as a new SpectrumFrame
on the update stream. The task idles when nothing is playing or no
client is listening. The TUI renders block-glyph bars in the now-playing
pane; the web client renders the same bins as CSS bars.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
crabidy-server now serves a browser client with the same functionality
as the TUI at its own address, behind the default-on web-ui feature.
The new cbd-web crate is a client-side Leptos/WASM app talking gRPC-web
(tonic-web-wasm-client) over the same crabidy-core client and proto the
TUI uses, so parity is structural: library browsing, search terms,
marks, bookmarks/captures with live progress and confirmed deletion,
the full queue and playback controls, and the update stream with
reconnect. Keys mirror the TUI; every key also has a clickable control.
Styling is hand-written modern CSS with a single crab orange-red accent
and light/dark themes.
The server wraps its existing gRPC service in tonic-web and composes one
axum router (auth layer -> grpc-web -> service, web bundle as fallback);
axum::serve replaces tonic transport, and native gRPC (h2c) still works.
The bundle is embedded via include_dir behind a build.rs that falls back
to a placeholder so a plain cargo build needs no wasm toolchain. To make
crabidy-core build for wasm, tonic is codegen-only there (transport
generation disabled) and native config loading is target-gated.
devenv gains the wasm toolchain and build-web/serve-web scripts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
crabidy-server.toml gains an [auth] section with one argon2 PHC hash
per role: owner (everything), queue-owner (queue and playback, no
library writes), queue-appender (browse, search, and Append only).
Enforcement is a single fail-closed tower layer in front of the tonic
service — unknown methods require owner, a malformed config aborts
startup, and a missing one keeps the server open as before. Successful
credentials are cached so argon2 runs once, failures re-verify at full
cost and stay indistinguishable. crabidy-server hash-password turns a
stdin password into the config hash; cbd-tui sends the header from new
user/password options.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Download captures now write straight into captures/<name>: satisfied
entries are reused, uncapturable tracks are recorded as skipped tomls
(a new fourth playable, marked red in the TUI and skipped by playback
with a bounded pass), and a failed run keeps its progress so capturing
the same name resumes it. The capture RPC replies on acceptance and
streams CaptureProgress over the update stream, rendered as status
lines in the library pane; help and the input overlay warn that
captures are slow. Colored list items switch to a dark foreground
under the focused selection bar so they stay readable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
crabidy-server and cbd-tui become libraries with thin mains:
crabidy_server::serve(addr) hosts the whole server stack,
cbd_tui::run(config) the client loops. The new cbd binary logs both
halves to one file, starts the server in-process, waits for the socket
(adopting an already-running standalone server on an occupied port),
and runs the TUI against it over the unchanged localhost gRPC wire.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
W on a downloadable library node mirrors the subtree into /captures
(a fourth fsdy instance) like a bookmark, but downloads every track's
audio next to its toml; the toml points at the sibling by relative
name, so captures play with no provider round trip. Nodes opt in via
the new is_downloadable flags — Tidal blesses queueable and
track-listing nodes. The bookmark walk is now the shared capture walk
parameterized by a per-track sink.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
w on a queueable library selection snapshots the whole subtree into a
third fsdy instance at /bookmarks: the orchestrator walks the source
iteratively and mirrors it as order-prefixed folders of link track
files (shared naming with queue persistence), tmp-and-swapped with
size caps so a runaway tree cannot fill the disk. One additive rpc,
CaptureLibraryNode(path, name), carries the flow; the TUI reuses the
input overlay prefilled with the selection title.
fsdy instances can now opt into an editable top level: root child
folders carry is_editable/is_deletable and support no-merge rename and
idempotent delete. /bookmarks mounts with it, and /queues too
(reserving current), so saved queues are renamable and deletable
through the existing e/d flows without TUI changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Queues now survive restarts, built entirely on the fs provider:
fsdy::Client is instance-mountable and a second, read-only instance
serves <config>/crabidy/queues/ as /queues. Every queue is a folder of
order-prefixed link track files plus a hidden state sidecar, written
only by the new QueueStore (tmp-and-swap). The playback loop streams
every queue change through a latest-wins watch channel to a debouncing
persister task and restores queues/current/ (tracks, position,
modifiers) at startup without autoplay. w on the queue pane asks for a
name and drives the previously stubbed SaveQueue rpc; reloading a
saved queue is just queueing /queues/<name>, since link entries
rewrite to their targets at listing time. The old "no links into /fs"
parse rejection gave way to one-hop link semantics so queues can
reference fs tracks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolving a nested node used to collect every track before the queue
changed: one broadcast at the very end, playback only after the full
walk, and the playback loop blocked for the duration. Now provider
resolution streams bounded chunks (tidaldy: one per 50-track page), the
playback loop applies and broadcasts each chunk as it lands, playback
starts with the first chunk, and Replace/Clear cancel in-flight
resolves down to the HTTP fetch. Queue.resolving (additive proto field)
drives an animated-dots pseudo-item in the TUI queue pane.
Also fixes Enter on a non-queueable library item blanking the queue
while audio kept playing, and the reversed album order left by the old
LIFO walk.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root causes found and fixed:
- QueueManager could panic and kill the playback task permanently:
is_last_track() underflowed on an empty queue, remove_tracks accepted
pos == len (Vec::remove panic) and corrupted positions when removing
multiple tracks (indices shifted mid-loop), shuffle_behind indexed
out of range on an empty play order, insert_tracks shifted play-order
entries by the queue length instead of the inserted count and then
assert!()ed on the resulting inconsistency, and clear() left
play_order stale. All mutation methods are now guarded, multi-remove
works highest-position-first, and an inconsistent play order is
rebuilt instead of panicking. Regression tests cover these cases.
- The tidal access token was only obtained at startup and never
refreshed, so long-running sessions ended with every track fetch
failing (playback just stopped at the next track boundary). Login
state now lives behind a lock; tokens are refreshed proactively
before expiry (5 min margin) and once reactively on a 401, and all
API responses are status-checked (new ClientError::ApiError) instead
of being fed to the JSON decoder blind. The http client also got a
30s timeout so a hung connection cannot wedge the provider loop.
- (from the rodio rewrite, same bug class) end of stream used to be
detected by string-comparing an io::Error message; any other decode
or network error ended the stream silently without an EndOfStream
message, so playback never advanced. EOS is now a guaranteed
callback with a generation counter.
Plus workspace-wide clippy cleanup (zero warnings), cargo-machete
cleanup, and fmt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The old pattern passed a Span in every channel message and entered it
with a guard that was held across await points, which misattributed
events from interleaved tasks. Messages are now {span, command} pairs:
the span is captured automatically at send time (Span::current) and the
consumer instruments the whole handler future with a child span
(playback_command/provider_command with a command name field), so events
are attributed correctly across the queue boundary and all the manual
in_current_span() plumbing is gone.
Also:
- server: EnvFilter with RUST_LOG support (default: own crates at
debug, rest at info), log-crate bridge for symphonia/cpal
- cbd-tui: logs to a file under the state dir (the terminal belongs to
the TUI), EnvFilter, no more println into the alternate screen
- tidaldy: fix misused levels (error->debug), structured fields,
payload dumps moved to trace, login flow at info/warn
- no panic on missing notification daemon in the TUI
- no panic on backwards clock steps in QueueManager
- provider init errors propagate instead of expect()
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tonic 0.9 -> 0.14 (tonic-prost/tonic-prost-build split), prost 0.14
- ratatui 0.20 -> 0.30 (Frame no longer generic, Line instead of Spans),
crossterm 0.29
- rodio 0.17 -> 0.22: replace the custom symphonia decoder with rodio's
built-in decoder, seeking (try_seek) and position tracking (get_pos);
end-of-stream is now signalled via an EmptyCallback source with a
generation counter so a replaced track can never emit a stale EOS
- replace the vendored stream-download crate with the published
stream-download 0.24 (rustls), with a 30s open timeout
- reqwest 0.12->0.13 (rustls/webpki-roots/query features), base64 0.22
Engine API, rand 0.10, flume 0.12, thiserror 2, dirs 6, toml 1
- unify everything under [workspace.dependencies]; drop unused deps
(once_cell, serde_json in server; confique, secrecy in tidaldy)
- devenv: add protobuf (protoc) for prost-build
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>