ytcs.sh fetches YouTube channel feeds, lets you browse them with fzf, and plays videos with mpv and yt-dlp, so you can watch through your normal browser cookies instead of using the YouTube site.
If what you want is “show me my subscriptions, let me search quickly, and play things in mpv”, that is what this script is for.
TabfzfFor normal use you need:
mpvfzfyt-dlp or youtube-dlcurl or wgetUseful extras:
xmlstarlet for faster and more reliable feed parsingtimg for thumbnail previewsjq for --addsub when resolving YouTube handleskitty and wmctrl for --kittyxclip and/or copyq if you want the current video URL copied to your clipboardcatt for casting videos to a Chromecastcurl is the preferred fetcher when both curl and wget are installed.
Clone the repository from GitHub or GitLab and make the script executable if needed:
git clone <repo-url>
cd ytcs
chmod +x ytcs.sh
cp ytcs.env.example ytcs.env
Then edit ytcs.env if you want to change defaults.
If you create a local ./cache directory beside the script, ytcs will use that. Otherwise it falls back to ${XDG_DATA_HOME:-$HOME/.local/share}/ytcs.
These commands should give you a good idea of how ytcs works and what it can (and cannot) do.
Play a single video directly:
./ytcs.sh 'https://www.youtube.com/watch?v=rveUASDFk58'
Subscribe to a channel:
./ytcs.sh --addsub https://www.youtube.com/@complexly
Refresh cached feeds:
./ytcs.sh --refresh
Browse in time order:
./ytcs.sh --time
Browse without Shorts:
./ytcs.sh --time --noshorts
Subscribe to a second channel:
./ytcs.sh --addsub https://www.youtube.com/@pbsspacetime
Refresh cached feeds:
./ytcs.sh --refresh
Browse by subscription:
./ytcs.sh --subscription
Launch the interface inside kitty (if kitty is installed):
./ytcs.sh --kitty --time
Browse and send the selected video to the default Chromecast using catt, if installed.:
./ytcs.sh --kast --fancy --time
ytcs uses a mix of flags and positional arguments:
./ytcs.sh URL: the bare trailing URL is a positional argument for direct playback./ytcs.sh --import FILE: FILE is a positional argument consumed by --import./ytcs.sh --addsub URL: URL is a positional argument consumed by --addsubIf you pass a URL directly as the first non-option argument, ytcs hands it off to playback. This is the simplest mode:
./ytcs.sh 'https://www.youtube.com/watch?v=VIDEO_ID'
That also updates the local watched cache after playback.
If you want to cast instead, add --kast / -k. That always sends the full URL to catt cast. Check out and get catt at its repository. ytcs will send to the default configured device only.
There are three main subscription views:
--subscription / -s: browse by channel, then choose a video--grouped / -g: browse grouped channel sections--time / --chronological / -t / -c: browse one reverse-chronological feedGrouped and chronological views support multi-select queueing with Tab.
Videos detected as vertical content use V_GEOMETRY1 / V_GEOMETRY2 when available. That currently covers feed entries marked as Shorts and direct URLs that look like YouTube Shorts, TikTok, or Facebook video links.
Import a CSV export like this:
./ytcs.sh --import /path/to/subscriptions.csv
The CSV should use the usual channel export shape: channel id, URL, channel name, with no trailing comma. The included sample matches the format FreeTube exports.
The path after --import is positional, so keep it immediately after the switch.
You can add a channel from either a handle URL or a /channel/... URL:
./ytcs.sh --addsub 'https://www.youtube.com/@kurzgesagt'
Handle resolution uses the YouTube Data API, so YTUBE_API_KEY must be set in ytcs.env for @handle inputs.
The URL after --addsub is positional, so keep it immediately after the switch.
--help, -h: show help text--loud, -l: print progress and diagnostic output to stderr--kitty: relaunch in a dedicated kitty window using ytcs-kitty.conf--fancy, -f: force kitty graphics in previews without relaunching into kitty mode--refresh, -r: refresh cached feeds and rebuild grouped/time caches--noshorts, -n: exclude entries marked as Shorts from listing views--kast, -k: use catt instead of mpv for playback--import, -i FILE: import subscriptions from CSV--addsub URL: add one subscription from a YouTube handle URL or /channel/ URL--subscription, -s: browse videos by channel--grouped, -g: browse videos grouped by channel--time, --chronological, -t, -c: browse videos in reverse chronological orderIf you run ytcs.sh with no arguments, it opens an fzf launcher for the main actions.
Positional argument rules:
URL means “play this one video or page directly”--import consumes the following FILE--addsub consumes the following URL--kast is a mode flag and can be combined with direct-URL playback or the browsing viewsEnter: run the selected action or play the selected videoEsc: cancel the current viewTab: queue multiple videos in grouped and chronological viewsVideo previews can show:
timg is installedIn --kitty mode, the UI is relaunched inside kitty and the preview pane moves below the list.
In --fancy mode, the main UI stays in the normal layout, but preview rendering still gets kitty-style behavior. This can be handy, but it is also the mode most likely to behave oddly in tmux, nested terminals, or terminals that only partially support kitty graphics.
The defaults from ytcs.env.example are:
export MAX_CHANNEL_AGE=182
export MAX_GROUPED_VIDS=10
#export watchtop=4
export LOUD=0
export YTDLP_COOKIES="firefox"
export MARK_AGE="TRUE"
export GEOMETRY1="1366x768+50%+50%"
export GEOMETRY2="1366x768"
export V_GEOMETRY1="450x800+50%+50%"
export V_GEOMETRY2="450x800"
# This is if you have a special case for the provider server being on a
# nonstandard port or machine ONLY, see https://github.com/Brainicism/bgutil-ytdlp-pot-provider?tab=readme-ov-file#usage
export YTPOT_BASEURL="youtubepot-bgutilhttp:base_url=http://127.0.0.1:8080"
export YTUBE_API_KEY=""
The settings you are most likely to care about are:
MAX_CHANNEL_AGE: maximum channel age in days for grouped viewMAX_GROUPED_VIDS: maximum videos shown per channel in grouped viewwatchtop: maximum concurrent workers for refresh and parsing; if unset, ytcs uses up to your CPU core countYTDLP_COOKIES: browser profile source for yt-dlpMARK_AGE: enable or disable age markers in listsGEOMETRY1, GEOMETRY2: mpv geometry optionsV_GEOMETRY1, V_GEOMETRY2: mpv geometry options for vertical videoYTPOT_BASEURL: optional extractor args for the BGUtil POTS providerYTUBE_API_KEY: required for resolving YouTube handles in --addsub./cache or under ${XDG_DATA_HOME:-$HOME/.local/share}/ytcs.grouped_data.txt, time_data.txt, parsed_time/, and grouped per-channel cache fragments are derived caches.--refresh refreshes channel XML feeds and rebuilds grouped and chronological caches.--refresh, cached thumbnails older than 30 days are deleted automatically.--time reuses an existing valid time cache and only rebuilds it when needed.--addsub clears grouped and time caches so they rebuild on next use.--mark-watched to yt-dlp, and ytcs also records local watched state in watched_files.txt after the playback pipeline exits.--kast / -k switches playback from mpv to catt cast, always using the full resolved URL.LOUD=0, and invalid URL parsing is now routed through the normal loud/quiet behavior.-c assigned to chronological browsing and using -k for casting instead.curl errors are now suppressed when LOUD=0, so quiet mode stays quiet during refresh failures.ytcs updates local watched markers inline in grouped_data.txt and time_data.txt when those cache files exist.grep scans of watched_files.txt, which reduces formatting overhead on larger lists.parsed_time/ and thumbnails/, so channel browsing should not emit stray grep: ... Is a directory warnings.This script is for personal use. Make sure your usage complies with YouTube’s terms and with the wishes of the creators whose videos you are watching.
MIT