Skip to content

marcelocantos/ytt

Repository files navigation

ytt

Fetch YouTube video transcripts from the command line.

A thin wrapper around youtube-transcript-api that extracts video IDs from URLs, handles multiple videos, and optionally prefixes each segment with a timestamp.

Install

Homebrew (recommended)

brew install marcelocantos/tap/ytt

From the GitHub release

Each release attaches standalone binaries for macOS arm64, Linux x86_64, and Linux arm64. Download the tarball matching your platform from the releases page, extract, and put ytt on your PATH.

From source

Requires Python 3.10+:

pipx install git+https://github.com/marcelocantos/ytt

Usage

ytt dQw4w9WgXcQ                                  # raw video ID
ytt https://www.youtube.com/watch?v=dQw4w9WgXcQ  # full URL
ytt https://youtu.be/dQw4w9WgXcQ                 # short URL
ytt --timestamps dQw4w9WgXcQ                     # one line per segment, [mm:ss] prefix
ytt --json dQw4w9WgXcQ                           # full API payload as JSON
ytt <id1> <id2> <id3>                            # multiple videos, blank line between

Plain output joins all segments with spaces — convenient for piping into word counts, LLM prompts, or search tools:

ytt dQw4w9WgXcQ | wc -w
ytt dQw4w9WgXcQ | grep -i "never"

With --timestamps (-t), each segment is on its own line:

[00:00] Never gonna give you up
[00:03] Never gonna let you down
[00:07] Never gonna run around and desert you
...

With --json, the full upstream payload is emitted — per-segment timing, language metadata, and the auto-generated flag — one compact JSON object per video (JSONL for multi-video):

ytt --json dQw4w9WgXcQ | jq .
{
  "video_id": "dQw4w9WgXcQ",
  "language": "English",
  "language_code": "en",
  "is_generated": false,
  "snippets": [
    {"text": "Never gonna give you up", "start": 0.0, "duration": 2.5},
    ...
  ]
}

Flags

Flag Purpose
-t, --timestamps Prefix each segment with [mm:ss] (or [h:mm:ss] for long videos), one per line
--json Emit the full API payload as JSON (one object per video, JSONL for multi). Mutually exclusive with -t.
--version Print version
--help Print usage
--help-agent Extended help oriented toward AI/agent consumers

Subcommands

Command Purpose
ytt ingest [PLAYLIST_URL] Bulk-ingest a playlist + tracked channels — see Playlist ingest below

Exit codes

Code Meaning
0 All transcripts fetched successfully
1 One or more videos failed (unavailable, transcripts disabled, etc.)
2 Usage error (no arguments, bad flag)

Errors are written to stderr, one line per failure, in the form ytt: <video-id>: <reason>.

Playlist ingest

ytt ingest is a bulk workflow for building a local knowledge base of video transcripts and synopses. It walks a YouTube playlist and any tracked channels, fetches transcripts for new videos, and writes one directory per video under $YOUTUBE_INGEST_ROOT.

export YOUTUBE_INGEST_PLAYLIST=https://www.youtube.com/playlist?list=PL...
export YOUTUBE_INGEST_ROOT=~/knowledge/youtube
ytt ingest

What lands on disk

$YOUTUBE_INGEST_ROOT/
├── <video-id>/
│   ├── .transcript/transcript.json # full youtube-transcript-api payload (hidden from Obsidian graph)
│   ├── meta.json                   # title, channel, upload date, duration, …
│   └── <slug>.md                   # synopsis (generated via `claude`)
├── .processed                      # dedup state (one video ID per line)
├── .channels/<handle>              # per-channel cursor file
├── .ingest.log                     # append-only run log
└── youtube-knowledge-base.md       # index, regenerated from the per-video files

Configuration

Env var Default Purpose
YOUTUBE_INGEST_PLAYLIST (required) Playlist URL. Can be passed as the first arg instead.
YOUTUBE_INGEST_ROOT ~/think/knowledge/youtube Where ingested videos land.
YOUTUBE_CHANNELS_FILE ~/.config/ytt/channels.yaml YAML list of channels to track newest-first. Resolved from $XDG_CONFIG_HOME/ytt/not the install directory, which a brew upgrade replaces.
YOUTUBE_INGEST_CONCURRENCY 4 Parallel video workers.
YOUTUBE_INGEST_YTT_BIN ytt (PATH) Absolute path to the ytt used for transcript fetches. Pin it in scheduled runs so launchd and your shell can't resolve two different builds.
YOUTUBE_INGEST_CLAUDE_BIN claude (PATH) Absolute path to Claude Code used for synopses. Pin it in scheduled runs because launchd has a deliberately minimal PATH.
YOUTUBE_INGEST_LOG $YOUTUBE_INGEST_ROOT/.ingest.log Ingest log path. Point it outside the content tree for scheduled runs.
YOUTUBE_INGEST_NETWORK_WAIT 14400 Seconds to wait (awake-time) for connectivity before giving up — covers launchd ticks that fire in a no-network DarkWake window.
YOUTUBE_INGEST_STALE_DAYS 7 Days of zero ingests (with channels tracked) before the run is judged unhealthy. 0 disables the check.
YOUTUBE_INGEST_STATE_DIR ~/.local/state/ytt Where the liveness stamp lives. Kept out of the content tree.
YOUTUBE_INGEST_BLURTER_BIN blurter (PATH) The blurter binary used to report events. Pin it in scheduled runs.

