NewPipe-style YouTube client for the terminal: fzf UI, mpv playback, yt-dlp extraction, RSS subscriptions
  • Python 96.8%
  • Makefile 3.2%
Find a file
2026-08-17 17:58:16 +02:00
bin Add Shorts, transcription, and robust high-quality playback 2026-08-17 17:58:16 +02:00
config Add Shorts, transcription, and robust high-quality playback 2026-08-17 17:58:16 +02:00
mpv Initial commit: yt — NewPipe-style terminal client (fzf + mpv + yt-dlp) 2026-08-17 11:47:12 +02:00
.gitignore Initial commit: yt — NewPipe-style terminal client (fzf + mpv + yt-dlp) 2026-08-17 11:47:12 +02:00
Makefile Initial commit: yt — NewPipe-style terminal client (fzf + mpv + yt-dlp) 2026-08-17 11:47:12 +02:00
README.md Add Shorts, transcription, and robust high-quality playback 2026-08-17 17:58:16 +02:00

yt — a NewPipe-style YouTube app for the terminal

No Electron, no daemon, no account. fzf as the UI, mpv as the player, yt-dlp as the extractor, and optional typeout transcription.

Install

git clone https://git.miskam.xyz/mxm/yt.git && cd yt
make deps        # brew install yt-dlp mpv fzf
make install     # installs bin/yt to ~/.local/bin, seeds ~/.config/yt/
yt doctor        # verify

make install never overwrites an existing ~/.config/yt/channels, config.json, or ~/.config/mpv/mpv.conf. make uninstall removes only the binary.

SponsorBlock in mpv (optional but recommended):

brew install --cask mpv   # any mpv works
curl -L -o ~/.config/mpv/scripts/sponsorblock.lua \
  https://raw.githubusercontent.com/po5/mpv_sponsorblock/master/sponsorblock.lua
# requires a sponsorblock API call per video — set "sponsorblock" script-opts if wanted

Daily workflow

yt                        subscription feed (newest first, cached 30 min; Shorts labeled)
yt veritasium             feed filtered to channel name substring
yt -f                     force-refresh the feed cache
yt channels               pick one of your channels → its latest videos
yt channels add <url>     subscribe (resolves @handle / channel URLs automatically)
yt channels rm <pattern>  unsubscribe
yt search <query>         video search (no API key — via yt-dlp ytsearch)
yt search -c <query>      channel search: tab-mark channels, enter = subscribe
yt search -p <query>      playlist search: enter = browse contents
yt queue                  watch-later list (play removes from queue; ctrl-d drops)
yt history                everything you watched, re-watchable
yt dl <url> [--audio]     download video / extract mp3
yt doctor                 sanity-check setup

Picker keys

key action
type fuzzy-filter the list
/ or ctrl-j/ctrl-k move the cursor
tab / ctrl-t mark/unmark video (shift-tab marks upward)
enter play focused video — marked videos play as a playlist (in mpv: < prev, > next)
ctrl-q add focused/marked to the watch-later queue
ctrl-y transcribe focused/marked videos and copy the text
ctrl-a/ctrl-u mark all / mark none
ctrl-d (in yt queue) drop marked from queue
esc back out

Shorts are identified from YouTube's feed links or a cached Shorts-URL check and shown with a [SHORT] label in video lists.

If tab does nothing: VS Code's "Tab Moves Focus" mode (cmd+shift+m) swallows Tab — use ctrl-t instead. If arrow keys do nothing: you're inside an embedded terminal tab (e.g. a zot command tab) that eats escape sequences — use ctrl-j/ctrl-k/ctrl-n/ctrl-p, which are single bytes and always get through, or run yt in a real terminal.

Playback robustness

Before playback, yt prints a compact overview with the creator, title, published date, length, and selected resolution/frame rate/codec.

Single videos use two yt-dlp-managed pipes: best-quality video up to 1080p, plus audio from YouTube's reliable combined progressive format. mpv combines them during playback. yt confirms that both pipes produce data and retries fresh CDN URLs before launching mpv, avoiding ffmpeg's direct Googlevideo requests and their intermittent HTTP 403 errors. If the adaptive streams still fail, yt retries with the combined progressive format alone. Override video_sort in config.json to change the quality cap. Marked-video playlists still use mpv's native playlist mode and fall back to the pipe if direct playback fails. Keeping yt-dlp current matters more than anything else here.

Files

~/.config/yt/channels      one channel per line: "name | UCxxxx" or any YT URL
~/.config/yt/queue.json    watch-later list        ★ = queued, shown in feed
~/.config/yt/config.json   cache_minutes, search_results, mpv_args, video_format, video_sort, audio_format, typeout_args, feed_workers
~/.cache/yt/feed.json      merged feed cache (30 min TTL)
~/.cache/yt/shorts.json    cached Shorts classifications
~/.cache/yt/transcripts/   cached audio used for transcription
~/.local/state/yt/history.json   watch history (last 1000)

Importing NewPipe subscriptions

NewPipe → Settings → Export → subscriptions JSON. Convert:

jq -r '.subscriptions[] | .name + " | " + .url' newpipe_subscriptions.json \
  >> ~/.config/yt/channels
yt        # first run resolves all URLs to channel IDs (one-time, slowish)

Why this shape

  • Playback is mpv's problem — hardware decode, resume-position, SponsorBlock, keybinds, all from the mpv ecosystem instead of reimplemented.
  • Extraction is yt-dlp's problem — when YouTube changes something, brew upgrade yt-dlp fixes it; the app never breaks.
  • State is plain files — grep it, edit it, sync it with git, nuke it.
  • UI is fzf — instant fuzzy filter, zero layout code.