Skip to content

Repository files navigation

ytb

CI Release Go Reference Go Report Card License

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

ytb searching YouTube and reading a video record from the command line

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.

Install

go install github.com/tamnd/ytb-cli/cmd/ytb@latest

Or 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 10

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

Commands

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.

Usage

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 search

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

Global flags

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

Signing in, if you want 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 them

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

The local store

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 Markdown

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

Serve and MCP

Every 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 stdio

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

Exit codes

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.

Development

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.

Releasing

Push a version tag and GitHub Actions runs GoReleaser:

git tag -a v0.5.0 -m "v0.5.0"
git push --tags

The image tag carries no v prefix (ghcr.io/tamnd/ytb:0.5.0).

License

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.

About

YouTube from the terminal with no API key: videos, channels, search, comments, transcripts and ranged downloads, every record saying which public surface answered.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages