diff --git a/Cargo.lock b/Cargo.lock index cbb1afe..9a5a755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -760,6 +760,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "ytdy", ] [[package]] @@ -5133,6 +5134,21 @@ dependencies = [ "synstructure", ] +[[package]] +name = "ytdy" +version = "0.1.0" +dependencies = [ + "async-trait", + "crabidy-core", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.19", + "tokio", + "toml", + "tracing", +] + [[package]] name = "zbus" version = "5.18.0" diff --git a/Cargo.toml b/Cargo.toml index 71f5173..3bbe36b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crabidy-server", "fsdy", "tidaldy", + "ytdy", ] [workspace.package] @@ -68,3 +69,4 @@ audio-player = { path = "audio-player" } crabidy-core = { path = "crabidy-core" } fsdy = { path = "fsdy" } tidaldy = { path = "tidaldy" } +ytdy = { path = "ytdy" } diff --git a/architecture/youtube-provider.md b/architecture/youtube-provider.md new file mode 100644 index 0000000..54f0425 --- /dev/null +++ b/architecture/youtube-provider.md @@ -0,0 +1,186 @@ +# YouTube provider (ytdy) + +## Context and problem statement + +A new library provider for YouTube, mounted at `/youtube`: + +- **Search** works without any login — creatable search-term nodes like + `/tidal/search` (`%` creates a term, results list as tracks). +- **Login is optional.** When configured, the user's **playlists** appear + as an extra subtree; without it, the provider still works (search + only). +- **Captures** (`W`, download) must work on YouTube nodes — search + results and playlists are downloadable. + +## Assumptions (confirmed) + +- The captures machinery is provider-agnostic: anything that answers + `get_lib_node`/`get_urls_for_track` and raises `is_downloadable` gets + `w`/`W` for free — no new wire or TUI work at all. +- The TUI's creatable/editable/deletable node flows (`%`/`e`/`d`) are + generic; mirroring tidal's search-term semantics costs no TUI change. +- `pkgs.yt-dlp` (2026.06.09) exists in nixpkgs; the devenv already pins + the toolchain, so the engine binary is declared, not assumed. +- The audio player streams plain https URLs; googlevideo stream URLs + (from `bestaudio`) are plain https and also downloadable with the + captures reqwest client. + +## Decisions + +### D1 — Extraction engine: `yt-dlp` subprocess + +Options considered: + +- *(a)* **Pure-Rust extractor crates** (`rustypipe`, `rusty_ytdl`): no + external binary, but they chase YouTube's extraction changes with + small maintainer teams, and logged-in user playlists are weakly or not + supported. +- *(b)* **Invidious/Piped instances**: no extraction code at all, but a + hard runtime dependency on third-party servers of unpredictable + availability — worse than a local binary for a self-hosted player. +- *(c)* **`yt-dlp` as a subprocess** with `-J` JSON output: the de-facto + standard extractor, fastest to track YouTube changes, supports search + (`ytsearchN:`), playlists, cookies-based login, and direct stream + URLs. Cost: a non-Rust runtime dependency and subprocess plumbing. + +**Decision: (c).** The binary is declared in `devenv.nix` (dev) and is a +documented runtime requirement (deploys). All calls go through one +`Engine` seam (`tokio::process::Command`, `kill_on_drop`, per-call +timeout, bounded stdout, `serde_json` parsing) so tests fake the binary +with a script and a future pure-Rust engine stays swappable. + +### D2 — Crate `ytdy`, mounted at `/youtube`, non-fatal init + +New workspace crate `ytdy` implementing `ProviderClient`, following +`tidaldy`'s shape. `init` probes ` --version` (with timeout); +a missing or broken binary disables the provider with a warning — the +server and every other provider keep running. Settings (`ytdy.toml`): + +```toml +binary = "yt-dlp" # optional; PATH lookup by default +cookies = "/path/cookies.txt" # optional; presence = "logged in" +search_results = 20 # ytsearchN cap +``` + +The orchestrator gains `youtube_client: Option>` and +`/youtube` routing arms (same completeness as the other providers); +`get_lib_root` lists `youtube` only when the probe succeeded. + +### D3 — Tree shape + +- `/youtube` — children: `search` (always, `is_creatable`), `playlists` + (only when cookies are configured). +- `/youtube/search/` — created via `%` like tidal search; terms + live in memory (`RwLock>`, dedup, recreated implicitly on + stale paths), renamable and deletable. The node lists the top + `search_results` results as **tracks** (`ytsearchN:`, + `--flat-playlist`); it is queueable (results are homogeneous tracks, + unlike tidal's mixed search). +- `/youtube/playlists` — the user's playlists as children + (`https://www.youtube.com/feed/playlists` with cookies, flat). +- `/youtube/playlists/` — playlist entries as tracks. +- Track paths: `/`; metadata from the flat entries + (title, uploader as artist, duration). `get_metadata_for_track` runs + a single-video `-J` when called directly. + +### D4 — Streams, downloads, login + +- `get_urls_for_track`: `yt-dlp -f bestaudio/best -g --no-playlist +