7.9 KiB
YouTube provider (ytdy)
Extraction engine superseded by
youtube-rustypipe.md: theyt-dlpsubprocess (D1, D5, thebinarysetting, and thebestaudioformat choice) was replaced with the pure-Rustrustypipeclient after playback turned out broken (bestaudio = Opus, which the rodio/symphonia player cannot decode) and the Python subprocess proved unwanted. The tree shape, search-term store, path scheme, and optional-login gating described here still hold.
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_trackand raisesis_downloadablegetsw/Wfor 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-dlpas a subprocess with-JJSON 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 <binary> --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):
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<Arc<ytdy::Client>> 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/<term>— created via%like tidal search; terms live in memory (RwLock<Vec<String>>, dedup, recreated implicitly on stale paths), renamable and deletable. The node lists the topsearch_resultsresults as tracks (ytsearchN:<term>,--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/playlistswith cookies, flat)./youtube/playlists/<pid>— playlist entries as tracks.- Track paths:
<node>/<videoid>; metadata from the flat entries (title, uploader as artist, duration).get_metadata_for_trackruns a single-video-Jwhen called directly.
D4 — Streams, downloads, login
get_urls_for_track:yt-dlp -f bestaudio/best -g --no-playlist <video>→ the stream URL(s). Cookies are passed to every call when configured (search and streams benefit too, e.g. age-gated videos).- Downloadability mirrors tidal's central rule: a node is downloadable
when it is queueable or lists tracks; children mirror
is_queable.extension_forgainsaudio/webm → webm(bestaudio is usually opus in webm; googlevideo URLs carry no path extension). - "Logged in" is exactly "a cookies file is configured and readable at init" — no OAuth flow of our own, no credential storage beyond the user-provided file. The cookies path is config; its contents are a secret and must never appear in logs or error reports.
D5 — Subprocess discipline
- One shared
Enginewith the binary path; every invocation:--no-warnings -J(or-g), argument-list only (never shell), per-call timeout (default 60 s, configurable for tests), stdout capped, stderr summarized into warnings (never echoed wholesale — it can contain URLs). - Errors are typed (
EngineError: spawn/timeout/exit-status/parse) and map toProviderError::FetchError/MalformedPathat the trait boundary; no panic on any subprocess condition. - Calls run sequentially per request (no engine-level queue); the orchestrator already spawns long walks.
D6 — Out of scope (explicitly)
- OAuth/device login flows, cookie refresh, or storing credentials.
- Uploads, likes, subscriptions feeds, comments; YouTube Music.
- Caching search or playlist results across calls (each listing is a fresh subprocess call, like tidal's fresh HTTP calls).
- SponsorBlock, chapters, DRM-protected content (skipped as unplayable).
Structure
direction: right
server: crabidy-server {
orch: ProviderOrchestrator
}
ytdy: "ytdy (crate)" {
client: "Client\n(ProviderClient)"
terms: "search terms\n(in-memory, like tidal)"
engine: "Engine\n(subprocess seam)"
client -> terms
client -> engine
}
bin: "yt-dlp binary\n(devenv / PATH)" {
shape: hexagon
}
yt: "YouTube" { shape: cloud }
cookies: "cookies.txt\n(optional, user-provided)" { shape: page }
server.orch -> ytdy.client: "/youtube/..."
ytdy.engine -> bin: "argv + -J, timeout,\nbounded stdout"
bin -> yt
cookies -> ytdy.engine: "--cookies (when set)"
Key flow: search, then W
shape: sequence_diagram
tui: TUI
orch: Orchestrator
y: ytdy
e: yt-dlp
tui -> orch: "% on /youtube/search: 'lofi'"
orch -> y: "create_lib_node"
y -> e: "ytsearch20:lofi -J --flat-playlist"
e -> y: "entries (id, title, uploader, duration)"
y -> tui: "term node: 20 tracks, downloadable"
tui -> orch: "W → CaptureLibraryNode(download)"
orch -> y: "per track: get_urls_for_track"
y -> e: "-f bestaudio -g <id>"
e -> y: "googlevideo url"
orch -> orch: "captures store: stream to\n/captures/<name>/ (webm + toml)"
Risks and open questions
feed/playlistsextraction: the exact yt-dlp invocation for "my playlists" remains live-unvalidated (no cookies were available during development; search, streams, and download captures were validated live). Fallback if it turns out wrong: adjust the feed URL inplaylists_node, or bookmark playlist URLs as search terms.- yt-dlp breakage/drift: extraction can break until the devenv pin is bumped; errors stay typed and non-fatal.
- Stream URL lifetime: googlevideo URLs expire (~6 h); captures download immediately after resolving, playback resolves on play — both within the window.
- Terms of service: downloads are personal-use copies of streams the client can already play, same stance as tidal captures.