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>
Pressing / in either pane opens a live case-insensitive substring
filter: typing narrows the visible rows, Enter keeps the filter and
returns to navigation, Esc clears it. A shared Filter helper keeps each
pane full list intact and maps view indices to real ones, so movement
keys work on the filtered view unchanged and the queue maps a filtered
selection back to the real server position before removing or setting
current. The library resets search on node change; the queue preserves
it across stream updates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deletion (d) previously reached only top-level folders of editable
stores. /captures now exposes its whole tree: nested folders delete
recursively, single tracks delete their metadata file plus the
downloaded audio next to it (never audio outside the instance root).
Tracks advertise this through the new LibraryNode.tracks_deletable
flag. Because these deletes destroy slow-to-redo downloads, the TUI
asks delete <title>? [y/N] first; cheap deletables (search terms,
bookmarks, saved queues) stay unconfirmed.
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>
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>
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>
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>
- 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>