ytdown 0.3.0

A Rust library mirroring yt-dlp's core: extract, select, and download media. Library only — no CLI.
Documentation

ytdown

CI crates.io docs.rs

A Rust library 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(())
}

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

License

Licensed under either of

at your option.