# ytdown
[](https://github.com/4thel00z/ytdown/actions/workflows/ci.yaml)
[](https://crates.io/crates/ytdown)
[](https://docs.rs/ytdown)
A Rust **library** (and companion **CLI**) mirroring [yt-dlp](https://github.com/yt-dlp/yt-dlp)'s
core: resolve a media URL into structured metadata and stream formats, select a
format, and download it to disk.
## Quickstart
```rust,no_run
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:
```toml
[dependencies]
futures = "0.3"
```
```rust,no_run
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(())
}
```
[`futures::StreamExt`]: https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html
## 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`.
| 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
| `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 [`Extractor`]s; `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:
```bash
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:
```bash
cargo test --all-features -- --ignored
```
[`Registry`]: https://docs.rs/ytdown/latest/ytdown/struct.Registry.html
[`Extractor`]: https://docs.rs/ytdown/latest/ytdown/trait.Extractor.html
## CLI
The `ytdown-cli` crate ships a `ytdown` binary over the same engine:
```sh
cargo install ytdown-cli
```
```sh
# 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
# Playlists/channels download entry-by-entry
ytdown get 'https://www.youtube.com/playlist?list=…' --limit 10 -o '{index} - {title}.{ext}'
```
### Subcommands
| `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`)
| *(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
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
- MIT license ([LICENSE-MIT](LICENSE-MIT))
at your option.