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>
notify-rust defaults the app-name to the executable file name
(cbd-tui), so notification-daemon rules keyed on app-name break
whenever the binary is renamed or wrapped. Set it explicitly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The player position can overrun a stale or wrong duration (streams,
hand-written track files), and ratatui's LineGauge asserts its ratio
into 0..=1 — the TUI died with "ratio should be between 0 and 1"
mid-playback. Clamp instead, with render regression tests for both the
overrun and the zero-duration case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.track.toml was too generic for files that only crabidy understands;
the cbd- prefix makes them unmistakable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A new fsdy crate exposes a configured root directory as /fs:
*.track.toml files are track nodes carrying metadata plus exactly one
playable reference — a local audio file, an http(s) URL, or a
crabidy-internal link. Link tracks rewrite Track.path to the target at
listing time, so playback routes through the existing prefix routing;
links into /fs are rejected, making chains impossible. Paths are
percent-encoded segments validated in one place (no root escape), the
orchestrator wires the provider optionally (a broken local config only
costs the /fs subtree), and the default chunked resolve walk provides
progressive queueing for free.
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>
Search-term nodes created via % are now modifiable: e renames the
selected node (prefilled overlay; the new title re-runs the search,
colliding titles merge) and d deletes it, both gated on new additive
LibraryNodeChild.is_editable/is_deletable flags and marked [ed] in the
library list. Two new rpcs follow the create contract: RenameLibraryNode
returns the renamed node (the TUI navigates into it), DeleteLibraryNode
returns the refreshed parent listing. Queued tracks from a renamed or
deleted term keep playing; verified end-to-end against the live API.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pressing % inside /tidal/search opens an input line; the entered term
becomes a tree node whose contents are the search results: track hits
queueable in place, artist and album hits as canonical /tidal/artists
paths. New CreateLibraryNode rpc + is_creatable flags (wire-compatible),
ProviderClient::create_lib_node routed by prefix, percent-encoded term
segments in crabidy-core, and a modal input overlay in the TUI with
creatable nodes marked [%]. Search terms live in memory for the process
lifetime; term nodes are deliberately not queueable so the resolve sweep
cannot drag whole discographies into the queue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pressing ? opens an overlay listing usage notes and every key binding.
The bindings now live in one declarative table (app/bindings.rs) that
both key dispatch and the help modal render from, so the help can never
drift from the real bindings. Includes the dev-flow design artifacts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Playing a network track panicked in rodio 0.22's symphonia backend:
"Seek errors should not occur during initialization". During init
symphonia probes the container length; on a source with no known byte
length it seeks from the end, and StreamDownload handles that in a way
rodio turns into an `unreachable!`. This surfaced after the dependency
bump from rodio 0.17 (with the old custom SymphoniaDecoder) to 0.22.
Pass the stream's content length to the decoder via with_byte_len (the
local-file path already did this), which lets symphonia skip the
end-relative seek entirely. Verified against a real Tidal AAC/mp4
stream: the streamed decode now succeeds instead of panicking.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tidal retired the built-in client id (internal cid 3235): device login
still issues tokens for it, so browsing kept working, but token refresh
returns "Client id 3235 not found" and playbackinfopostpaywall 401s, so
no track ever resolves stream urls. This is a Tidal-side retirement, not
a regression -- the pre-fable code shipped the identical credentials and
fails the same way.
Swap in a currently-live client id + secret, verified end to end against
a real account: refresh succeeds and playbackinfopostpaywall returns a
playable BTS manifest. The XOR "hidden from fulltext search" scheme is
kept, but the obfuscated payloads are now explicit byte arrays rather
than raw control-character string literals, so the change is reviewable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A stale config can hold a refresh token issued by a Tidal client that
no longer exists ("Client id ... not found") next to an access token
that is still perfectly valid. The proactive refresh in
ensure_fresh_token treated its own failure as fatal, so every request
died on the unusable refresh token without ever trying the working
access token.
Classify refresh failures: a 4xx from the token endpoint is permanent,
so warn once, stop proactive refreshing, and keep serving with the
current access token; transport errors and 5xx are transient and keep
the retry metadata. The 401-retry in make_request stays as the backstop
for an access token that has actually expired, and a restart heals the
state via the device-login fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the current design in architecture/overview.md: crate topology,
the three concurrency domains and the channels between them, the
filesystem-like path addressing scheme, the span-carrying message
envelope used for tracing, and the Tidal device-flow auth.
Captures the trade-offs behind the load-bearing choices (paths vs opaque
ids, blocking engine thread, lossy broadcast) and the known gaps, so
later changes can see what was traded away.
Adds d2 to devenv for rendering the embedded diagrams.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tidal edge WAF rejects HTTP Basic-Auth on /oauth2/token with a 403
HTML page, which the poll loop treated as an ordinary "not yet
authorized" response and retried with no logging, making a broken
login indistinguishable from a slow one until the code expired.
Send the client secret in the request body instead (also fixed for
token refresh), and classify poll outcomes explicitly so pending polls
are logged and real failures surface immediately with the actual
Tidal error.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tracing-subscriber .init() already installs the LogTracer via its
default tracing-log feature; the explicit init only produced a startup
warning. Remove it and the direct tracing-log dependency.
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>
Identifiers like node:tidal / node:playlist:<id> / track:<id> are
replaced by absolute, hierarchical paths that encode the position in
the library tree:
/ global root
/tidal provider root
/tidal/playlists/<id> playlist (tracks inside)
/tidal/playlists/<id>/<track> track
/tidal/artists/<id>/<album> album
/tidal/artists/<id>/<album>/<t> track
- proto: uuid -> path, uuids -> paths (same field tags, wire
compatible); crabidy-core gains ROOT_PATH, parent_path, join_path,
path_segments helpers with unit tests
- ProviderClient gains is_track_path; the orchestrator routes by path
prefix and exposes a single ResolveTracks command (track path ->
that track, node path -> flattened subtree), replacing the
track:-prefix sniffing in the playback loop
- tidaldy parses paths into a typed TidalPath enum; node parents are
derived from the request path, which removes the album.artist
unwrap() panic; the network-dependent scratch test is #[ignore]d
- TUI navigates by paths
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>
Adds devenv.nix (with alsa-lib and pkg-config for rodio/cpal builds),
rust-toolchain.toml pinning stable, and updated .gitignore.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>