Commit Graph

81 Commits

Author SHA1 Message Date
Test User 378066470e rss: subscribe to podcast feeds at /rss, including premium ones
A new rssdy crate mounted at /rss. Subscriptions are (name, url) pairs in
rss.toml; `%` on /rss takes a pasted feed URL, fetches it once, names the
subscription from the feed's own title and persists it, `e` renames, `d`
unsubscribes without touching captured audio. Feeds are read as RSS
2.0/1.0/0.x, Atom or JSON Feed through feed-rs.

**A premium feed URL is the credential.** Library paths are displayed,
logged, and persisted into saved queues and bookmark tomls, so a URL in one
leaks into all of them. Paths therefore carry a slug of the subscription name
plus blake3(guid)[..16] — /rss/the-economist-podcasts/676f8bfa48c9cac3 — and
URLs are redacted from every Debug impl and kept out of errors (reqwest goes
through without_url).

**Nothing is cached, at either end.** A listing always fetches. The half that
is easy to miss is client-side: both clients cache listings by path and only
/crabidy, /fs and /orphans bypassed it, so /rss joins MUTABLE_ROOTS in both —
otherwise a re-visit answers from the client and the server's freshness is
invisible. One memo, written by listings and read only when resolving a track
(bounded to 8 feeds), keeps queueing 40 episodes at one fetch instead of 41
without a TTL to guess at.

Verifying against the user's real Economist feed caught a bug no unit test
would have: feed-rs parses <itunes:duration> as NPT, which has no MM:SS form,
so "53:25" fell through to its leading-number regex and a 53-minute episode
reported 53 *seconds* ("1:20:40" happens to parse fine). That field is now
recovered from the raw body — a shallow scan keyed by guid and enclosure URL —
and the live feed reports 3205/2830/1662 s, matching 53:25/47:10/27:42.

Bounded by design: per-request timeout, an 8 MiB body cap enforced while
reading chunks rather than after the fact, an episode cap, newest-first
enforced at the provider boundary so any backend obeys it. A malformed entry
is skipped; only an unfetchable feed errors, and it fails that node alone.

Behind a default-on `rss` cargo feature like every other provider, with a row
in check-features. Documented in docs/src/providers/rss.md and
rssdy/README.md, both stating plainly that the URL is a credential, that
listings are never cached, and that bookmarks depend on publisher guids —
capture what you want to keep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:36:30 +02:00
Test User 7fe4326923 queue: insert at a position, not after it, and show queue marks
Two bugs from the register work, both reported from actual use.

**Paste landed one row too far.** `QueueManager::insert_tracks` spliced at
`position + 1` — it inserted *after* the given index, while its own CLI help
says "insert tracks/subtrees at a position". So `p` (which sent cursor + 1)
landed two below the cursor and `P` (cursor) landed one below, exactly as
reported. The clients were already computing the right indices for an
insert-*at* API.

Fixed at the primitive rather than in the clients, because "after" cannot
express the front of the queue: the earliest reachable index was 1, so
pasting before the first row — and therefore undoing a delete of it — was
impossible. `insert_tracks` now inserts **at** `position`, pushing that row
down, with 0 the front and past-the-end an append. The two callers that
genuinely mean "after" pass `N + 1`: `ResolveKind::InsertAfter` (which keeps
its name and its streaming-chunk arithmetic) and `queue_tracks` (play-next,
`L`). All existing behaviour is preserved — the whole server suite passes
untouched — and three tests pin the new front/interior/play-next cases.

`queue insert <POS>` on the CLI shifts by one accordingly, which brings it
in line with what its help always claimed. Documented in the proto, the CLI
help, and the book.

**Queue marks were invisible.** The TUI rendered no mark indicator, so `s`
and visual mode had no feedback. Marked rows now carry the library's `*`
prefix and the same green bold; the playing row keeps `>` and its red, and a
row that is both shows `> * title`. The web client already rendered marks
(its `.marked .title` rule), but neither client showed visual mode outside
the TUI's pane title — both panes there now get a VISUAL badge in the
toolbar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:04:03 +02:00
Test User 95700bf31f cbd-web: queue marks, visual mode, and the register
Brings the web client level with the TUI in the same change rather than
deferring parity again — which also closes the library visual mode that was
left out when v/V landed in the terminal.

