diff --git a/README.md b/README.md index 4d7eac7..171bbc8 100644 --- a/README.md +++ b/README.md @@ -1 +1,101 @@ # crabidy + +A client/server music player. A headless gRPC server owns the library, +the play queue, and audio output; a terminal UI connects to it over +localhost (or the network). Media comes from pluggable **providers**, +each mounted as a subtree of one library: + +```text +/ +├── tidal Tidal streaming (see tidaldy/README.md) +├── youtube YouTube search & playlists (see ytdy/README.md) +├── fs a local music folder (see fsdy/README.md) +├── queues saved play queues (managed by the server) +├── bookmarks link snapshots of library subtrees (`w`) +└── captures downloaded snapshots with local audio (`W`) +``` + +## Binaries + +- `crabidy-server` — the server: providers, queue, playback, gRPC on + `0.0.0.0:50051`. +- `cbd-tui` — the terminal client. Press `?` inside for all key + bindings. +- `cbd` — both in one process: starts the server, waits until it + accepts connections, then runs the TUI. Adopts an already-running + server instead of failing on an occupied port. + +## Quick start + +The toolchain is managed by [devenv](https://devenv.sh): + +```sh +devenv shell # provides rust, yt-dlp, and friends +cargo run -p cbd # server + TUI in one process +``` + +Or run the halves separately: `cargo run -p crabidy-server` and, in +another terminal, `cargo run -p cbd-tui`. + +## Configuration + +All configuration lives in `~/.config/crabidy/` (the platform config +directory). Every file is optional; missing providers simply do not +mount. Files are created/rewritten on first start with their defaults +filled in. + +| File | Component | Documentation | +| -------------- | --------- | -------------------------------------- | +| `tidaly.toml` | Tidal | [tidaldy/README.md](tidaldy/README.md) | +| `ytdy.toml` | YouTube | [ytdy/README.md](ytdy/README.md) | +| `fsdy.toml` | local fs | [fsdy/README.md](fsdy/README.md) | +| `cbd-tui.toml` | TUI / cbd | below | + +The server-managed folders (`queues/`, `bookmarks/`, `captures/`) also +live in `~/.config/crabidy/`; they need no configuration and hold plain +folders of track files in the format documented in +[fsdy/README.md](fsdy/README.md). + +### `cbd-tui.toml` + +Configuration of the TUI (and the TUI half of `cbd`): + +```toml +# Where to find the server. Default shown. +address = "http://127.0.0.1:50051" +``` + +Every option is also available as a command-line flag +(`cbd-tui --address ...`). + +## Using the library + +Navigation is vim-style: `j`/`k` select, `l` enters the selected +folder, `h` goes to the parent, `Tab` switches between library and +queue, `Enter` replaces the queue with the selection. `%` creates a +node where the pane title shows `% to add` (e.g. a search term), `e` +renames, `d` deletes. + +- `w` saves the selection as a **bookmark** (links; needs the source + provider to replay) or, in the queue pane, saves the queue. +- `W` **captures** the selection: the subtree is mirrored under + `/captures/` with every track's audio downloaded next to its + metadata — fully local playback afterwards. Captures are incremental: + re-capturing the same name resumes and completes it; tracks whose + source cannot be captured are recorded as *skipped* (red in the UI, + skipped by playback). Download captures can take long; progress is + shown in the library pane. + +Press `?` for the full binding table. + +## Logs + +`cbd` and `cbd-tui` log to `~/.local/state/crabidy/` (daily files); +`crabidy-server` logs to stderr. Stream URLs and credentials are +redacted from logs by design. + +## Development + +Design documents live in `architecture/`, per-feature quality gates in +`quality/`, and implementation plans in `plan/`. See `CLAUDE.md` / +`AGENTS.md` for the development workflow and coding rules. diff --git a/fsdy/README.md b/fsdy/README.md new file mode 100644 index 0000000..cb4483c --- /dev/null +++ b/fsdy/README.md @@ -0,0 +1,61 @@ +# fsdy — the filesystem provider + +Mounts a local directory at **`/fs`** in the crabidy library. The same +engine also powers the server-managed mounts `/queues`, `/bookmarks`, +and `/captures` — everything below applies to their on-disk format too. + +## How it works + +The provider walks one configured root directory. Every **directory** +becomes a queueable library node; every file ending in +**`.cbd-track.toml`** becomes a track; everything else (audio files, +covers, hidden entries) is invisible to the library. Listing order is +case-insensitive by file name — prefix files with numbers to fix an +order (the capture/bookmark writers use `0001`-style prefixes for +exactly this reason). + +A track file carries the track's metadata plus a reference to the +playable thing: + +```toml +title = "We Will Rock You" # required +artist = "Queen" # optional +duration = 122 # optional, seconds + +[album] # optional +title = "News of the World" +release_date = "1977-10-28" + +# Exactly ONE of the following four: +[playable] +file = "we-will-rock-you.flac" # local audio; absolute, or relative to + # this file's directory (relocatable) +# url = "https://example.org/radio.mp3" # http(s) stream +# link = "/tidal/artists/1/2/3" # another provider's track +# skipped = true # no audio: a capture recorded its + # source as uncapturable; shown red, + # skipped by playback +``` + +Malformed track files are skipped with a warning; they never break the +listing. `link` playables resolve exactly one hop (a link to a link +fails at play time), which keeps cycles impossible. + +## Configuration — `~/.config/crabidy/fsdy.toml` + +```toml +# Absolute path of the directory to expose under /fs. +# Default: the platform music directory (e.g. ~/Music). When neither is +# available the /fs mount is disabled — the rest of the server runs on. +# root = "/home/me/music" +``` + +## The server-managed instances + +`/queues`, `/bookmarks`, and `/captures` are fsdy instances over +`~/.config/crabidy/{queues,bookmarks,captures}/`, written by the server +(queue persistence, `w`, `W`) and readable/editable with any file +manager — they are just folders of the track files described above. +Their top-level folders can be renamed (`e`) and deleted (`d`) from the +TUI; `/queues/current` is the continuously persisted play queue and is +protected. diff --git a/tidaldy/README.md b/tidaldy/README.md new file mode 100644 index 0000000..f1b4d09 --- /dev/null +++ b/tidaldy/README.md @@ -0,0 +1,61 @@ +# tidaldy — the Tidal provider + +Mounts your Tidal account at **`/tidal`** in the crabidy library, using +Tidal's web API. + +## How it works + +On first start the provider runs Tidal's OAuth **device login**: the +server prints a `link.tidal.com` verification URL to its stdout (for +`cbd`, check the log file) — open it in a browser, authorize, and the +provider finishes logging in by itself. The obtained tokens are written +back into the config file and refreshed automatically from then on; you +should not need to log in again. + +The tree offers your playlists, favorite artists (with their albums and +tracks), and a **search** node: press `%` under `/tidal/search` to +create a search term; the term becomes a persistent child node holding +its results (tracks, artists, albums). Terms are renamable (`e`, +re-runs the search) and deletable (`d`). + +Everything queueable is also capturable: `w` bookmarks a subtree as +links, `W` downloads a subtree's audio into `/captures/`. + +## Configuration — `~/.config/crabidy/tidaly.toml` + +The file is rewritten on every server start with the current values +(including refreshed tokens), so **it contains your credentials — keep +it private**. All options with their defaults: + +```toml +# Tidal API endpoints. Change only if you know why. +base_url = "https://api.tidal.com/v1" +hifi_url = "https://api.tidalhifi.com/v1" + +# Stream quality: "Low" | "High" | "Lossless" | "HiRes". +audio_quality = "Lossless" + +# Managed by the provider: filled in by the device login and refreshed +# automatically. Delete this whole section to force a fresh login. +[login] +# device_code = ... +# user_id = ... +# country_code = ... +# access_token = ... +# refresh_token = ... +# expires_after = ... + +# OAuth client identity used for the device flow. Working defaults are +# built in; override only to use your own client registration. +[oauth] +# client_id = ... +# client_secret = ... +base_url = "https://auth.tidal.com/v1/oauth2" +``` + +Notes: + +- A missing or empty file is fine: defaults are used and the device + login runs on the next start. +- If Tidal ever invalidates the session (long offline periods), delete + the `[login]` section and restart. diff --git a/ytdy/README.md b/ytdy/README.md new file mode 100644 index 0000000..10e05ab --- /dev/null +++ b/ytdy/README.md @@ -0,0 +1,79 @@ +# ytdy — the YouTube provider + +Mounts YouTube at **`/youtube`** in the crabidy library. + +## How it works + +Metadata comes from the pure-Rust [rustypipe] Innertube client: + +- **`/youtube/search`** works without any login. Press `%` to create a + search term; the term becomes a persistent child node listing the top + video results as tracks. Terms are renamable (`e`, re-runs the + search) and deletable (`d`). Terms live in memory — a server restart + forgets them, but navigating to an old term path recreates it. +- **`/youtube/playlists`** appears when a cookie login is configured + and accepted: it lists the account's saved playlists. + +**Stream URLs** are resolved through a minimal `yt-dlp` sidecar when +the binary is available (one bounded subprocess call per played or +captured track — no other functionality uses it). This is a deliberate +concession: YouTube currently caps tokenless stream URLs at their +leading ~1 MiB, and `yt-dlp` is the only maintained solver for the +cipher challenges that lift the cap; its URLs stream whole files at a +throttled ~32 KB/s (still ~2× audio bitrate). Without the binary the +provider still works, but playback of a track stops after roughly a +minute. Streams prefer `audio/mp4` (AAC), the format the built-in +player decodes; captures therefore store `.m4a` files. See +`architecture/youtube-rustypipe.md` for the full analysis. + +Search results and playlists are downloadable: `W` captures them with +audio into `/captures/` (slow under the throttle, but captures +are resumable — re-capture the same name to continue). + +## Configuration — `~/.config/crabidy/ytdy.toml` + +All options with their defaults: + +```toml +# Netscape cookies.txt export for the logged-in features (saved +# playlists). Export with a browser extension like "Get cookies.txt +# LOCALLY", ideally from an Incognito session you close afterwards. +# The provider caches the rotated session cookie under +# ~/.config/crabidy/rustypipe/, so the export only needs to be valid +# once. Default: unset (logged out; search still works). +# cookies = "/home/me/youtube-cookies.txt" + +# Results per search term. Default: 20. +# search_results = 20 + +# Per-request timeout in seconds (Innertube calls; the yt-dlp sidecar +# gets at least 60 s). Default: 30. +# call_timeout_secs = 30 + +# The yt-dlp binary used ONLY for stream URLs. A bare name resolves +# via PATH. Default: "yt-dlp". Keep it fresh — a stale yt-dlp is the +# first suspect when YouTube playback breaks. +# binary = "yt-dlp" + +# Optional rustypipe-botguard binary for PO-token attestation (also +# auto-detected on PATH). Currently ineffective upstream, but once +# rustypipe's web-client deciphering is fixed this makes streams work +# without yt-dlp. Default: unset. +# botguard_bin = "/home/me/.cargo/bin/rustypipe-botguard" +``` + +Secrets: the cookies file and the rustypipe cache grant access to your +YouTube session — keep both private. The provider never logs their +contents (paths only), and stream URLs are redacted from logs. + +## Failure behavior + +- No network / broken client at startup: only `/youtube` is disabled, + the server runs on. +- Rejected or missing cookies: degrades to logged-out with a warning. +- Missing `yt-dlp`: warning at startup; streams stop after ~1 MiB. +- `403 Forbidden` on streams in the log: YouTube's per-IP/per-video + enforcement fluctuates; usually transient. If it persists, update + `yt-dlp`. + +[rustypipe]: https://crates.io/crates/rustypipe