yth — search your YouTube watch history¶
yth is a local-first CLI + Television (tv) picker for your own YouTube watch history.
YouTube's native /feed/history search only matches titles; yth indexes titles,
channels, descriptions, and caption text in a local sqlite database and searches them with
FTS5 — so "I know I watched something about X" becomes a one-line query. It's the
gh-select-style twin pattern used by mlf + tv mlflow and fleet hosts.
Binary: dot_dotfiles/bin/executable_yth (→ ~/.dotfiles/bin/yth). Modules: scripts/yth/.
Picker twin: tv yth (dot_config/television/cable/yth.toml).
Install¶
Deployed automatically by chezmoi:
ythself-bootstraps its Python deps via auv run --scriptshebang (PEP 723 inline metadata declaringyt-dlp,tyro,rich,platformdirs) — no separate install step.yt-dlpis also installed as a standalone binary by thepython_uv_toolsansible role (same dual pattern asmlflow↔mlf), soyt-dlpis on PATH independently.- Needs
uvon PATH;batfor thetv ythpreview pane;ffmpeg(already via themedia_toolsrole) is optional (only for muxing, whichythdoesn't do).
Two data sources (and why)¶
Your watch history is not in the YouTube Data API (removed ~2016). yth gets it two ways:
| Source | Command | Cookies? | Coverage | Timestamps |
|---|---|---|---|---|
| Google Takeout (primary) | yth import-takeout <file> |
no | full multi-year history | exact (from the export) |
yt-dlp :ythistory (secondary) |
yth sync |
yes | recent page-loadable window only | approximate (sync time) |
Export from Google Takeout → YouTube and YouTube Music →
history → JSON format → watch-history.json. Import is idempotent: re-importing the
same (or an overlapping) export adds nothing. Use sync for quick incremental top-ups
between exports; Takeout stays the source of truth.
Quick start¶
yth import-takeout ~/Downloads/watch-history.json # backfill (cookie-free)
yth enrich --limit 500 # fill descriptions/durations (public)
yth fetch-subs --recent 100 # index captions for recent videos
yth search "rust async" # search title/channel/description
yth search "borrow checker" --subs # also search caption text
tv yth # fuzzy-pick the whole history
Subcommands¶
| Command | What it does |
|---|---|
import-takeout <file> |
Backfill from a Takeout watch-history.json (JSON only). Idempotent. |
sync [--limit N] [--full] |
Incremental :ythistory sync (needs cookies). Walks newest→cursor. |
enrich [--limit N \| --all] [--cookies] [--force] |
Fetch title/description/duration/upload_date. Cookie-free for public videos. Permanent failures (removed/private) are stamped; transient (bot-check/429) retry next run. |
fetch-subs <id>… \| --recent N \| --all [--cookies] [--langs …] |
Download manual+auto captions, flatten VTT→text, index. |
search <query> [--subs] [--json] [--limit N] [--raw] |
FTS5 search. --subs also matches captions; --raw uses native FTS5 syntax. |
list [--tsv] [--limit N] |
List history newest-first. --tsv is the tv yth source. |
show <id> [--json] |
One-video detail (DB-only, no network). Powers the tv preview. |
open <id> / copy <id> / play <id> |
Open in browser / copy URL / play (mpv if configured, else browser). |
YouTube runtime + cookies (only sync + restricted videos)¶
Public videos need no cookies — enrich and fetch-subs use packaged
yt-dlp-ejs + the repo-managed Node 22+ runtime and should work unauthenticated. Legacy
armv7/armv6 Node 20 and EL7 Node 16 are deliberately not selected as EJS runtimes; doctor
reports that platform limitation. Cookies are intrinsically required only for yth sync (account-private history) and private/restricted
content. For complete history, prefer cookie-free Google Takeout.
Do not collapse every Sign in to confirm you're not a bot into a cookie problem. Fix EJS /
Node warnings first, then stop retrying, use a clean residential IP, and reduce request rate.
Upstream warns that using a logged-in account with yt-dlp can cause temporary or permanent
account suspension.
Cookies are bearer credentials
Never print, paste, screenshot, commit, chezmoi add, or cloud-backup cookies. Prefer a
dedicated YouTube-only profile/account, not a daily primary account. The target
~/.config/{yth,ytmv}/cookies.txt are explicitly excluded from chezmoi management.
The shared loader requires mode 0600 and rejects malformed, expired/empty, and non-YouTube-domain
files before yt-dlp can echo a malformed row or consume the jar.
yt-dlp's supported browser names include brave, chrome, chromium, edge, firefox, opera,
safari, vivaldi, whale — not Arc or Zen:
| Browser | Mechanism | Config |
|---|---|---|
| Zen (Firefox fork) | Read its Firefox-format profile; auto-detected when installed. | from_browser = "firefox:/path/to/zen/Profiles/<profile>" |
| Arc | Arc is not a supported browser name; Chrome's macOS handler is hard-coded to Chrome Safe Storage. Use an isolated YouTube-only export. | cookiefile = "~/.config/yth/cookies.txt" |
| Standard Chrome/Firefox/… | Use the supported name, preferably with a dedicated profile. | from_browser = "firefox" (or chrome, …) |
Safe Arc/Chromium export¶
- Open a private/incognito session and sign in to YouTube.
- Keep exactly one tab: https://www.youtube.com/robots.txt.
- Export only
youtube.comcookies using the exact extension Get cookies.txt LOCALLY. The similarly named former extension without “LOCALLY” was reported as malware. - Save as
~/.config/yth/cookies.txt, thenchmod 600 ~/.config/yth/cookies.txt. - Close the private session; replace/delete the file after expiry/invalidation.
Do not combine --cookies-from-browser with --cookies FILE to export a daily profile: it
can write cookies for every site. ytmv doctor --cookies safely checks source
loading/decryption without printing contents.
On macOS, find-generic-password failed plus cannot decrypt v10 cookies: no key found
may mean Chrome's exact Safe Storage item is absent. Status 44 / OSStatus -25300 happens
before authorization, so no popup appears; a replacement key cannot decrypt old values. See
ytmv help for the metadata-only diagnostic and safe alternatives.
The tv yth channel¶
tv yth (or bare yth) opens a fuzzy picker over yth list --tsv. Television's source is
single-shot and never sees the live query, so the channel fuzzy-filters the title/channel
display string only — caption/description search stays in yth search --subs.
| Key | Action |
|---|---|
Enter / Ctrl+O / Alt+B |
Open the video in the browser |
Ctrl+Y |
Copy the video URL |
Alt+P |
Play (mpv if configured, else browser) |
Alt+S |
Fetch captions for this video, then show detail |
Alt+J |
Dump the video's JSON detail |
Alt+ (not Ctrl+) is used for new actions because this repo's tmux prefix is Ctrl+b —
a ctrl-b binding would be swallowed before tv saw it. The o/b/y/p/s/j mnemonics are the
SSOT for a future yth tui.
Config — ~/.config/yth/config.toml¶
# cookiefile = "~/.config/yth/cookies.txt" # isolated YouTube-only export; chmod 600
# from_browser = "firefox:/…/zen/Profiles/xxxx.Default" # dedicated Firefox/Zen profile
langs = ["en", "en-US"] # subtitle languages to fetch
open_target = "browser" # browser | mpv
Storage¶
Resolved via platformdirs (honours XDG_*): DB at ~/.local/share/yth/history.db, config
at ~/.config/yth/config.toml, cache at ~/.cache/yth/. Set $YTH_DB to override the DB
path (used by tests). Schema: videos (+ videos_fts), watch_events (one row per watch;
stats via the video_stats view), subtitles (+ subtitles_fts), meta. FTS is
external-content with triggers, so enrich/fetch-subs UPDATEs re-index automatically.
Cross-file invariants¶
Touching yth means keeping these in sync (see the CLAUDE.md cross-file table row):
dot_dotfiles/bin/executable_yth— launcher (PEP723 deps + dict-dispatch). Shared helpers live inscripts/yth/__init__.py; leaf subcommands inscripts/yth/*.py.dot_config/television/cable/yth.toml— theo/b/y/p/s/jmnemonics mirror any futureyth tui; new actions useAlt+.dot_config/zsh/tools/53_yth_completion.zsh+dot_config/bash/53_yth_completion.bash— keep the two in sync (Strategy B).yt-dlp[default]stays aligned across this launcher's PEP723 block, ytmv's PEP723 block, andpython_uv_tools;yt-dlp-ejsplusyt_dlp_runtime_opts()/Node are one runtime contract.- Docs: this page (+
yth.zh-TW.md),docs/shells/aliases.md,docs/zsh/zsh-completions.md§F,docs/this_repo/tool-managers.mdA–Z + uv inventory,mkdocs.ymlnav,dot_agents/skills/chezmoi-dotfiles/SKILL.md.tmpl.
Troubleshooting¶
Sign in to confirm you're not a botonenrich/fetch-subs/sync— treated as transient (not stamped). Fix EJS/Node warnings first, stop repeated retries, leave datacenter/VPN/proxy egress, and reduce rate. Cookies are intrinsic forsync, but an account-risking last resort—not the first fix—for publicenrich/fetch-subs.no captions— the video genuinely has no subtitles in the configuredlangs. Add langs (--langs en,en-orig,zh-Hant) or accept it (the video is stamped so it won't retry;--forceto retry).yth sync: no cookie source configured— expected until you setcookiefileorfrom_browser. Preferyth import-takeoutfor full history (no cookies needed).- Empty
tv yth— no history imported yet. Runyth import-takeoutoryth sync.