The web queue owns no list (just a cursor, with rows rendered straight from
the server signal), so its marks live on QueueCursor beside the snapshot:
mark flags plus the paths they were taken against, carried across each
update by the same greedy in-order path match the TUI uses. Same rule, same
seven reconciliation cases tested here too, so the two clients cannot drift.

Adds s / v / V / y / p / P in the queue, v / V / y in the library, marks
rendered on queue rows, register count in the queue toolbar, and the visual
auto-leave rule. The "insert" button became "paste". Library movement now
paints in visual mode — a dead-code warning on the wasm target was what
caught that it did not.

Docs: the TUI page gains a register section (what it is, and that it is per
client, one slot, and re-resolves paths on paste), the web page points at
it, queue.md explains why Remove takes positions and Insert takes paths,
and the README walkthrough covers the keys. All of them state plainly that
p changed meaning.

Verified on both targets: cbd-web clippy is clean for native *and*
wasm32-unknown-unknown (mod app only compiles for wasm, so native alone
proves nothing), 20 tests pass, and the trunk bundle builds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:35:21 +02:00
Test User ad19b6352e cbd-tui: queue marks, visual mode, and a paste register
The queue could only delete the row under the cursor, one at a time
(queue.rs carried the FIXME asking for exactly this), and nothing in the
system had an undo. Both gaps close with one vim-shaped concept.

Marks and visual mode move into a MarkedPane trait in list.rs, next to the
existing StatefulList, and both panes implement it — the library keeps its
behaviour verbatim (its whole visual-mode suite passes untouched), the
queue gains it with an always-allowed mark gate, since its rows carry
is_queable: false.

The register is one unnamed in-memory slot holding library paths, written
only by y (both panes) and d/c/C in the queue, and read by p (insert after
the cursor) and P (before it). So d then P restores exactly what you
deleted, d … p is a move, and clearing 200 tracks with C is finally
recoverable. Paste leaves the register intact and an empty register pastes
nothing rather than sending an empty Insert.

No server work: Remove already accepted many positions and Insert already
took a path list. Because the register holds paths, paste re-resolves — a
yanked album node expands at paste time, and a path that no longer resolves
does not come back.

The one genuinely hard part is that queue marks are positional while the
queue is server-pushed and rebuilt on every change. carry_marks() carries
marks across a snapshot by a greedy in-order match on track path, so a mark
follows its own track through appends, removals, and playback advancing
instead of silently retargeting; irreconcilable snapshots clear rather than
guess. Positions handed to Remove are always read off the newest list.

Breaking change: p in the queue pastes the register instead of inserting
the library selection. That flow is now y then p; a/L/Enter are untouched.
Library queue_insert() had no other caller and is gone.

Also rebalances the help modal's columns (Global + Queue left, Library
right). It was already overflowing at 46 rows in one column; the new
bindings made that worse. It still truncates below ~43 rows — pinned by a
test rather than hidden, and scrolling remains the real fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:24:51 +02:00
Test User a03e3de84e build: put every provider, opus, and the spectrum behind cargo features
All on by default, so a plain build is unchanged (verified: the default
dependency set for crabidy-server is byte-identical to before). Tailor a
smaller binary with --no-default-features --features …
(architecture/build-features.md).

Compile-time features draw dependency boundaries; the existing
crabidy-server.toml providers list keeps doing per-mount runtime
toggling. The compile-time set bounds the runtime one: a provider built
out cannot be enabled from the config, and naming it earns one startup
warning rather than silence.

- crabidy-server: tidal · youtube · fyyd · abs · soundcloud · jamendo ·
  fs · opus · spectrum · web-ui, plus the all-providers group.
- fs is local files *and* persistent state (D5): the /fs mount, the
  content store behind /crabidy and /orphans, bookmarks/captures, queue
  persistence, and scan. Without it Capture/SaveQueue answer
  Unimplemented and scan says which feature is missing — never a panic.
