English ·
简体中文 ·
繁體中文 ·
日本語 ·
हिन्दी ·
Español ·
العربية ·
Français
বাংলা ·
Português ·
Bahasa Indonesia ·
اردو ·
Русский ·
Deutsch ·
Naijá ·
مصري
A command-line downloader for video and audio, built for people and AI agents alike.
Give it a link. Choose your streams. Get video, audio, subtitles, chapters, and cover in one file.
Install · Quick start · Supported sites · Agent guide · Options · Releases
| ↓ Your media, your way | ⌘ One binary, every desktop | { } Ready for automation |
|---|---|---|
| Pick quality, codecs, pages, and file names with the same vocabulary across five sites | A single Go binary for macOS, Linux and Windows, with parallel ranged downloads that resume | One JSON document on stdout, logs on stderr, meaningful exit codes, and no prompts without a terminal |
Take a look inside the terminal
$ haul info -q 720p "https://youtu.be/DdCEmlAydcw"
haul v1.0.0
Inside Anthropic's molecular biology lab
channel Anthropic · 2026-09-24 · 00:01:15
Video
▶ 0 720p 1190×720 AVC 24fps 755 kbps 6.8 MB
1 720p 1190×720 VP9 24fps 528 kbps 4.7 MB
2 720p 1190×720 AV1 24fps 388 kbps 3.5 MB
3 1080p 2048×1240 VP9 24fps 5827 kbps 52.3 MB
…
19 144p 238×144 AVC 24fps 50 kbps 459.0 KB
Audio
▶ 0 OPUS 131 kbps 1.2 MB
1 M4A 130 kbps 1.2 MB
…
For personal, research and other non-commercial use. You are responsible for respecting copyright and each site's terms.
haul runs on macOS, Linux and Windows (amd64 and arm64). It needs ffmpeg for muxing; YouTube, X and Weibo also need yt-dlp, and YouTube's player challenges need deno:
brew install ffmpeg yt-dlp deno # macOS
sudo apt install ffmpeg pipx unzip curl # Linux (Debian / Ubuntu)
pipx install "yt-dlp[default]" && pipx ensurepath
curl -fsSL https://deno.land/install.sh | sh -s -- -y
winget install Gyan.FFmpeg # Windows, one package at a time
winget install yt-dlp.yt-dlp
winget install DenoLand.DenoOn Linux, install yt-dlp with pipx or as the yt-dlp_linux binary from its releases (yt-dlp_linux_aarch64 on arm64), not from your distribution: YouTube changes often, and packaged versions fall behind. Open a new terminal after installing, so the tools are on your PATH.
Download the archive for your system from Releases — haul-<version>-<os>-<arch>.tar.gz (.zip on Windows) — and put haul anywhere on your PATH:
tar -xzf haul-*-darwin-arm64.tar.gz
mkdir -p ~/.local/bin && mv haul-*/haul ~/.local/bin/macOS releases built by the current workflow are signed and notarized with Apple. Choose haul-<version>-darwin-<arch>.pkg for an installer with an attached notarization ticket; it installs haul into /usr/local/bin. The archive contains the same signed executable, but its first verification may need an internet connection. Older releases may be unsigned.
Build from source · Go 1.26+
go install github.com/ac1982/haul/cmd/haul@latestor, from a clone:
go build -o haul ./cmd/haulStart with a link. haul chooses the best available streams by default.
haul "https://youtu.be/DdCEmlAydcw"Inspect before downloading, or choose a quality and codec:
haul info "https://youtu.be/DdCEmlAydcw"
haul -q 720p -c avc,m4a "https://youtu.be/DdCEmlAydcw" # H.264 + AAC for QuickTimeCommand reference
haul <url> [options] download (same as `haul download <url>`)
haul info <url> [--urls] show the item, its pages and streams; download nothing
haul login bilibili log in to bilibili for higher qualities
haul templates file-name template variables
haul --help sites, agent usage, exit codes, examples
haul download --help every option
| Site | Links | Needs |
|---|---|---|
| YouTube | youtube.com/watch?v=…, youtu.be/…, /shorts/…, /embed/…, /live/… |
yt-dlp, deno |
| X | x.com/<user>/status/<id>, twitter.com/…; /video/<n> picks one video of a post |
yt-dlp |
weibo.com/<uid>/<id>, weibo.com/tv/show/…, video.weibo.com/show?fid=…, m.weibo.cn/status/…, t.cn/… |
yt-dlp | |
| bilibili | videos, bangumi, courses, collections, series, favorites, user spaces, b23.tv, bare BV… av… ep… ss… md… |
– |
| Xiaoyuzhou | xiaoyuzhoufm.com/episode/<id>, /podcast/<id> |
– |
| Apple Podcasts | podcasts.apple.com/<cc>/podcast/<name>/id<show>, with ?i=<episode> for one episode |
– |
Just the audio
haul --audio-only "https://youtu.be/DdCEmlAydcw"
haul --audio-only -c m4a "https://youtu.be/DdCEmlAydcw" # AAC rather than OpusA few parts, a whole season, or the latest episode
# Selected parts, saved to your Movies folder
haul -p 1-3,10 -w ~/Movies "https://www.bilibili.com/video/BV1Wv411h7kN"
# Every episode of a season
haul -p ALL "https://www.bilibili.com/bangumi/play/ss33073"
# The newest podcast episode
haul -p LAST "https://podcasts.apple.com/us/podcast/the-daily/id1200361736"
# Every video in an X post
haul -p ALL "https://x.com/<user>/status/<id>"Organized files, subtitles, or an interactive choice
haul -o "<uploader>/<title> [<quality>]" "https://youtu.be/DdCEmlAydcw"
haul --sub-lang en,zh "https://youtu.be/DdCEmlAydcw" # only these subtitle languages
haul -i "BV1qt4y1X7TW" # choose streams with the arrow keyshaul --help carries everything an agent needs; llms.txt is the same guide as a file. The short version:
haul info --json "<url>" # 1. inspect
haul --json --video-stream 2 --audio-stream 0 "<url>" # 2. download exactly those streams
haul --json -q 720p -c avc,m4a "<url>" # or let priorities choose- With
--json, stdout carries only one JSON document; progress and logs go to stderr. Itsfileslists the output files, new or already there. - Nothing is interactive without a terminal.
-iwithout one fails with exit code 2 and names the flags to use instead. - Stream indexes in
infofollow the order haul chooses in; pass the same-q/-ctoinfoand to the download. infoon a playlist, season, show or multi-video post lists its pages;-p <n>adds that page's streams and subtitles.- Pages already on disk are reported as
"status": "skipped", "reason": "exists", so re-running is safe.
Example JSON response · a successful download
A download prints:
{
"ok": true,
"command": "download",
"site": "youtube",
"input": "https://youtu.be/DdCEmlAydcw",
"title": "Inside Anthropic's molecular biology lab",
"uploader": "Anthropic",
"published": "2026-09-23T18:01:32Z",
"description": "…",
"pageCount": 1,
"files": ["/Users/me/Movies/Inside Anthropic's molecular biology lab.mp4"],
"pages": [
{
"index": 1,
"id": "DdCEmlAydcw",
"title": "Inside Anthropic's molecular biology lab",
"durationSeconds": 75,
"published": "2026-09-23T18:01:32Z",
"selected": true,
"status": "downloaded",
"file": "/Users/me/Movies/Inside Anthropic's molecular biology lab.mp4",
"sizeBytes": 3434260,
"selectedVideo": 0,
"selectedAudio": 0,
"video": [{ "index": 0, "quality": "360p", "resolution": "594x360", "codec": "AVC", "fps": 24, "bitrateKbps": 214, "sizeBytes": 2014782 }],
"audio": [{ "index": 0, "codec": "M4A", "bitrateKbps": 130, "sizeBytes": 1220994 }],
"subtitles": [{ "lang": "en" }]
}
]
}That is haul --json -q 360p -c avc,m4a …. Here the keys are in reading order and the stream lists are cut to the chosen ones; the real document sorts its keys alphabetically and lists every stream.
A failure prints "ok": false with "error": {"kind", "message", "exitCode"}, and keeps the pages done before it.
| Exit code | Meaning | error.kind |
|---|---|---|
| 0 | done | – |
| 1 | the download or extraction failed | failed |
| 2 | unsupported link or bad option value | input |
| 3 | a required tool is missing (ffmpeg, yt-dlp) | dependency |
| 4 | login needed or expired | auth |
| 64 | bad command line (unknown flag, missing argument) | – |
| 130 | cancelled | cancelled |
haul download --help lists them all. The main ones:
| Group | Options |
|---|---|
| General | --json, --config <file>, --debug |
| Streams | -q, --quality <list>, -c, --codec <list>, --video-stream <n>, --audio-stream <n>, -i, --interactive, --video-ascending, --audio-ascending |
| Pages | -p, --pages <spec>, --show-all, --hide-streams |
| Content | --audio-only, --video-only, --subtitle-only, --cover-only, --skip-subtitle, --sub-lang <list>, --auto-subtitles, --skip-cover, --skip-mux |
| Output | -o, --output <template>, --multi-output <template>, -w, --work-dir <dir>, --lang <code>, --no-tags, --archive, --delay <seconds> |
| bilibili | --api web|tv|app|intl, --danmaku, --danmaku-only, --danmaku-format xml,ass, --cookie, --token; more with --help-hidden |
| Tools | --ffmpeg, --yt-dlp, --use-mp4box, --mp4box, --use-aria2c, --aria2c, --aria2c-args, --single-connection |
Quality and codec priorities
Qualities and codecs. -q takes the labels the stream table shows: 1080p, 720p60 on YouTube; 8K, Dolby Vision, HDR, 4K, 1080P60, 1080P+, 1080P, 720P on bilibili. -c takes av1 vp9 hevc avc for video and m4a opus flac eac3 mp3 for audio. They are priorities, not filters: whatever is not listed comes after, best first. --video-ascending / --audio-ascending turn the order around for the smallest download.
Page selection
Pages. 8, 1,2, 3-5, 1-3,10, ALL, LAST (the last page, which is the newest episode of a show; LATEST works too). A link to one episode, or ?p=N, selects that page by itself. Pages are the parts of a bilibili video, the episodes of a season, show or list, and the videos of an X post.
File names and template variables
File names. An item with one page: <title>; with several (even when -p takes one): <title>/[P<pageNumberWithZero>]<pageTitle>, zero-padded to the page count. Variables: <title> <pageNumber> <pageNumberWithZero> <pageTitle> <id> <site> <uploader> <uploaderId> <quality> <resolution> <fps> <videoCodec> <videoBitrate> <audioCodec> <audioBitrate> <publishDate> <pageDate>, plus bilibili's <bvid> <aid> <cid> <api>. Dates take a format: <publishDate:yyyy-MM-dd>. The extension is added.
YouTube · extraction, subtitles, and playlists
YouTube protects its stream URLs with player challenges that need a JavaScript runtime, so extraction is left to yt-dlp -J (which runs them in deno); everything after that is haul's own. Uploaded subtitles are muxed in; auto-generated ones only with --auto-subtitles. googlevideo serves only bounded byte ranges, so tracks are fetched one 10 MB range at a time. watch?v=…&list=… downloads just the video. Keep yt-dlp current: that is where fixes for YouTube changes land.
X · public posts and audio
X needs no login for public posts. Its videos are single MP4 files with the audio inside, so the table lists video only; --audio-only extracts the audio. The qualities carry different audio, so for --audio-only haul first measures each one's audio with ffmpeg and downloads the smallest file with the best audio; the table then shows it as audio 128 kbps.
Weibo · public videos and t.cn links
Weibo needs no login for public videos. Like X, its videos are single MP4 files with the audio inside, and --audio-only picks the smallest one with the best audio (usually 720p: 480p has 48 kbps audio, 720p and 1080p 128 kbps). A t.cn short link is expanded by haul before yt-dlp reads the video; one that leads somewhere other than a Weibo video is an input error naming where it goes.
bilibili · login, higher qualities, and danmaku
bilibili is read through its own web, TV, APP (gRPC) and international APIs. Logged out, it offers only lower qualities (typically up to 480P); log in for 1080P, 4K, HDR, Dolby Vision and Hi-Res audio:
haul login bilibili # scan a QR code with the bilibili app
haul login bilibili --from-edge # macOS: reuse Microsoft Edge's login (or --from-chrome; --profile "Profile 1")
haul login bilibili --tv # TV access token, for --api tv / --api appReading a browser's login works only on macOS for now: it needs Full Disk Access for the terminal, and macOS asks once for the "Safe Storage" keychain item. --danmaku saves the bullet comments as XML and ASS; --danmaku-format ass keeps one of them.
Xiaoyuzhou & Apple Podcasts · episodes and metadata
Xiaoyuzhou and Apple Podcasts need no yt-dlp: Xiaoyuzhou pages carry the audio link, Apple links go through the public iTunes API and the show's RSS feed. Episodes keep their format (.mp3 or .m4a) with the cover embedded, the show as album and the host as artist. A show link lists its latest episodes oldest first, so -p LAST is the newest.
~/.config/haul/ (or $HAUL_HOME) holds:
| File | Purpose |
|---|---|
config.json |
defaults for any option |
cookie.txt, tv-token.txt, app-token.txt |
bilibili login |
archives.txt |
pages already downloaded (--archive) |
config.json uses the option names in camelCase, bilibili's under "bilibili", and needs only what it changes; a list can be an array or a comma-separated string. Command-line flags override it:
{
"workDir": "~/Movies/haul",
"codec": "avc,hevc,m4a",
"quality": "1080p,1080P",
"output": "<uploader>/<title>",
"archive": true,
"bilibili": { "danmaku": true, "danmakuFormat": ["ass"] }
}go build ./cmd/haul
go test ./... # offline, a few seconds
HAUL_LIVE=1 go test -run Live ./internal/... # also hits bilibili, YouTube, X, Weibo and Apple PodcastsThe offline suite never touches the network: tests talk to a stub http.RoundTripper that answers from recorded API responses and from simulated CDNs (range-only servers, dropped connections, servers that ignore ranges). End-to-end tests mux with a real ffmpeg and check the result with ffprobe; they are skipped when ffmpeg is not installed. See AGENTS.md for the architecture and conventions.
Pushing a v* tag tests, cross-compiles and publishes a release for every platform through GitHub Actions.
yt-dlp, FFmpeg, GPAC, aria2, pflag, x/term, rsc.io/qr, and the API notes of bilibili-API-collect and bilibili-grpc-api.
One link. Your media.
Get haul · Agent reference · Contribute