Skip to content
ytb

CLI

Every command and subcommand, with the flags that matter.

ytb <command> [subcommand] [flags]

Run ytb <command> --help for the full flag list on any command. This page is the map.

Global flags

Persistent flags accepted by every command.

Flag Meaning
-o, --output Output format: list, table, markdown, json, jsonl, csv, tsv, url, raw (auto)
--fields Comma-separated columns to show
-n, --limit Max rows emitted (0 = unlimited)
--max-pages Max continuation pages fetched (0 = unlimited)
-j, --workers Concurrency for detail fetches (4)
--rate Minimum delay between requests (1.5s)
--retries Retry attempts on 429/5xx (3)
--timeout Per-request timeout (30s)
--hl InnerTube interface language (en)
--gl InnerTube content country (US)
--db Tee every record into a generic store (e.g. out.db, postgres://...)
-q, --quiet Suppress progress output
-v, --verbose Print each request that went out; -vv adds the query and the headers
--color Color output: auto, always, never (auto)
--template Go text/template applied per row
--no-header Omit the header row in table/csv/tsv
--cache-ttl How long a cached response is served before it is refetched (15m)
--no-cache Bypass the on-disk caches
--data-dir Override the data directory (the store, the cache, the cookies)
-y, --yes Assume yes to prompts
--dry-run Print actions without performing them
--yt-dlp-bin Path to the yt-dlp binary (download --use-yt-dlp)
--ffmpeg-bin Path to ffmpeg (used to merge/convert when present)

-n means the same thing everywhere: stop after N records. A read that pages is told the number up front and stops asking for continuations; a read that answers in one request cuts its list on the way out. So ytb captions <id> -n 2 prints two tracks and ytb search <q> -n 2 makes one request rather than paging to the default limit.

There is no --config flag. The config file has one path, ytb config path prints it, and --data-dir moves the data rather than the config. --profile is accepted by the framework and nothing in ytb reads it yet.

-v writes to stderr, one line per request, so records on stdout still pipe. A cache hit never prints, because the trace sits in the HTTP transport and a cache hit never reaches it, which makes the line count an honest answer to what a command cost. -vv adds the full query and the headers that decide what a request means, which is the Range on a media fetch and the client name on a player POST. Cookie and Authorization are never printed at either level.

Commands

Command What it does
video Resolve one or more videos to full metadata
channel A channel's record: header, about panel and external links
about A channel's about panel on its own
uploads Stream a channel's uploads
feed A channel's Atom feed: the newest fifteen, with exact times
playlists List a channel's playlists, from any of its four playlist tabs
playlist Playlist header
items Stream a playlist's videos
search Search with the full filter grid
trending What's hot right now
comments Comments and replies
community Community / posts tab
hashtag A hashtag feed
related The related-videos graph
discover Breadth-first walk of the graph linked from a video, channel, or playlist
id Classify any id, handle or URL, and derive what it implies
edges The claims one read makes: subject, predicate, object, and who said so
predicates The closed vocabulary: every predicate with its domain and its range
surfaces The eleven surfaces a record can name, with the host each one reads
clients Every InnerTube client this tool claims to be, with its version
routes Every read, with its HTTP route and its MCP tool name
rdf The same claims as n-triples, turtle or json-ld, with provenance
graph Walk the frontier the claims name, on a budget counted in requests
suggest Search autocomplete suggestions
transcript Captions as text
captions A video's caption tracks, one record each
formats Streaming formats (metadata only)
music YouTube Music (artists, albums, songs)
download Download media with the native engine (or yt-dlp)
extract Extract a specific stream via yt-dlp
sponsorblock List community SponsorBlock segments
thumbnail List a video's thumbnails, or fetch the best one
chapters List a video's chapter markers
crawl Walk the graph from seeds into the store
archive Write one read down in full: page, payloads, headers, and what ytb parsed
query Run SQL over the store, read-only
export Render the store as interlinked Markdown
db The local SQLite store
cache The on-disk response cache: where it is, what is in it, and how to empty it
auth Manage the optional YouTube session
config View and manage configuration
version Print version information
serve Serve the operations over HTTP as NDJSON, covered in serve and MCP
mcp Run as an MCP server over stdio, covered in serve and MCP

video

ytb video <id|url>... [--flags] resolves a video to full metadata (HTML bootstrap plus /player plus /next). Pass - to read ids/urls from stdin.

Flag Meaning
--captions Also list the caption tracks that fetch, one extra request
--formats Also read the stream list, one extra request
--transcript Fetch and attach the transcript text
--lang Preferred caption language for --transcript
--thumbnails Confirm each constructed thumbnail rendition with a HEAD
--microdata Include what the page's own schema.org markup says
--no-player Never call the mobile player, whatever else was asked

Chapters, related videos and the description's links come back on a plain read and need no flag; ytb chapters and ytb related are the same data on its own.

formats is off by default and stays off, because it costs a mobile player request per video and a crawl of a thousand videos should not make a thousand extra calls nobody asked for. The watch page carries a stream list for free and a plain read still leaves it off: 29 formats nobody asked for bury the dozen fields the caller wanted.

channel

ytb channel <id|@handle|url> [--flags]. Prints the channel record.

Flag Meaning
--no-about Skip the about panel: one request fewer, and no join date, lifetime views or link titles
--counts Read the four derived playlists and check that videos + shorts + streams = uploads (four extra requests)

A channel page publishes its facts in four blocks that barely overlap, and the record is all four merged. channelMetadataRenderer has the id, the keywords, the RSS url and the 249 country codes it will serve in. microformatDataRenderer has the crawler's view plus a schema.org ProfilePage, and mainEntity.sameAs in that ProfilePage is the whole external link list with no continuation behind it, which is the cheapest place on the site to get a channel's links. pageHeaderViewModel has the handle, the badge and the banner. The about panel is a continuation and is the only source of the join date, the lifetime view count, the country in words and the link titles, which is what --no-about trades away.

Subscriber counts always carry subscriber_count_is_approximate: true. The site rounds every one of them to three significant figures, including the number in the ProfilePage's interactionStatistic, so 1490000 means somewhere in the 1.49 millions and never means exactly that.

The header and the about panel can disagree with each other about the video count, and both can disagree with the uploads playlist. The panel beats the header, --counts beats both, and via names the number that lost:

$ ytb channel @RickAstleyYT --counts --fields handle,videos,counts
╭───────────────┬────────┬─────────────────────────────────────╮
│ HANDLE        │ VIDEOS │ COUNTS                              │
├───────────────┼────────┼─────────────────────────────────────┤
│ @RickAstleyYT │ 435    │ 139 + 294 + 2 = 435 uploads, agrees │
╰───────────────┴────────┴─────────────────────────────────────╯

Those are the four playlists derived from the channel id: UU is every upload, UULF long form, UUSH shorts, UULV past live streams. The three partition the first, so a mismatch means the site is mid-write or a video is in a state none of the three tabs claims. The table shows the whole check as one counts cell; -o json carries the four ids, the four numbers and agrees separately.

about

ytb about <id|@handle|url>. The about panel on its own: description, join date, lifetime views, country, and the external links with their titles. No flags beyond the globals.

uploads

ytb uploads <id|@handle|url> [--flags]. Streams a channel's uploads.

Flag Meaning
--kind Which uploads: all, videos, shorts, streams, popular (default all)
--via Read them from the derived playlist or from the channel tab (default playlist)
--exact Cross-read the Atom feed so the newest fifteen get exact timestamps (one extra request)
--enrich Call /player per video for full metadata
--max-pages Max continuation pages (0 = unlimited)

The playlist route is the default because it is the only one that answers --kind all. No tab lists a channel's uploads: the Videos tab is long form only and returns exactly what UULF returns, so --kind all --via tab and --kind popular --via tab are usage errors rather than something that quietly returns less.

A shorts row comes back from the two routes with the same id and the same view count and a different via, because the two pages render the same view model differently. The Shorts tab writes a readable shorts-shelf-item-<id> and a "22K views" overlay; the UUSH playlist writes an opaque hash and no overlay at all, and the only view count on the row is the one spelled out in its accessibility label, "22 thousand views". Both are read, and via says which.

--exact fetches the Atom feed once and merges it into the newest fifteen rows. Those get a timestamp to the second and an exact view count; everything after row fifteen keeps the rounded text and says so in missed, because the feed serves fifteen entries and has no continuation.

feed

ytb feed <id|@handle|url>. The channel's Atom feed: fifteen entries, newest first, each with a publication time to the second and an exact view count. No flags beyond the globals.

This is the only surface outside the player that says whether a video is a short, which it does by linking to /shorts/<id> instead of /watch?v=. media:starRating count on an entry is a rating count and not a like count, so it is never written into like_count.

playlists

ytb playlists <id|@handle|url> [--flags]. Lists the playlists a channel has published.

Flag What it does
--kind Which tab to read: playlists, releases, podcasts or courses. Default playlists
--max-pages Max continuation pages, 0 for unlimited

Four tabs, one shape. releases is albums and singles, podcasts is shows, courses is course playlists, and each one is a page of playlist rows read by the same code. Most channels have only playlists, so asking for one a channel does not have exits 3 and names the tabs it does have. That check matters here more than elsewhere: YouTube answers a request for a missing tab with the channel's home page and a 200, so without it a courses read would return the channel's videos and look like it worked.

A releases row carries its release date, which no field on a playlist holds, so it is kept verbatim in metadata_parts.

playlist

ytb playlist <id|url>. Prints the playlist header.

items

ytb items <playlist-id|url> [--flags]. Streams a playlist's videos, each with its position.

ytb search <query> [--flags]. Search with the full filter grid.

Flag Meaning
--type Video, Channel, Playlist, Movie
--duration Short, Medium, Long
--upload-date Hour, Today, Week, Month, Year
--sort Relevance, Date, Views, Rating
--cc Closed captions / subtitles
--creative-commons Creative Commons license
--purchased Purchased titles only
--4k 4K only
--hd HD only
--hdr HDR only
--live Live only
--360 360-degree video
--vr180 VR180 only

ytb trending [--flags]. Trending videos.

Flag Meaning
--category Music, Gaming, Movies, News

comments

ytb comments <video-id|url> [--flags]. Comments and replies.

Flag Meaning
--sort Top, New
--replies Also fetch replies (parent_id set)

Use -n for how many, and -n 0 for no cap at all.

community

ytb community <channel-id|@handle> [--flags]. Community / posts tab. No notable flags beyond the globals.

hashtag

ytb hashtag <tag> [--flags]. Streams a hashtag's videos.

Flag Meaning
--info The hashtag's own record instead of its videos

ytb related <video-id|url> [--flags]. The related-videos graph. No notable flags beyond the globals.

discover

ytb discover <seed>... [--flags] (alias walk). Walk the graph of linked objects breadth-first from one or more seeds (a video, channel, or playlist reference), streaming one row per node reached. See graph discovery.

discover streams records, one per node it reached. graph, below, streams claims, and the two answer different questions: what is out there, and who said so.

Flag Meaning
--follow Edges to follow: a preset (content, feed, comments, all) or a comma-separated edge list (channel, related, comments, uploads, playlists, community, items, owner, commenter). Default content
--depth Hops to follow from each seed (default 1; 0 = seeds only)
--fanout Max neighbors to follow per edge (default 25; 0 = unlimited)
--store Write every node reached into the local store: a record for what it fetched, a sighting for what it only saw

The comment edges (comments, commenter) are served only when YouTube is not applying its per-IP Restricted Mode to this network; when it is, they are noted on stderr and skipped and the rest of the walk continues. -n/--limit is the total node budget (default 500).

id

ytb id <ref>. Classify any id, handle or URL, say what it names, and derive whatever follows from it without a request. No flags beyond the globals.

Four of YouTube's id shapes decode locally and the command says which one it got, whether reaching it needs a request, and why.

$ ytb id UUuAXFkgsw1L7xaCfnd5JJOw -o json
{
  "id": "UUuAXFkgsw1L7xaCfnd5JJOw",
  "kind": "uploads_playlist",
  "url": "https://www.youtube.com/playlist?list=UUuAXFkgsw1L7xaCfnd5JJOw",
  "channel_id": "UCuAXFkgsw1L7xaCfnd5JJOw",
  "needs_request": false
}

A channel id is five playlists: swap the UC prefix for UU, UULF, UUSH, UULV or UULP and you have every upload, the long-form videos, the shorts, the past streams and the popular list, none of which costs a request to construct. The reverse works too, which is why a UU id found in a payload names a channel nobody fetched.

A handle reports needs_request: true and says why: a handle is an alias a channel can change, so it is a property of a channel and never the key. A video id reports needs_request: false and stops, because eleven characters of base64url decode to nothing else and a tool that guessed the channel from them would be making it up.

edges

ytb edges <ref>... [--flags]. Read anything YouTube has an id for and print what that read claims. See claims and RDF.

A claim is an edge with its provenance attached: which URL asserted it, which surface answered, which client ytb said it was, and at which tier. The source is part of the claim's identity, so two surfaces asserting the same edge stay two rows and a disagreement between them is something you can query rather than something the last read overwrote.

One request per reference. Each flag below is another read and says what it costs. A handle costs one more, to resolve it, because a handle is not an identity and can never be a node.

Flag Meaning
--captions Read the ANDROID player for the caption list (1 request)
--comments Read up to n comments (1 request per page of about 20)
--items Read up to n playlist items (1 request per page)
--featured Read a channel's home tab for its featured channels (1 request)
--posts Read up to n community posts (the tab refuses tier 0 today)
--music Read the same id through YouTube Music (1 request)

predicates

ytb predicates. The twenty one predicates the graph plane may write, with the domain and range of each, the RDF term it maps to, and where on the site it comes from. This command makes no request. No flags beyond the globals.

The table is closed. A predicate not in it cannot be written, which is what stops a typo becoming a claim that looks fine, is never queried because nobody knows to ask for it, and is found a year later by somebody counting.

Where the arrow turns round on the way to RDF the rdf column says (inverse): ytb writes channel published video because that is the direction a page reads in, and schema:author runs from the work to its author.

surfaces

ytb surfaces. The surface table every record's surfaces field points into. This command makes no request. No flags beyond the globals.

id constant host tier
s1 SurfaceWatchHTML www.youtube.com 0
s2 SurfaceInnerTube www.youtube.com 0
s3 SurfaceMobilePlayer www.youtube.com 0
s4 SurfaceBrowseHTML www.youtube.com 0
s5 SurfaceOEmbed www.youtube.com 0
s6 SurfaceFeed www.youtube.com 0
s7 SurfaceThumbCDN i.ytimg.com 0
s8 SurfaceMediaCDN googlevideo.com 0
s9 SurfaceSuggest suggestqueries-clients6.youtube.com 0
s10 SurfaceMusic music.youtube.com 0
s11 SurfaceSession www.youtube.com 1

A surface is one way of reading YouTube rather than one endpoint. s2 is the whole web InnerTube API because browse, next, search and player answer with the same shapes from the same host under the same client. s3 is a separate surface from s2 despite also being /player, because asking as ANDROID gets stream URLs and caption URLs that return bytes and asking as WEB does not.

The tier column is the only one that is about you rather than about YouTube. Tier 0 is anonymous and covers everything except s11, which is a read that carried your cookies.

clients

ytb clients. The clients ytb identifies as, which is what the client field on a record names. This command makes no request. No flags beyond the globals.

name num version host
WEB 1 2.20260114.08.00 www.youtube.com
MWEB 2 2.20260114.08.00 www.youtube.com
ANDROID 3 20.10.38 www.youtube.com
IOS 5 20.10.4 www.youtube.com
ANDROID_VR 28 1.65.10 www.youtube.com
WEB_REMIX 67 1.20260114.03.00 music.youtube.com

Which client asked decides what comes back, so this is not cosmetic. ANDROID_VR is the one anonymous client that still answers /player with directly fetchable stream URLs and no proof-of-origin token, which is why downloads use it. WEB_REMIX is the only one music.youtube.com answers. WEB has the richest page data and the least media. The JSON form carries a user_agent and an extra per client, which is the per-client block that goes into context.client.

The version numbers are the part that goes stale. A read that starts failing for everybody at once is usually a version that has aged out.

routes

ytb routes. Every operation under its three names: the command path, the HTTP route ytb serve answers on, and the tool name ytb mcp publishes. This command makes no request. No flags beyond the globals.

ytb routes -o table --fields command,route,tool
| command      | route            | tool         |
|--------------|------------------|--------------|
| about        | /v1/about        | about        |
| music album  | /v1/music/album  | music_album  |

Thirty three rows today. The list is every read; the commands that write something local, download, crawl, archive, query, export, config and auth, are not on it because none of them is a read of YouTube and none of them is served. routes itself is not served either, because the server publishes its own list at /v1/openapi.json and a route that named the routes would be the one route missing from it.

rdf

ytb rdf <ref>... [--flags]. The same claims as n-triples, turtle or json-ld, with provenance. See claims and RDF.

The vocabulary was not invented here. YouTube publishes schema.org about its own pages, so a watch page is a schema:VideoObject, its author a schema:Person and both counts schema:InteractionCounter blocks, and the mapping is read off the site. Where the site says nothing, the terms are the ones x-cli and facebook-cli already export into, so a store from all three tools joins, and a predicate with no schema.org equivalent goes in the yt: namespace declared in the output rather than assumed.

Output is byte stable between two runs over the same input in all three formats, because a dump that reorders itself cannot be diffed and a diff is how somebody notices that YouTube started saying something different.

Flag Meaning
--format nt (default, streams), turtle, jsonld
--check Compare our triples with the page's own microdata, predicate by predicate
--types-only Write nothing but the rdf:type of each node
--no-provenance Drop the source and client annotations

ytb rdf takes the same six extra-read flags as ytb edges.

graph

ytb graph <seed>... [--flags]. Collect claims from the seeds, then from the nodes those claims named, and so on, and print how many claims of each predicate came back.

--depth 0 is the seeds and nothing else, which is exactly what ytb edges does.

The budget is in requests and it is counted, not estimated. Every request that goes out passes a hook the walk counts on, and the walk stops when the count reaches the budget, mid-level if that is where it lands. A cache hit never reaches the hook, so it is not a request and does not count, which is why a second walk over the same seeds gets further on the same budget.

The frontier is every node the claims so far have named and nothing has read. Only nodes ytb can read are followed, so an external URL and a hashtag are named and never fetched.

Flag Meaning
--depth Hops to follow from each seed (default 1; 0 = the seeds only)
--budget How many requests the walk may spend (default 25)
--edges Print the claims themselves rather than the per-predicate counts

ytb graph takes the same six extra-read flags as ytb edges.

suggest

ytb suggest <query> [--flags]. Search autocomplete suggestions. No notable flags beyond the globals.

This reads s9, the JSONP autocomplete endpoint, which is not InnerTube: no key, no POST and no client context. So a suggestion record carries an empty client list where every other record names at least one. The suggestions are conditioned on the locale in the request, and the request is in sources, so two runs that disagree can be told apart by their --hl and --gl.

transcript

ytb transcript <video-id|url> [--flags]. Fetches one caption track and writes it out. Use ytb captions to see what a video has.

Flag Meaning
--format text (default), srt, vtt, json
--lang Caption language code
--auto Take the auto-generated track
--translate Ask YouTube to machine-translate the chosen track into this language code
--out Write to this file instead of stdout

All four serializations come off one parse, so the timings in the srt and the json are the same timings.

--lang en matches en-GB when there is no plain en. With no --lang the human track wins over the auto-generated one, because a person's punctuation is worth more than a machine's word list. --auto asks for the auto track even when a human one exists, and it is the only one that carries per-word timings, which json keeps.

No yt-dlp, no Deno and no JavaScript interpreter is involved: the track is fetched and parsed in-process. When the caption endpoint refuses and a yt-dlp binary is on PATH, it is used as a fallback and says so.

captions

ytb captions <video-id|url>. The caption tracks a video has, one record each: language code, vss_id, name, whether it was machine generated and whether it can be auto-translated. No flags beyond the globals.

The tracks come off the ANDROID player. The watch page lists the same tracks with URLs that answer HTTP 200 and an empty body, so they are not listed here.

vss_id is the track's own name for itself, and it is the column that matters: .en is the human English track and a.en is the machine one, which is what tells two tracks with the same language code apart.

$ ytb captions dQw4w9WgXcQ
╭───────────────┬─────────┬──────────────────────────┬───────┬──────────────╮
│ LANGUAGE_CODE │ VSS_ID  │ NAME                     │ AUTO  │ TRANSLATABLE │
├───────────────┼─────────┼──────────────────────────┼───────┼──────────────┤
│ en            │ .en     │ English                  │ false │ true         │
│ en            │ a.en    │ English (auto-generated) │ true  │ true         │
│ de-DE         │ .de-DE  │ German (Germany)         │ false │ true         │
│ ja            │ .ja     │ Japanese                 │ false │ true         │
│ pt-BR         │ .pt-BR  │ Portuguese (Brazil)      │ false │ true         │
│ es-419        │ .es-419 │ Spanish (Latin America)  │ false │ true         │
╰───────────────┴─────────┴──────────────────────────┴───────┴──────────────╯

ytb transcript fetches one of these tracks; this command is the one that says which tracks exist.

formats

ytb formats <video-id|url> [--flags]. Lists formats from /player streamingData, deduped by itag, audio first then video then muxed. Metadata only by default; pass --urls to resolve the deciphered, directly-fetchable stream URLs through the native engine.

The list is read from the ANDROID player, which is the only one that answers with plain URLs. On the watch page every adaptive format arrives with a contentLength and neither a url nor a signatureCipher, so there is nothing to fetch, and the read says so when it has to fall back to it.

Under the table is a note naming the client that answered and when its URLs expire, and saying that fetching any of them without a Range header runs at 32 KiB/s where the same URL fetched in ranges runs at 4 MiB/s. It goes to stderr, so -o json stays a clean stream. A muxed format comes back from the mobile player with no contentLength, so its size column is empty and its note says size unknown rather than showing a 0 that reads as an empty file.

The table shows eight columns and -o json carries the whole record: kind, container and codec parsed off the mime type, average_bitrate, audio_channels, audio_sample_rate, approx_duration_ms, init_range and index_range for a DASH reader, last_modified for when the rendition was encoded, the url when a mobile player answered, expires_at off the URL's own expire, and is_throttled_unranged, which is true on every format because it is a fact about googlevideo rather than about the format. A field that does not apply is absent rather than zero, so an audio format has no width and no fps instead of having them set to 0.

Flag Meaning
--audio Audio-only adaptive formats
--video Video-only adaptive formats
--muxed Progressive (muxed) formats only
--urls Resolve playable stream URLs (deciphered) via the native engine

music

ytb music [command] [--flags]. Search and browse YouTube Music via the WEB_REMIX client context.

Subcommand What it does
search <query> Search everything, or one kind with --type
artist <browseId|url> Artist profile with discography, top songs, videos and related artists
album <browseId|url> Album header and track list
playlist <id|url> Music playlist and tracks
track <video-id> Track detail (with --lyrics if available)

Notable subcommand flags:

Flag Subcommand Meaning
--type music search song, video, album, artist, playlist, podcast, episode
--lyrics music track Fetch the lyrics tab if it has any

Every row is classified by the type on its own endpoint and never by the word the page rendered next to it, so a search reads the same in any --hl. music_video_type is ATV for an art track and OMV, UGC or OFFICIAL_SOURCE_MUSIC for a video of the same song, which is what keeps the two ids of one song apart. A count with no type on it, like 116 songs against 121 views, is kept verbatim in metadata_parts rather than filed as an item count.

download

ytb download <id|url>... [--flags]. Downloads media with the built-in pure-Go engine.

The native engine fetches streams through the ANDROID_VR client (no API key, no token), which answers with plain signed URLs, so there is nothing to decipher and no JavaScript to run.

Every request goes out as a byte range, in 1 MiB chunks by default. That is not a tuning choice: an un-ranged GET to googlevideo is throttled to about 32 KiB/s and never finishes, while the same URL fetched in ranges runs at line speed. Because contentLength is known before the first byte, the progress total is real and --continue resumes from the size of the part file.

--audio and --video each write one stream and need nothing else installed. --mux fetches both and merges them, which needs ffmpeg, as do --audio-format and --embed-thumbnail. When a requested operation needs ffmpeg and none is found on PATH (or at --ffmpeg-bin / YTB_FFMPEG_BIN), the command exits with code 6. Pass --use-yt-dlp to delegate to a yt-dlp binary instead.

The --format selector accepts a yt-dlp-style grammar: keywords (best, worst, bestvideo/bv, bestaudio/ba, bv*), explicit itags (22), a single + to merge a video and audio track (bv*+ba, 137+140), / fallback groups (bv*+ba/b), and [key OP value] filters on height, width, fps, ext, vcodec, acodec, itag, and bitrate (bv*[height<=720]+ba).

Flag Meaning
-x, --audio Download the audio stream only, no ffmpeg needed
--video Download the video stream only, no ffmpeg needed
--mux Download video and audio and merge them, needs ffmpeg
--audio-format Convert audio to this codec (mp3, m4a, opus, flac); needs ffmpeg
-f, --format Format selector (e.g. best, 22, bv*+ba, bv[height<=720]+ba)
--itag Download this exact itag, as listed by ytb formats
--quality Max video height (e.g. best, 1080, 1080p)
--out Output directory (.)
--output-template yt-dlp-style filename template (default %(title)s [%(id)s].%(ext)s)
--chunk Byte range requested per GET (e.g. 512K, 1M, 4M; default 1M)
--concurrent-fragments Parallel byte-range workers (4)
--continue Resume into an existing part file instead of starting over
--embed-thumbnail Embed the thumbnail as cover art (mp4/m4a); needs ffmpeg
--playlist-items Playlist item selection (e.g. 1,3,5-7,10-)
--sub-langs Subtitle language to write (e.g. en)
--sub-format Subtitle format to write (srt, vtt, txt)
--write-subs Write the subtitle sidecar file
--download-archive Record downloaded ids here and skip ones already present
--use-yt-dlp Delegate to a yt-dlp binary instead of the native engine

extract

ytb extract <audio|video|transcript|all> <id|url> [--flags]. Extract a specific stream via yt-dlp.

Flag Meaning
--format Audio format for extract audio (e.g. mp3)
--quality Max video height for extract video (e.g. 1080)
--out Output directory (.)

sponsorblock

ytb sponsorblock <id|url> [--flags]. Lists community-submitted segments from the public SponsorBlock API (sponsor, intros, outros, self-promo, and more). This is an independent community service; no key is required.

Flag Meaning
--categories Segment categories to fetch (default all): sponsor, selfpromo, intro, outro, ...

thumbnail

ytb thumbnail <id|url> [--flags]. Lists the standard thumbnail renditions, or writes the best available one to disk.

Each of the five standard names can be constructed for any video id and only some of them exist, so the list is HEADed before it is printed and a rendition that answers 404 is left out. A video with no maxresdefault answers that URL with a 1097 byte body that is still Content-Type: image/jpeg, so nothing but the status code separates a rendition from a placeholder.

Flag Meaning
--fetch Write the best available rendition to disk
--unconfirmed List the constructed URLs without HEADing them
--out Output path or directory for --fetch

chapters

ytb chapters <id|url>. Lists a video's chapters: position, start time, title and origin. No notable flags beyond the globals.

origin is a column because a macro marker and a timestamped description are two different things that produce the same list. markers means YouTube served a macroMarkersListRenderer, so the site itself treats these as chapters, draws them on the scrubber and has a preview frame for each. description means somebody typed 1:23 Verse 2 and the list is only as good as their typing. Both line layouts are read: the timestamp can come first, as in 0:00 Introduction, or last, as in Eyes To The Sky - 0:00:00.

crawl

ytb crawl <seed>... [--flags]. Read each seed, write its claims into the store, then read what those claims named, hop by hop, until the depth or the budget runs out.

The budget is counted in requests rather than estimated, and a cache hit makes no request so it costs nothing. The frontier is every node the store has heard of and not read, which is a query and not a queue, so --resume with no seeds picks up where an earlier run stopped. Mixes, a channel's popular playlist, comments and formats are off the frontier by default, though none of them is refused as a seed.

Flag Meaning
--depth Hops to follow from each seed (default 1; 0 = seeds only)
--budget Request budget for the whole crawl (default 200; 0 = no limit)
--items Playlist items to read per playlist (default 100; 0 = header only)
--comments Comments to read per video (default 0)
--posts Community posts to read per channel (default 0)
--captions Read each video's caption list (one ANDROID player call per video)
--featured Read each channel's featured channels shelf
--music Read each video through YouTube Music too
--uploads Put each channel's derived playlists on the frontier (default true)
--resume Start from the nodes the store has heard of and not read
--manifest Where to write the manifest (default <data-dir>/crawls/crawl-<unix>.json)

archive

ytb archive <ref> [--flags]. Write one read down in full: the page, every InnerTube payload, a meta.json naming each request and the file its answer went into, and a record.json with the records and claims ytb parsed.

It needs the cache on, since it works by replaying what the cache stored, so --no-cache is refused. Session headers are removed from meta.json before it is written.

Flag Meaning
--dir Where to write (default <data-dir>/archive/<ref>-<unix>)
--items Playlist items to read, for a playlist ref
--comments Comments to read, for a video ref
--captions Read the caption list too

query

ytb query <sql>. Run one SQL statement over the store and print the rows. The file is opened read-only, so a statement that would write is refused by SQLite itself rather than by a check in ytb. No notable flags beyond the globals.

ytb query "select predicate, count(*) c from claims group by 1 order by c desc"
ytb query "select uri from nodes where kind='video' and record is null limit 20"

export

ytb export [channel-id|@handle] [--flags]. Render the stored data as an interlinked Markdown site. With no argument, every channel in the store is exported.

Flag Meaning
--out Output directory for the Markdown site

db

ytb db [command]. Inspect the store at <data-dir>/ytb.db. Pure-Go, no cgo.

Three tables. nodes is everything with an identity, one row per URI, with the record as JSON and a null record for a node a claim named that nobody has fetched. 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. reads is the log: every request, what answered it, and how big it was.

Subcommand What it does
stats Nodes by kind, claims by predicate, reads by surface and client
search <query> Full-text search over stored data
path Print the db file location
vacuum Compact the database file
reset Drop and recreate all tables

Notable subcommand flags:

Flag Subcommand Meaning
--channels db search Search channels instead of videos

cache

ytb cache [command]. Inspect or empty the response cache at <data-dir>/cache/innertube.

Every read goes through it, keyed by the URL plus the client that claimed it, so a WEB player response is never served to a caption read that asked as ANDROID. An entry older than --cache-ttl is not served and is not deleted either, so a cache can be almost entirely stale and still take up the whole of its space.

Subcommand What it does
path Print the cache directory
info Entries, how much space they take, how many are still fresh, and the age range
clear Delete every entry, fresh or stale
$ ytb cache info
╭─────────┬───────────────────────────────────────────────╮
│ KEY     │ VALUE                                         │
├─────────┼───────────────────────────────────────────────┤
│ dir     │ /Users/apple/.local/share/ytb/cache/innertube │
│ ttl     │ 15m0s                                         │
│ entries │ 350                                           │
│ fresh   │ 4                                             │
│ stale   │ 346                                           │
│ size    │ 263.7 MiB                                     │
│ oldest  │ 2026-07-30T09:23:06+07:00                     │
│ newest  │ 2026-08-12T10:40:09+07:00                     │
╰─────────┴───────────────────────────────────────────────╯

Fresh and stale are counted against the --cache-ttl in effect for this run rather than stored, so the same 350 files come back 109 fresh under ytb cache info --cache-ttl 24h.

clear takes --dry-run, which reports the count and the size it would free and deletes nothing. Nothing in here is a record: the store keeps what was parsed and this keeps the bytes a request answered with, so clearing it costs time on the next run and loses nothing. ytb archive writes outside this cache and is never touched by clear.

All three exit 2 with a usage error under --no-cache, rather than printing an empty path or a count of zero and letting you read that as an empty cache.

auth

ytb auth [command] [--flags]. Manage your YouTube session, which is optional and which the signing in guide covers in full.

Subcommand What it does
import Store the session cookies out of a browser you are already signed into
status Name the stored cookies, without their values, and say what they unlock
clear Delete the local cookie file (alias logout)
Flag Subcommand Meaning
--cookies auth import A cookies.txt path, a pasted Cookie header, or - for stdin

config

ytb config [command] [--flags]. View and manage configuration.

Subcommand What it does
show Print the resolved configuration
path Print the config file path
init Write a commented config template
edit Open the config file in $EDITOR

version

ytb version [--flags]. Print version information.

Flag Meaning
--short Print just the version number