- opus drops symphonia + symphonia-adapter-libopus, and with them the
  bundled libopus C build (no more cmake requirement). It also decides
  whether scan indexes .opus at all, so scan never indexes what this
  build cannot play. An Ogg-Opus file in an opus-less build reports the
  missing feature and is skipped like any undecodable file.
- spectrum drops realfft and the FFT task; clients just never receive a
  frame. cbd-tui gains notifications (notify-rust, a D-Bus stack).
- crabidy-server/cbd features print the compiled set, and startup logs
  it, so a tailored binary is self-describing.

Not gated, deliberately: [auth]/argon2 (a build ignoring configured
hashes would run open — fail-open security hole), and hls.rs /
spectrum_tap.rs / windowed_http.rs (no dependency of their own, so
gating them buys cfg noise and nothing else).

devenv gains check-features: the curated matrix (defaults, nothing, each
provider alone, each axis dropped, both worked examples, the client
crates) all clippy-clean under -D warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 03:15:32 +02:00
Test User 2e94760a69 cbd-tui: fix visual mode stranding the turnaround row (anchor the range)
The per-step paint toggled the row you arrived at, so going down then
back up toggled off the rows re-entered but never the furthest row you
turned around on — it stayed marked. Anchor the selection instead: on
entering visual mode record the anchor row, and on each move reconcile
marks to the contiguous range [anchor, cursor], toggling only the rows
whose membership changed. Moving back now cleanly reverses; jumps
reconcile the whole span. visual state becomes Option<usize> (the
anchor). Adds a regression test (down then fully up leaves only the
anchor); 90 cbd-tui tests green, clippy/fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 16:29:26 +02:00
Test User 0a9456e173 cbd-tui: add library visual (paint-select) mode on v/V; move spectrum to f
Press v or V (both the same) to enter visual mode in the library pane;
movement then toggles the mark of every row it sweeps over, so a run of
items is selected by v then moving (g/G and Ctrl-d/u paint the whole
span). Entering toggles the current row (vim-style); a second v/V or Esc
leaves the mode with marks kept, and any other action leaves it first
then runs. This frees v, so the frequency-spectrum toggle moves from v
to f.

Painting reuses the existing marks (is_queable-gated, filter-mapped);
no wire, proto, or server change. Library-only for now — the queue has
no marks yet. Ran the full dev-flow; artifacts under architecture/,
quality/, plan/. 89 cbd-tui tests green (14 new); clippy and fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 16:20:55 +02:00
Test User 3a03114cb9 Always refetch /orphans (and fix the web client's stale cache roots)
The clients cache library listings except for server-mutable folder roots.
/orphans is recomputed from the store on every visit, so a cached listing
froze the orphan set until a client restart. Add /orphans to the TUI's
mutable-roots list so entering the provider always re-walks.

The web client's list was also stale from the store refactor — it still
named the removed /captures, /queues, /bookmarks providers and omitted
/crabidy, so /crabidy (and now /orphans) listings went stale there too.
Reset it to the real mutable roots: /crabidy, /fs, /orphans.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 00:13:50 +02:00
Test User 7eaa8fa9b4 tui: clear-filter on Esc, captured arrow marker, spectrum toggle
Three terminal-UI refinements:

- Esc in navigation clears an active / filter (new ClearSearch action);
  Enter keeps the filter and returns to navigation, / re-opens editing.
- Captured rows render a trailing down-arrow at the end of the row
  (outside the action brackets), visible while browsing any provider.
- v toggles the frequency spectrum at runtime; the spectrum client-config
  value still sets the startup default. The server keeps computing and
  streaming the bars regardless.

README and the mdbook (clients/tui.md, store.md) updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 22:35:51 +02:00
Test User 0a0d50f748 Implement the comprehensive CLI (stage 5)
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>
2026-07-22 12:13:51 +02:00
Test User dbd1b955fb Implement the crabidy content store and single /crabidy provider
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>
2026-07-22 11:34:40 +02:00
Test User 21d4fddb2f Design the crabidy content store; add wire + fsdy foundations
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>
2026-07-22 10:58:11 +02:00
Test User ef69afdd5f Capture local files by copying them; add W to capture the queue
Two capture fixes.

Capturing already-local playables: a download capture recorded any
non-http source as skipped, so capturing an fs node or a queue mixing
streamed and local tracks produced red, audioless entries even though
the audio was on disk. fetch_track now copies a local-file source into
the capture next to its toml (source extension kept, counted against the
byte budget); a missing or unreadable source still records skipped.

Queue W: the queue pane only had w (save), so capturing the queue meant
save, navigate, then W. Shift-W in the queue now download-captures the
continuously persisted /queues/current directly.

Deferred to a later refactor: relocating the internal stores out of
.config into .local/state, and a central content-addressed audio store
so captures dedup and link instead of copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 01:08:29 +02:00
Test User 80c4b6e7ed Implement mute (it was a stub)
Toggling mute did nothing: the server logged a FIXME and never touched
the player, and the TUI ignored the Mute stream update. Now the player
engine mutes by zeroing the sink volume and remembering the level to
restore (setting the volume unmutes), ToggleMute drives it and
broadcasts the new state, the TUI shows a Muted marker in the
now-playing pane, and the web client mute button already reflected the
Mute update so it now works too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 00:20:01 +02:00
Test User 7f34f869a9 Fill the now-playing pane to the bottom of the right column
The right column split capped the now-playing pane at Max(10), which
left the rows below it unallocated — a gap under the spectrum (verified:
on a 40-row area the pane ended at row 38). Min(10) lets the pane grow
to the bottom instead, so the spectrum fills all the space left below
the queue and scales with the terminal height.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 00:13:12 +02:00
Test User bdfc0ec29c Size the now-playing info block to its content, spectrum fills the rest
Instead of a fixed info-block height, derive it from the number of info
lines (4 with a track, 3 without) plus the border, and let the spectrum
take all remaining rows. Both regions now size themselves: the info
block is exactly as tall as it needs, the bars fill everything left.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 00:08:26 +02:00
Test User b3fec6d3c0 Make the TUI spectrum bars fill the height below the progress
The bars were a single glyph row because the info block grew to fill
the now-playing pane. The info block now takes a fixed height and the
spectrum fills the remaining rows, drawn as full-height columns (full
blocks stacked from the bottom, a partial block for the fractional top
cell) instead of one row of sub-cell glyphs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 00:06:49 +02:00
Test User 0b9970b550 Make the spectrum bars actually visible
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>
2026-07-22 00:00:58 +02:00
Test User 572be04206 Add a server-streamed frequency spectrum visualizer
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>
2026-07-21 23:14:03 +02:00
Test User 550237f69a Add a / search filter to the TUI library and queue panes
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>
2026-07-21 22:54:39 +02:00
Test User 4e59e50943 Gate the gRPC surface behind basic-auth roles
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>
2026-07-21 20:47:06 +02:00
Test User 1c83217745 Delete captures from disk at any depth, behind a confirmation
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>
2026-07-21 20:34:03 +02:00
Test User c84dec9ca2 Refetch mutable library listings and redact stream URLs from logs
The TUI cached every library listing for the whole session, so a
finished capture (or a saved queue) never appeared under /captures
until a restart — captures looked broken while they had succeeded on
disk. Listings under /captures, /queues, /bookmarks, and /fs are now
always refetched (cheap local walks on the server); remote provider
nodes keep the instant back-navigation cache.

The player engine also logged full stream URLs (including googlevideo
sig tokens) through its play span; sources are now logged as
scheme://host only, local paths verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 18:37:10 +02:00
Test User 214dece19f Make download captures incremental with skipped tracks and progress
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>
2026-07-21 15:45:04 +02:00
Test User 58f7f9c66b Bundle server and TUI into a single cbd binary
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>
2026-07-21 14:15:47 +02:00
Test User 7f0e900c56 Capture library subtrees with downloaded audio
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>
2026-07-21 13:45:04 +02:00
Test User 655e8054b8 Capture library subtrees as bookmarks
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>
2026-07-21 11:11:27 +02:00
Test User 4e44f672e8 Persist queues as fs-provider folders
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>
2026-07-21 10:41:49 +02:00
Test User 20e071f0af Pin the desktop notification app-name to crabidy
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>
2026-07-21 02:20:29 +02:00
Test User 8bb653ab3e Clamp the progress gauge ratio
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>
2026-07-21 02:06:02 +02:00
Test User ccab43a133 Queue large collections progressively
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>
2026-07-21 01:08:34 +02:00
Test User 333c6e040a Add rename and delete for modifiable library nodes
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>
2026-07-20 23:34:47 +02:00
Test User d741523e53 Add Tidal search as creatable library nodes
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>
2026-07-20 17:58:44 +02:00
Test User 8586194096 Add a help modal to cbd-tui behind a declarative binding table
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>
2026-07-20 17:27:12 +02:00
AI User d504ebc85f Fix intermittent playback stops and harden the queue
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>
2026-07-19 22:00:22 +02:00
AI User 56bc0b0d04 Refactor provider addressing to filesystem-like paths
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>
2026-07-19 21:53:12 +02:00
AI User 6d8bc7f166 Overhaul tracing: fix span misattribution, broaden coverage
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>
2026-07-19 21:48:00 +02:00
AI User 6eb5a87b15 Update all dependencies to current versions
- 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>
2026-07-19 21:41:10 +02:00
chmanie a1987869c2 Styling fixes
CI checks / stable / fmt (push) Has been cancelled Details
2023-10-08 23:48:49 +02:00
chmanie 2f89886e5d Show album and release date
stable / fmt Details
stable / cross-${{ matrix.target }} (aarch64-unknown-linux-gnu) Details
stable / cross-${{ matrix.target }} (armv7-unknown-linux-gnueabihf) Details
stable / cross-${{ matrix.target }} (x86_64-unknown-linux-gnu) Details
2023-06-13 00:59:54 +02:00
chmanie 4b34fa7233 Add key command to select currently playing track
stable / fmt Details
stable / cross-${{ matrix.target }} (aarch64-unknown-linux-gnu) Details
stable / cross-${{ matrix.target }} (armv7-unknown-linux-gnueabihf) Details
stable / cross-${{ matrix.target }} (x86_64-unknown-linux-gnu) Details
2023-06-13 00:17:56 +02:00
chmanie 02f47d682b Implement clear queue in tui
stable / fmt Details
stable / cross-${{ matrix.target }} (aarch64-unknown-linux-gnu) Details
stable / cross-${{ matrix.target }} (armv7-unknown-linux-gnueabihf) Details
stable / cross-${{ matrix.target }} (x86_64-unknown-linux-gnu) Details
2023-06-12 22:07:41 +02:00
chmanie 17a0e8606e Try to add release files
stable / fmt Details
stable / cross-${{ matrix.target }} (armv7-unknown-linux-gnueabihf) Details
stable / cross-${{ matrix.target }} (aarch64-unknown-linux-gnu) Details
2023-06-12 18:06:00 +02:00
chmanie 79ac68479e Remove unused import statements
ubuntu / stable / cross-${{ matrix.target }} (aarch64-unknown-linux-gnu) Details
ubuntu / stable / cross-${{ matrix.target }} (armv7-unknown-linux-gnueabihf) Details
ubuntu / stable / fmt Details
2023-06-12 13:03:24 +02:00
chmanie 7f48bca5df Split up cbd-tui into components 2023-06-11 20:06:06 +02:00
chmanie d970e372af TUI: Add config file and argument parsing w/ clap 2023-06-11 01:02:14 +02:00
chmanie a65ad793dc Implement shuffle, repeat and their indicators 2023-06-10 13:01:29 +02:00
chmanie 773cb511e0 Implement multi track queue, replace, append, insert 2023-06-09 18:47:27 +02:00
chmanie 191ed4eed2 Always send prev and next 2023-06-09 15:05:39 +02:00
chmanie ef6249d9d7 Improve completion widget 2023-06-09 15:05:17 +02:00