Commit Graph

27 Commits

Author SHA1 Message Date
Test User 4c0c5f1401 Audio: select the output device (fixes silent output on Raspberry Pi)
The player always opened the system default output device. On a
Raspberry Pi that default is often HDMI, so playback ran but nothing
came out of the headphone jack or a USB/DAC -- "it plays but I hear no
sound".

Add an [audio] device option to crabidy-server.toml: a case-insensitive
substring of the output device name (a memorable fragment is enough).
The player engine opens the first matching device and falls back to the
system default with a warning if none matches. Absent config keeps the
system default, so existing setups are unchanged.

To discover the names, a new `crabidy-server audio-devices` subcommand
(also on `cbd`) lists the output devices and marks the one the current
config selects, using the same match the server applies at startup.

Plumbing: audio_player::output_device_names() enumerates via cpal;
Player::new(Option<String>) replaces the device-less construction
(Default = new(None)); Playback::new takes the device and serve() reads
it from settings. cpal's name() is deprecated in favor of description(),
but name() returns the ALSA-stable string users see in `aplay -l` and
match against, so it is kept behind a documented #[allow(deprecated)].

README documents the [audio] device option under the Pi/config section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:13:23 +02:00
Test User 33fd3b227c Playback: retry the start when a skipped/short head runs the player dry
A queue replace starts playback from the first resolved chunk. If that
chunk's track is is_skipped, or is so short it finishes before the next
chunk resolves, play() found nothing playable and stopped the player --
and the later chunks return None (append mode), so playback never
resumed even though playable tracks were arriving right behind it. The
queue sat stopped with tracks in it. This is exactly the short/skipped
leading-track case the read-ahead is meant to cover.

A pending op now carries a wants_start flag: set when a chunk makes a
track current, cleared only once a start is confirmed (play now returns
whether it handed a track to the player). While set, each arriving chunk
retries the start from the current position -- next_playable_urls
advances past skipped/unplayable heads to the first track that has since
resolved. Once playback takes hold the flag clears, so later chunks only
extend the queue and never restart the playing track, and a user stop
after playback started is respected. An op whose whole resolve yields
nothing playable is dropped and the player stays stopped.

play() returns bool; the now-redundant play_if_some helper is removed.
Documented as architecture/progressive-queueing.md D5. Tests cover the
wants_start lifecycle (set on first current-making chunk, held across
later chunks, cleared on mark_started; never set appending behind a
playing queue).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:55:18 +02:00
Test User ef1e56e1f4 Playback: exponential read-ahead when resolving a queue
The resolve forwarder walked the paths one at a time. That starts the
first track quickly, but refills the rest only as fast as a single
provider resolve -- so when enumeration is slow and the leading tracks
are very short or skipped, playback drains the resolved queue faster
than it fills and stalls into silence.

Resolve the paths concurrently under a read-ahead window that starts at
1 and doubles after each path completes (1, 2, 4, 8, 16, then steady
16). The first path still resolves alone, so time-to-first-track is
unchanged; the window then grows geometrically, so the resolved queue
runs exponentially ahead of linear playback and a short/skipped head
cannot catch it. The cap bounds concurrent provider load.

Chunks are still forwarded in strict path order -- the forwarder fully
drains the oldest in-flight resolve before the next -- so concurrency
never reorders the queue, and the per-op cursor and "first chunk starts
the player" semantics are untouched. Cancellation drops the in-flight
receivers, stopping every concurrent resolve at once.

This is a read-ahead over paths; a single collection is still enumerated
by its provider's page streaming, so the win is for multi-item
selections. Documented as architecture/progressive-queueing.md D8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:46:31 +02:00
Test User 659e678522 Web client: prompt for login on visit when auth is enabled
The browser client already sent stored credentials and showed a login
form, but only when the server answered UNAUTHENTICATED -- i.e. only
when every role was guarded. With a fallback role configured, an
anonymous browser silently connected as that role and was never offered
a way to log in as a higher one.

The server now reports its auth on/off switch on the InitResponse
(auth_enabled, field 8), which is reachable anonymously. The RPC handler
stamps it from Authenticator::enabled(); the playback loop, which owns
queue state and not the auth config, leaves it false.

On first connect with no stored credentials against an auth-enabled
server, the web client raises the login dialog. It is dismissible --
"continue as guest" keeps the unauthenticated fallback role -- and is
shown once per session so stream reconnects do not nag. When the server
denies anonymous access outright (UNAUTHENTICATED), the same dialog
appears without the guest option, because credentials are then the only
way in.

Docs: architecture/roles-auth.md and web-client.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:51:55 +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 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 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 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 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
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
Hans Mündelein 24e968ac08
Start playing when adding to an empty queue
CI checks / stable / fmt (push) Successful in 3s Details
CI release / stable / cross-${{ matrix.target }} (x86_64-unknown-linux-gnu) (push) Successful in 5m52s Details
CI release / stable / cross-${{ matrix.target }} (aarch64-unknown-linux-gnu) (push) Successful in 7m41s Details
CI release / stable / cross-${{ matrix.target }} (armv7-unknown-linux-gnueabihf) (push) Successful in 7m40s Details
2023-06-14 11:49:27 +02:00
Hans Mündelein efc36e88f8
Don't error on failing broadcast channel send
CI checks / stable / fmt (push) Successful in 3s Details
2023-06-14 11:21:33 +02:00
Hans Mündelein 6db11131c0
Fix incorrect offset when turning off shuffle
CI checks / stable / fmt (push) Successful in 4s Details
2023-06-14 11:15:42 +02:00
Hans Mündelein f8a77ee6ed
Stop playing if last track is removed
CI checks / stable / fmt (push) Successful in 3s Details
2023-06-14 10:59:05 +02:00
Hans Mündelein 5c50544523
Unnwrap server code
CI checks / stable / fmt (push) Successful in 4s Details
2023-06-14 09:57:26 +02:00
Hans Mündelein 5d1a62c630
Add exclude current clearing for the server
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
stable / fmt Details
2023-06-12 22:18:34 +02:00
Hans Mündelein ef22a84021
Fix toggle shuffle bug 2023-06-10 11:06:42 +02:00
Hans Mündelein 06c02ac75c
Fix resume on delte current and queue on empty 2023-06-09 18:44:42 +02:00
Hans Mündelein 4b1546d91f
Send quue modifier updates 2023-06-09 16:55:25 +02:00
Hans Mündelein 9be9039a05
Add shuffle and repeat for server 2023-06-09 16:02:35 +02:00
chmanie d782adb506 Hopefully unstuck the player 2023-06-09 03:14:06 +02:00
Hans Mündelein a31608aae9
Integrate new audio player 2023-06-08 16:10:17 +02:00
Hans Mündelein af63d50dc5
Add missing files after refactoring 2023-06-03 17:48:51 +02:00