ytdown 0.4.0

A Rust library mirroring yt-dlp's core: extract, select, and download media. Ships with a companion CLI (ytdown-cli).
Documentation

ytdown

CI crates.io docs.rs

A Rust library (and companion CLI) mirroring yt-dlp's core: resolve a media URL into structured metadata and stream formats, select a format, and download it to disk.

Quickstart

use std::path::Path;
use ytdown::Ytdown;

#[tokio::main]
async fn main() -> ytdown::Result<()> {
    let yt = Ytdown::builder().build()?;
    let info = yt.resolve("https://youtu.be/dQw4w9WgXcQ").await?;
    if let ytdown::MediaInfo::Single(video) = info {
        let fmt = video.formats().best_progressive()?;
        yt.download(fmt, Path::new("out.mp4"))
            .progress(|p| {
                if let Some(pct) = p.percent() {
                    eprintln!("{pct:.1}%");
                }
            })
            .await?;
    }
    Ok(())
}

Collections (playlists / channels / search)

resolve returns MediaInfo::Collection for playlists, channels, and ytsearch: queries. Its entries field is a futures::Stream that paginates lazily, so consume it with futures::StreamExt (next, take, collect, …). Add futures to your Cargo.toml to bring the extension trait into scope:

[dependencies]
futures = "0.3"
use futures::StreamExt;
use ytdown::{MediaInfo, Ytdown};

#[tokio::main]
async fn main() -> ytdown::Result<()> {
    let yt = Ytdown::builder().build()?;
    if let MediaInfo::Collection(mut col) = yt.resolve("ytsearch:rust async").await? {
        // Take the first 5 entries without fetching the whole collection.
        while let Some(entry) = col.entries.next().await {
            let entry = entry?;
            println!("{}{}", entry.id, entry.title.as_deref().unwrap_or(""));
            // Resolve an entry's full metadata + formats on demand:
            // let info = yt.resolve(&entry.url).await?;
        }
    }
    Ok(())
}

Supported URLs

YouTube is the only extractor registered by default (embedders can add their own via the Extractor trait). Accepted hosts: youtube.com, www./m./ music.youtube.com, youtube-nocookie.com, and youtu.be.

Kind URL shapes
Video …/watch?v=ID, youtu.be/ID, …/shorts/ID, …/embed/ID, …/v/ID, …/e/ID
Playlist …/playlist?list=ID, any non-watch URL with ?list=ID
Channel …/channel/UC…, …/@handle (streams the channel's Videos tab)
Search ytsearch:QUERY pseudo-URL (mirrors yt-dlp)

A watch URL that also carries &list= resolves to the video (the v= parameter wins). Anything else fails fast with Error::UnsupportedUrl — no network request is made.

Features

Feature Default Description
ffmpeg off Mux separate DASH audio + video streams via the system ffmpeg binary.

Architecture

src/
├── lib.rs              # Public API: Ytdown client (builder), re-exports
├── error.rs            # Error enum: Network, Extraction, Cipher, UnsupportedUrl, ...
├── types.rs            # MediaInfo, Format, Thumbnail, Entry, enums (Container, ...)
├── extractor/
│   ├── mod.rs          # Extractor trait + Registry (URL → extractor dispatch)
│   └── youtube/
│       ├── mod.rs          # URL recognition + orchestration
│       ├── innertube.rs    # InnerTube client: player/browse/search endpoints
│       ├── player.rs       # JS player fetch + sig/nsig function extraction
│       └── pagination.rs   # Continuation-token Stream for playlists/channels/search
├── jsi.rs              # boa_engine wrapper: execute extracted cipher fns
├── download/
│   ├── mod.rs          # Downloader: chunked GET, Range resume, retry/backoff
│   └── progress.rs     # Progress events + callback plumbing
├── format.rs           # FormatSelector: best/worst/filters
└── postprocess.rs      # [feature "ffmpeg"] mux / convert via system ffmpeg

A Registry holds boxed Extractors; Ytdown::resolve dispatches to the first matching extractor or returns Error::UnsupportedUrl. The shared reqwest::Client, config, and caches travel through an ExtractorContext.

Testing

Unit and offline integration tests (wiremock-backed) run with:

cargo test --all-features

Live tests in tests/live.rs hit the real YouTube network and are marked #[ignore = "network"], so they are skipped by default and in CI. Run them explicitly:

cargo test --all-features -- --ignored

CLI

The ytdown-cli crate ships a ytdown binary over the same engine:

cargo install ytdown-cli
# Inspect available formats
ytdown formats https://youtu.be/dQw4w9WgXcQ

# Download: best merged video+audio (needs ffmpeg), or best progressive
ytdown get https://youtu.be/dQw4w9WgXcQ -o '{title}.{ext}'

# Explicit formats: keywords, itags, or video+audio merge pairs
ytdown get -f 137+140 https://youtu.be/dQw4w9WgXcQ

# Metadata as JSON, search from the terminal
ytdown info https://youtu.be/dQw4w9WgXcQ | jq .title
ytdown search "rust async" -n 5

# Playlists/channels download entry-by-entry
ytdown get 'https://www.youtube.com/playlist?list=…' --limit 10 -o '{index} - {title}.{ext}'

Subcommands

Command Description
get <URL> Download a video, playlist, channel, or ytsearch: result set
info <URL> Print resolved metadata as JSON (--pretty, --limit for collections)
formats <URL> List a video's available formats as a table (--json)
search <QUERY> List search results as a table (-n/--limit, default 10; --json)
completions <SHELL> Generate shell completions (bash, zsh, fish, …)

Global flags on every command: -v/-vv (info/debug logs), -q (silence logs), --user-agent <UA>. RUST_LOG overrides the verbosity flags when set.

Format selection (-f)

Value Meaning
(omitted) Best split video+audio merged via ffmpeg; best progressive without ffmpeg
best Best progressive (muxed A+V) format
bestvideo / bestaudio Best video-only / audio-only stream
22 A specific format by itag
137+140 Video itag + audio itag, merged via ffmpeg

--max-height <H> and --container <mp4|webm> narrow the keyword selections; combining them with explicit itags is rejected (exit 2). Merging needs ffmpeg on PATH or via --ffmpeg <path>.

Output templates (-o, default {title}.{ext})

Placeholders: {title} {id} {ext} {height} {itag} {uploader} {index} ({index} is the 1-based position within a playlist/channel/search download). Substituted values are sanitized to safe path components; literal / in the template creates directories.

Downloads

--concurrency <N> parallel range chunks (with --chunk-size <BYTES>), --retries <N>, and resume of partial files by default (--no-resume to disable). Collections download entry-by-entry, honouring --skip <N> and --limit <N>; per-entry failures are logged and reported at the end without aborting the run.

Interactive picker

Run ytdown get on a TTY without -f and a format picker opens: arrows to navigate, / to filter, enter to select (video-only formats offer pairing with the best audio), q to quit. --no-tui (or piping) selects the best format automatically. The picker and all progress/log output draw on stderr, so stdout stays clean for piping.

Exit codes

0 success (including quitting the picker), 1 runtime errors (network, extraction, download), 2 usage errors (bad flags, invalid -f/-o values).

License

Licensed under either of

at your option.