Skip to content
ac1982Public

About

Video and audio downloader for YouTube, X, bilibili and podcasts, built for people and AI agents: --json output, no prompts, clear exit codes. Swift, macOS.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

haul — One link. Your media. A single-binary video and audio CLI for people and AI agents

English · 简体中文 · 繁體中文 · 日本語 · हिन्दी · Español · العربية · Français
বাংলা · Português · Bahasa Indonesia · اردو · Русский · Deutsch · Naijá · مصري

Build status Latest release Go 1.26 or newer macOS, Linux and Windows MIT license

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


Small command. Complete download.

↓ 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

Inspect pages and streams → choose quality and codecs → download media → mux tracks and metadata into a file

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.

Install

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.Deno

On 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.

Get the binary

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@latest

or, from a clone:

go build -o haul ./cmd/haul

Usage

Start 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 QuickTime
Command 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

Sites

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 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 –

Make it yours

Just the audio

haul --audio-only "https://youtu.be/DdCEmlAydcw"
haul --audio-only -c m4a "https://youtu.be/DdCEmlAydcw"   # AAC rather than Opus

A 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 keys

For AI agents and scripts

haul --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. Its files lists the output files, new or already there.
  • Nothing is interactive without a terminal. -i without one fails with exit code 2 and names the flags to use instead.
  • Stream indexes in info follow the order haul chooses in; pass the same -q / -c to info and to the download.
  • info on 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

Options

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.

Site notes

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 app

Reading 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

~/.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"] }
}

Development

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 Podcasts

The 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.

Acknowledgements

yt-dlp, FFmpeg, GPAC, aria2, pflag, x/term, rsc.io/qr, and the API notes of bilibili-API-collect and bilibili-grpc-api.

License

MIT


One link. Your media.
Get haul · Agent reference · Contribute

About

Video and audio downloader for YouTube, X, bilibili and podcasts, built for people and AI agents: --json output, no prompts, clear exit codes. Swift, macOS.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages