🇬🇧 English · 🇨🇿 Čeština · 🇪🇸 Español · 🇧🇷 Português · 🇩🇪 Deutsch · 🇫🇷 Français · 🇨🇳 简体中文 · 🇯🇵 日本語 · 🇸🇪 Svenska · 🇳🇴 Norsk · 🇩🇰 Dansk
A Python command-line tool for managing videos on YouTube through the YouTube Data API v3 and for reading channel performance through the YouTube Analytics API v2. It's for anyone who publishes on YouTube regularly and is tired of clicking through YouTube Studio. Uploads, metadata, thumbnails, playlists, stats, watch time and retention, all from the terminal. Built so that Claude Code or another AI agent can drive it.
Author: Jindřich Fáborský
v2.0.0 · Changelog · API notes
Tip
The app is free and it's yours. Clone it, use it, rebuild it your way.
Want to build tools like this for your own marketing? That's what I teach: faborsky.com.
📊 Views are counted differently since August 24, 2026. YouTube now counts a view as soon as playback starts, with no minimum watch time, for all formats (Shorts have worked this way since March 31, 2025). The old definition is gone from the public Data API and survives as engaged views in the Analytics API. This CLI shows both, see View counting.
Publishing a video on YouTube involves a lot of repetitive manual work: upload the file through the web interface, fill in the title and description, set privacy, add a thumbnail, put it in a playlist. This CLI automates those steps so you can script them or chain them with other tools.
Here's what a podcast release driven by Claude Code can look like:
- Transcribe the audio with whisper-cz-en
- Generate metadata (title, description, chapters) from the transcript
- Upload the audio to Podbean with podbean-app
- Upload the video to YouTube with this CLI
- Set the thumbnail and add it to a playlist, automatically
All in one conversation.
- Python 3.9+
- A YouTube channel linked to a Google account
- A Google Cloud project with the YouTube Data API v3 enabled and OAuth 2.0 credentials
- For the
analyticscommand: the YouTube Analytics API enabled in the same project
Before using the CLI, you need a Google Cloud project and OAuth credentials. You only do this once.
- Go to the Google Cloud Console
- Click the project dropdown (top bar) → New Project
- Name it (e.g.
youtube-cli) → Create - Select the new project in the dropdown
- Go to APIs & Services → Library (direct link)
- Search for YouTube Data API v3 → click it → Enable
- Search for YouTube Analytics API → click it → Enable (needed for the
analyticscommand and forstats --engaged; skip it if you only publish videos)
- Go to APIs & Services → OAuth consent screen
- Choose the External user type → Create
- Fill in the required fields:
- App name: any name (e.g.
Video Manager CLI) - User support email: your email
- Developer contact: your email
- App name: any name (e.g.
- Click Save and Continue
- On the Scopes page → Add or Remove Scopes → add
https://www.googleapis.com/auth/youtubeandhttps://www.googleapis.com/auth/yt-analytics.readonly→ Save and Continue - On the Test users page → Add Users → add the Gmail address that owns your channel → Save and Continue
Watch out for token expiry: In "Testing" mode, OAuth refresh tokens expire after 7 days. To get long-lived tokens, click Publish App on the OAuth consent screen page. For personal-use apps where only your own account signs in, Google usually doesn't require any further verification.
- Go to APIs & Services → Credentials (direct link)
- Click Create Credentials → OAuth client ID
- Application type: Desktop app
- Name: anything (e.g.
YouTube CLI) - Click Create
- Copy the Client ID and Client Secret
The Client ID looks like 123456789-abc.apps.googleusercontent.com; the Client Secret is a short alphanumeric string.
git clone https://github.com/faborsky/youtube-cli-app.git
cd youtube-cli-app
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# Edit .env and add your Google Cloud OAuth credentialsPut the OAuth credentials from the Google Cloud setup into .env:
YOUTUBE_CLIENT_ID=your-client-id-here
YOUTUBE_CLIENT_SECRET=your-client-secret-here
Then sign in once (a browser window opens):
python youtube_cli.py authThis saves a long-lived refresh token to ~/.youtube-cli-token.json. All other commands pick it up automatically, no browser needed.
Always activate the virtual environment first:
source venv/bin/activatepython youtube_cli.py videos
python youtube_cli.py videos --limit 10 --jsonpython youtube_cli.py video-get --video-id VIDEO_ID
python youtube_cli.py video-get --video-id VIDEO_ID --jsonVideos are uploaded as private by default, to be safe.
# Basic upload (private)
python youtube_cli.py upload \
--file /path/to/video.mp4 \
--title "My Video Title"
# Upload with all metadata
python youtube_cli.py upload \
--file /path/to/video.mp4 \
--title "My Video Title" \
--description "Video description with details" \
--tags "tag1,tag2,tag3" \
--category 22 \
--privacy private \
--thumbnail /path/to/thumbnail.jpg \
--playlist PLAYLIST_ID \
--language en \
--json
# Scheduled publishing
python youtube_cli.py upload \
--file /path/to/video.mp4 \
--title "Scheduled Video" \
--publish-at "2026-04-10T15:00:00Z"Uploads use resumable transfer in chunks of about 12.5 MB and retry automatically with exponential backoff on server errors.
Only the fields you pass are changed; everything else stays as it was:
python youtube_cli.py video-update --video-id VIDEO_ID --title "New Title"
python youtube_cli.py video-update --video-id VIDEO_ID --privacy public
python youtube_cli.py video-update --video-id VIDEO_ID --thumbnail /path/to/new-thumb.jpg
python youtube_cli.py video-update --video-id VIDEO_ID \
--title "Updated" \
--description "New description" \
--tags "new,tags" \
--jsonRequires the --confirm flag as a safety measure:
python youtube_cli.py video-delete --video-id VIDEO_ID --confirm# Channel overview
python youtube_cli.py stats
# Stats for a specific video
python youtube_cli.py stats --video-id VIDEO_ID
python youtube_cli.py stats --video-id VIDEO_ID --json
# Add lifetime engaged views (the view definition used until August 24, 2026)
python youtube_cli.py stats --engaged
python youtube_cli.py stats --video-id VIDEO_ID --engagedWatch time, retention and traffic sources via the YouTube Analytics API, for your own channel only.
Requires the YouTube Analytics API to be enabled and the yt-analytics.readonly scope (if you
granted access before enabling the API, run auth again).
# Channel summary for the last 28 days
python youtube_cli.py analytics --days 28
# Daily trend
python youtube_cli.py analytics --days 28 --dimensions day --sort day
# Most-watched videos
python youtube_cli.py analytics --dimensions video --sort -views --max-results 10
# Where views come from
python youtube_cli.py analytics --dimensions insightTrafficSourceType --sort -views
# Shorts vs. long-form
python youtube_cli.py analytics --days 90 --dimensions creatorContentType
# One video, a specific date range, raw JSON from the API
python youtube_cli.py analytics --video-id VIDEO_ID \
--start-date 2026-07-01 --end-date 2026-07-31 --jsonDefault metrics: views, engagedViews, estimatedMinutesWatched, averageViewDuration,
averageViewPercentage (retention), subscribersGained, subscribersLost, likes, comments,
shares. Change them with --metrics (full list).
The Analytics API only serves fixed combinations of dimensions and metrics, so the CLI fills in what you leave out: for dimensions that only support view metrics (traffic source, device, subscription status…) it narrows the default metric set, for top-N dimensions (video, playlist and others) it adds the required sort and result limit, and for --dimensions month it aligns dates to whole months. Every adjustment is printed to stderr, so --json output stays clean. Anything you set yourself takes precedence.
On August 24, 2026, YouTube unified view counting across all formats: a view counts as soon as playback starts, with no minimum watch time. Shorts have worked this way since March 31, 2025.
| Metric | Where | What it counts |
|---|---|---|
viewCount |
Data API (stats, videos, video-get) |
New definition, every playback start |
views |
Analytics API (analytics) |
New definition |
engagedViews |
Analytics API (analytics, stats --engaged) |
Old definition, views past the first few seconds |
The old number is no longer available anywhere in the public Data API, and only the channel owner can read engagedViews.
The jump in views around August 24, 2026 comes from the definition change. To compare across that
date, use engagedViews. More detail in docs/api-notes.md.
python youtube_cli.py playlists
python youtube_cli.py playlists --jsonEvery command supports --json for machine-readable output, useful for scripting and for AI agents. In JSON mode only the data goes to stdout; notes and warnings for humans go to stderr:
python youtube_cli.py videos --json
python youtube_cli.py analytics --days 28 --json # the API's resultTable, verbatim| Command | What it does | Main flags |
|---|---|---|
auth |
One-time OAuth setup (opens a browser) | none |
videos |
List recent videos | --limit, --json |
video-get |
Video details | --video-id (required), --json |
upload |
Upload a video with metadata | --file, --title (required), --description, --tags, --category, --privacy, --thumbnail, --playlist, --language, --publish-at, --json |
video-update |
Update an existing video | --video-id (required), --title, --description, --tags, --category, --privacy, --thumbnail, --json |
video-delete |
Delete a video | --video-id (required), --confirm (required) |
stats |
View statistics | --video-id (optional), --engaged, --json |
analytics |
Watch time, retention, traffic sources (Analytics API) | --days, --start-date, --end-date, --metrics, --dimensions, --sort, --video-id, --max-results, --json |
playlists |
List playlists | --json |
python youtube_cli.py --version prints the installed version.
| Flag | Required | Default | What it does |
|---|---|---|---|
--file |
yes | none | Path to the video file (MP4, MOV and others) |
--title |
yes | none | Video title |
--description |
no | empty | Video description |
--tags |
no | none | Comma-separated tags |
--category |
no | 22 |
YouTube category ID (22 = People & Blogs) |
--privacy |
no | private |
private, unlisted or public |
--thumbnail |
no | none | Path to a thumbnail image (JPG/PNG) |
--playlist |
no | none | ID of the playlist to add the video to |
--language |
no | none | Language code (e.g. en, cs, de) |
--publish-at |
no | none | Scheduled publishing (ISO 8601, forces privacy to private) |
- Private by default. Uploaded videos are
private. You make them public yourself, either with--privacy publicor later withvideo-update. - Deleting requires confirmation. The
--confirmflag is mandatory forvideo-delete. - Credentials stay with you.
.envis gitignored, and the OAuth token lives in your home directory (~/.youtube-cli-token.json). - No keys in the code. All credentials come from environment variables.
"Not authenticated" error:
Run python youtube_cli.py auth and complete the OAuth sign-in.
"Token has been expired or revoked" error:
Your refresh token expired (in "Testing" mode this happens after 7 days). Run auth again. To prevent it, publish the app in the Google Cloud Console.
"YouTube API quota exceeded" error: The default quota is 10,000 units per day, and one video upload costs about 1,600 units. Check usage in Google Cloud Quotas.
"Google hasn't verified this app" warning: Normal for personal-use apps. Click Continue.
Thumbnail upload fails: Custom thumbnails require a verified YouTube account (phone verification).
analytics reports a scope or permission error:
Enable the YouTube Analytics API in your Google Cloud project, then run python youtube_cli.py auth once. An existing token won't pick up the new scope on its own. Analytics only works for channels you own.
Views suddenly jumped:
It may not be growth. Since August 24, 2026, a view counts from the start of playback, with no minimum watch time. To compare like with like, look at engagedViews (the analytics command or stats --engaged).
youtube_cli.py # Main CLI (single file)
.env.example # Template for OAuth credentials
.env # Your credentials (gitignored)
requirements.txt # Python dependencies
CLAUDE.md # Instructions for Claude Code
CHANGELOG.md # Version history (Keep a Changelog, SemVer)
docs/api-notes.md # How the YouTube APIs actually behave
This CLI is one link in a content pipeline driven by AI:
| Step | Tool | What it does |
|---|---|---|
| Transcription | whisper-cz-en | Local transcription with Whisper Large V3 |
| Episode notes | Claude Code | Generates titles, descriptions, chapters |
| Cover | nanobanana-app | Generates artwork with AI |
| Audio publishing | podbean-app | Uploads podcast audio |
| Video publishing | youtube-cli-app (this repo) | Uploads videos to YouTube |
MIT, see LICENSE.
Jindřich Fáborský, marketer and educator from Prague. I teach marketers to build their own tools with AI.
YouTube CLI started as part of my content automation toolkit, and I use it in production to run a Czech podcast's YouTube channel.