A command line for YouTube.
ytb resolves any video, channel, playlist, comment thread, transcript, or YouTube Music record into clean structured data.
One pure-Go binary, no API key, no quota.
Install • Commands • Usage • The local store
It talks to the same public InnerTube endpoints the YouTube site uses, so there is no key to register and no quota to budget.
Responses are cached on disk, so a repeat call is instant.
ytb crawl walks the graph into a local SQLite store you can query with SQL.
ytb is an independent tool.
It is not affiliated with YouTube or Google.
go install github.com/tamnd/ytb-cli/cmd/ytb@latestOr grab a prebuilt binary, a Linux package (deb/rpm/apk), or a container image from the releases:
brew install tamnd/tap/ytb
docker run --rm ghcr.io/tamnd/ytb:latest search 'lofi hip hop' -n 10Shell completion is built in: ytb completion bash|zsh|fish|powershell.
Downloads and transcripts both run in-process, with no yt-dlp, no Deno and no JavaScript interpreter.
yt-dlp is optional and is only used by ytb extract, by ytb download --use-yt-dlp, and as a transcript fallback if YouTube gates the caption endpoints.
ffmpeg is optional too, and only for merging a separate video track with its audio, converting audio, and embedding cover art.
| Command | Reads |
|---|---|
ytb video <id|url>... |
one or more videos; full metadata |
ytb channel <handle|url> |
a channel's record; --counts, --no-about |
ytb about <handle|url> |
a channel's about panel: links, country, join date |
ytb uploads <handle|url> |
a channel's uploads; --kind, --via, --exact |
ytb feed <handle|url> |
the newest fifteen, with exact timestamps |
ytb playlists <handle|url> |
a channel's playlists; --kind playlists|releases|podcasts|courses |
ytb playlist <id|url> |
a playlist's header |
ytb items <id|url> |
a playlist's videos |
ytb search <query> |
search with type, duration, features, and sort filters |
ytb trending |
what is hot right now; --category |
ytb comments <id|url> |
a video's comments and replies; --sort |
ytb community <handle|url> |
a channel's community / posts tab |
ytb hashtag <tag> |
a hashtag feed |
ytb related <id|url> |
related videos for a video |
ytb suggest <term> |
search autocomplete terms |
ytb transcript <id|url> |
one caption track as text, srt, vtt or json; --lang, --auto, --translate |
ytb captions <id|url> |
the caption tracks a video has, and which of them fetch |
ytb formats <id|url> |
streaming format metadata; --audio, --video, --muxed |
ytb chapters <id|url> |
a video's chapters, and where each one came from |
ytb thumbnail <id|url> |
thumbnail renditions, confirmed with a HEAD; --fetch |
ytb sponsorblock <id|url> |
community SponsorBlock segments; --categories |
ytb music search <query> |
YouTube Music search |
ytb music artist <id|url> |
a Music artist's profile and releases |
ytb music album <id|url> |
a Music album |
ytb music playlist <id|url> |
a Music playlist |
ytb music track <id|url> |
a Music track; --lyrics |
ytb download <id|url>... |
download media with the native engine; -f, -x, --quality, --out |
ytb extract <audio|video|transcript|all> <id|url> |
extract one stream through yt-dlp |
ytb crawl <seed>... |
walk the graph from seeds into the store; --depth, --budget, --resume |
ytb archive <id|url> |
write one read down in full: page, payloads, headers, and what ytb parsed |
ytb edges <id|url>... |
the claims one read makes: subject, predicate, object, and who said so |
ytb rdf <id|url>... |
the same claims as n-triples, turtle, or json-ld |
ytb graph <seed>... |
follow the frontier those claims name; --depth, --budget |
ytb discover <seed>... |
breadth-first walk from a video, channel, or playlist |
ytb predicates |
the closed vocabulary: every predicate, its domain and range |
ytb surfaces |
the eleven surfaces a record can name, with the host each reads |
ytb clients |
every InnerTube client ytb claims to be, with its version |
ytb routes |
every read, with its HTTP route and its MCP tool name |
ytb id <ref> |
classify any id, handle, or URL, with no request at all |
ytb query <sql> |
run SQL over the store, read-only |
ytb export <handle|id> |
render the store as interlinked Markdown |
ytb db stats|search|path|vacuum|reset |
work with the local SQLite store |
ytb cache path|info|clear |
where the response cache is, what is in it, and how to empty it |
ytb auth import|status|clear |
store your browser's YouTube cookies, or forget them |
ytb config show|path|init|edit |
show, write or open the config file |
ytb serve |
one HTTP route per read, NDJSON, plus a generated OpenAPI spec |
ytb mcp |
the same reads as MCP tools over stdio |
ytb version |
print version, commit, and build date |
Full reference and guides live at ytb-cli.tamnd.com.
ytb video dQw4w9WgXcQ # full video metadata
ytb channel @MrBeast # a channel's record
ytb uploads @MrBeast -n 20 # a channel's uploads
ytb search 'lofi hip hop' -n 50 # search
ytb comments dQw4w9WgXcQ --sort new -n 100 # newest 100 comments
ytb transcript dQw4w9WgXcQ # transcript as text
ytb trending --category music # what is hot right now
ytb music search 'rick astley' # YouTube Music searchRecords come out as a table (the default on a terminal), list, markdown, JSON, JSONL, CSV, TSV, url, or raw. The table uses rounded borders and a colored header on a true-color terminal, and JSON and JSONL are syntax-highlighted too:
ytb search 'lofi hip hop' --fields id,title,channel,views -o table
ytb video dQw4w9WgXcQ -o json
ytb search 'go' -n 50 -o jsonl | jq 'select(.view_count > 100000)'
ytb search 'go' -o url
ytb uploads @MrBeast -o jsonl > mrbeast.jsonl
ytb items PLFgquLnL59alCl_2TQvOiD5Vgm1hCaGSI -o url | ytb video ---fields takes the table's column names and JSON keeps the record's own, which is why views selects a column and .view_count selects a field.
Chain commands through stdin with - for batch lookups:
ytb search 'go programming' -o url | ytb video --o, --output auto|table|list|markdown|json|jsonl|csv|tsv|url|raw (auto: table on a TTY, jsonl when piped)
--fields comma-separated columns to keep, in order
--no-header omit the header row
--template Go text/template applied per record
-n, --limit max records (0 = unlimited)
--max-pages max continuation pages (0 = unlimited)
-j, --workers concurrency for detail fetches (default 4)
--rate min delay between requests (default 1.5s)
--timeout per-request timeout (default 30s)
--retries retry attempts on 429/5xx (default 3)
--hl InnerTube interface language (default en)
--gl InnerTube content country (default US)
-v, --verbose print each request as it goes out; twice for the full URL and headers
-q, --quiet suppress progress output
--color auto|always|never
--db tee every record into a store (e.g. out.db, postgres://...)
--data-dir override the data directory, which is where the store and the cache live
--cache-ttl how long a cached response is served before it is refetched (default 15m)
--no-cache bypass the on-disk cache
--profile named profile to load from the config file
--dry-run print what would be done, do nothing
-y, --yes assume yes to prompts
--rate, --timeout and --retries show 0s, 0s and -1 in --help, which is how the flag layer says "not set".
The defaults above are what an unset flag resolves to.
Everything above works signed out, and that is the point of the tool. A session is optional and it buys five things: comments on a network with Restricted Mode turned on, the community tab of a channel that gates it, age-restricted videos, members-only videos and posts, and your own subscriptions, playlists and history.
ytb auth import --cookies ~/Downloads/cookies.txt # a Netscape cookies.txt from a browser extension
pbpaste | ytb auth import --cookies - # or a Cookie header pasted from the network panel
ytb auth status # which cookies are stored and what they unlock
ytb auth clear # forget themytb never asks for a password, never drives a login form and never touches a consent screen.
It copies the cookies your browser already has, keeps only the session ones, and writes them 0600 in the data directory.
They go into a request header to youtube.com and nowhere else: not to the CDN, not into the cache, not into the store, and ytb archive writes the header down as removed rather than as itself.
ytb auth status names the cookies and never prints one, so it is safe to run in front of other people.
Records fetched with a session say so.
Every record carries "tier": 1 and lists s11 in its surfaces, so a dataset built signed in is never mistaken for one anybody can reproduce.
The cache keys split the same way, which is what stops an age-restricted page fetched signed in from being served to a signed-out read.
Nothing in this binary writes to YouTube, so a session cannot post, like, subscribe or delete.
ytb crawl walks the graph from a seed and writes what it saw into a SQLite file at <data-dir>/ytb.db, which ytb db path will print.
There are three tables.
nodes is everything with an identity, one row per URI, with the record as JSON and a null record for a node somebody named that nobody has fetched yet.
claims is the edges, one row per observation, so the same edge seen on the watch page and in a browse response is two rows and each says where it came from.
reads is the log: every request, what answered it, and how big it was.
ytb crawl @MrBeast --depth 2 --budget 200 # walk the graph into the store
ytb crawl --resume --budget 50 # keep going where it stopped
ytb db stats # nodes by kind, claims by predicate
ytb db search "lofi" # full-text search over stored videos
ytb export @MrBeast --out site/ # render the store as MarkdownThe unread nodes are the frontier, which is a query rather than a queue, so a crawl that stops is just a crawl with rows left to read:
ytb query "select uri from nodes where record is null and kind='video' limit 20"
ytb query "select predicate, count(*) c from claims group by 1 order by c desc"ytb query opens the file read-only, so a statement that would write is refused by SQLite itself.
To keep the raw bytes as well, ytb archive <id> writes one read into a directory: the page, every InnerTube payload, the request headers with the session ones removed, and the records and claims ytb parsed out of them.
The response cache is a different thing and lives beside the store.
It holds the bytes a request answered with, so a repeat call costs nothing, and an entry older than --cache-ttl is not served and is not deleted either:
ytb cache info # entries, size, and how many are still fresh
ytb cache clear --dry-run # what emptying it would freeEvery read is one registration, and that registration is three surfaces: the command, an HTTP route, and an MCP tool. Neither server has a read the command line lacks, and neither can be missing one, because there is no second implementation to keep in step.
ytb serve --addr 127.0.0.1:8080 # 31 routes, NDJSON
curl -s localhost:8080/v1/video?ref=dQw4w9WgXcQ
curl -s "localhost:8080/v1/uploads?ref=@RickAstleyYT&kind=shorts&limit=5"
curl -s localhost:8080/v1/openapi.json # the generated spec
ytb mcp # the same set on stdioArguments go in the query as well as on the path, which is the form to use, because a path splits on slashes and half of what you pass ytb is a URL.
Flags keep the names the command gives them: --max-pages 3 is &max-pages=3.
Nothing that writes is served, so download, crawl, export, db, cache, config and auth stay on the command line where they belong.
Reference.
0 success
1 something else went wrong, including a 5xx that outlasted the retries
2 usage error, or nothing named to work on
3 the query ran and matched nothing
4 signing in would be needed to see this
5 still rate limited after the retries
6 no such video, channel, playlist or track
7 a missing external tool or capability: ffmpeg, yt-dlp, a SABR-only stream
8 the request never got an answer
The pair worth branching on is 3 against 6. A search that matched nothing is a normal outcome and an id that does not resolve is not. An unknown flag or an unknown command exits 1, because the argument parser decides that before ytb sees the run.
cmd/ytb/ thin main entry point
cli/ commands and output rendering
ytb/ HTTP client, InnerTube transport, parsers, models, crawl, store
pkg/ytid/ id and URL classification
pkg/graph/ URIs, predicates, and the claim vocabulary
pkg/rdf/ n-triples, turtle, and json-ld writers
pkg/srv3/ caption track parsing
docs/ documentation site (Hugo, tago-doks theme)
make build # ./bin/ytb
make test # go test ./...
make vet # go vet ./...
make fmt # gofmt -s -w .Requires Go 1.26+.
yt-dlp is optional; install it from its releases if you want ytb extract, ytb download --use-yt-dlp, or transcript recovery on a video whose captions are gated.
Push a version tag and GitHub Actions runs GoReleaser:
git tag -a v0.5.0 -m "v0.5.0"
git push --tagsThe image tag carries no v prefix (ghcr.io/tamnd/ytb:0.5.0).
Apache-2.0. See LICENSE.
ytb is an independent client.
Use it to access public data responsibly and within YouTube's Terms of Service.
YouTube is a trademark of Google LLC.
