- Python 96.8%
- Makefile 3.2%
| bin | ||
| config | ||
| mpv | ||
| .gitignore | ||
| Makefile | ||
| README.md | ||
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-dlpfixes 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.