Scheduling

To run ingest on a daily schedule, install the bundled launchd agent:

cp "$(brew --prefix)/libexec/scripts/playlist-ingest/launchd/com.marcelocantos.youtube-ingest.plist" \
   ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.marcelocantos.youtube-ingest.plist

It runs the installed ytt ingest at 06:30 daily, pins the ytt binary, and writes logs to ~/.local/var/log/youtube-ingest/ (not into the content tree). Edit the playlist URL and paths in the plist first; they are machine-specific. The scheduled path is deliberately network-tolerant: a tick that fires while the machine is asleep or between networks waits for connectivity rather than recording a false "nothing new".

Tracking channels (optional)

Channel config lives in ~/.config/ytt/channels.yaml:

mkdir -p ~/.config/ytt
cp "$(brew --prefix ytt)/libexec/scripts/playlist-ingest/channels.example.yaml" \
   ~/.config/ytt/channels.yaml
$EDITOR ~/.config/ytt/channels.yaml
ytt ingest --dry-run     # confirm the channels are seen before ingesting

Resolution order is $YOUTUBE_CHANNELS_FILE, then $XDG_CONFIG_HOME/ytt/channels.yaml (default ~/.config/ytt/), then a copy beside the scripts (dev checkouts only). Keep your config out of the install directory: Homebrew replaces .../Cellar/ytt/<version>/libexec/ wholesale on every upgrade, so config stored there disappears silently.

On first sight of a channel, the latest video is ingested and recorded as a cursor — no backfill of older uploads. Subsequent runs walk newer videos until the cursor is hit. With no channels file at all, ingest runs in playlist-only mode.

If a channels file goes missing while .channels/<handle> cursors still exist, that is treated as an orphaned config — a real failure, not a preference — and the run fails loudly instead of reporting "nothing to do". This case cost 15 days of silent no-ops before v0.10.0.

Alerting

ytt does not deliver notifications. It reports events to blurter, which owns the Slack credential, the delivery policy (dedup, re-notify windows, recovery notices) and the sinks. blurter is a hard dependency, declared by the Homebrew formula.

brew services start blurter    # if it isn't already running

Configure Slack once, in blurter, not per app — see its README. ytt holds no credential and needs no notification configuration of its own.

A run is unhealthy when any of these hold, and each is reported as a problem event:

  • a preflight abort (missing ytt/claude/curl, or network never returned)
  • a discovery source failed (playlist or channel feed)
  • a channels config was orphaned (see above)
  • any queued video failed to ingest, or the run watchdog fired
  • the Claude spend limit cut the run short
  • the knowledge-base index failed to refresh
  • nothing has been ingested for YOUTUBE_INGEST_STALE_DAYS days while channels are tracked — the liveness backstop for "every step succeeded and yet no knowledge arrived"

A healthy run reports an ok event, which blurter keeps silent unless it closes out a problem it previously reported.

blurter send spools and exits, so reporting never blocks the ingest and never fails it, and an alert raised while the machine is offline is still delivered later. Repeat suppression, RECOVERED notices and sink fallback all live in blurter — ytt deliberately implements none of it.

Use ytt ingest --dry-run to run discovery and the health checks and print the queue without fetching transcripts, calling Claude, or reporting events.

Runtime dependencies

ytt ingest shells out to yt-dlp, jq, and yq; the synopsis step also runs claude (Claude Code CLI). The Homebrew formula declares yt-dlp, jq, and yq as depends_on; install claude separately via npm i -g @anthropic-ai/claude-code if you want synopses.

The bundled launchd plist pins both ytt and claude to absolute paths and fails the scheduled run if any discovered video does not land, so scheduler status reflects an incomplete ingest rather than a misleading successful exit.

Requirements

  • Internet access to YouTube (the underlying library scrapes YouTube's caption endpoints; YouTube occasionally changes these and breaks transcript fetching until the library catches up)
  • Python 3.10+ only if installing from source; the Homebrew and GitHub-release downloads bundle their own interpreter
  • For ytt ingest: yt-dlp, jq, yq on PATH (auto-installed via Homebrew), plus optionally claude (npm) for synopsis generation

License

Apache 2.0 — see LICENSE.

About

Fetch YouTube video transcripts from the command line.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages