Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Docs

on:
push:
branches: [main]
pull_request:
branches: [main]
paths:
- docs/**
- mkdocs.yml
- scripts/build_docs_reference.py
- src/ytstudio/** # CLI surface feeds the auto-generated reference
- pyproject.toml
- uv.lock
- .github/workflows/docs.yml
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

# Only one Pages deploy at a time; never cancel a running deploy.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Set up Python
run: uv python install 3.12

- name: Install docs dependencies
run: uv sync --locked --group docs

- name: Build (strict)
run: uv run mkdocs build --strict

- name: Upload Pages artifact
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v5
with:
path: site

deploy:
needs: build
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ htmlcov/
.vscode/
.DS_Store
demo/

# Docs build artefacts and generated content
site/
docs/reference.md
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
args: ["--unsafe"]
- id: check-added-large-files
- id: check-merge-conflict

Expand Down
183 changes: 31 additions & 152 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,169 +1,41 @@
# YT Studio CLI
<h1 align="center">YT Studio CLI</h1>

[![CI](https://github.com/jdwit/ytstudio/actions/workflows/ci.yml/badge.svg)](https://github.com/jdwit/ytstudio/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/ytstudio-cli)](https://pypi.org/project/ytstudio-cli/)
[![Python](https://img.shields.io/pypi/pyversions/ytstudio-cli)](https://pypi.org/project/ytstudio-cli/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
<p align="center">
<img src="ascii.png" alt="ascii">
</p>

Manage and analyze your YouTube channel from the terminal. Ideal for automation and AI workflows.
<p align="center">
<a href="https://github.com/jdwit/ytstudio/actions/workflows/ci.yml"><img src="https://github.com/jdwit/ytstudio/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="https://pypi.org/project/ytstudio-cli/"><img src="https://img.shields.io/pypi/v/ytstudio-cli" alt="PyPI"></a>
<a href="https://pypi.org/project/ytstudio-cli/"><img src="https://img.shields.io/pypi/pyversions/ytstudio-cli" alt="Python"></a>
</p>

![demo](demo.gif)
<p align="center">
Manage and analyze your YouTube channel from the terminal. Ideal for automation and AI workflows.
</p>

## Motivation

I built this because I needed to bulk update video titles for a YouTube channel I manage with 300+
videos. YouTube Studio does not support bulk search-replace operations, which made it a tedious
manual process. This tool uses the YouTube Data API to perform bulk operations on video metadata.
Furthermore, it provides features for analytics and comment moderation, all accesible from the
Furthermore, it provides features for analytics and comment moderation, all accessible from the
command line.

## Installation
## Features

I recommend the excellent [uv](https://uv.io/) tool for installation:
- Bulk update video metadata across hundreds of videos in one pass (search-replace titles,
descriptions, tags).
- Upload videos from a directory using YAML sidecars for metadata and thumbnails.
- Schedule, start, stop, and update YouTube livestream broadcasts, including RTMP ingest details.
- Multi-channel profiles: manage several channels from one machine and switch per command.
- Comments moderation: list, reply, and moderate from the CLI.
- Channel analytics queries via the YouTube Analytics API.

```bash
uv tool install ytstudio-cli
```

The CLI is installed as `ytstudio`; `yts` is a short alias for the same entry point.

## Setup

1. Create a [Google Cloud project](https://console.cloud.google.com/)
1. Enable **YouTube Data API v3** and **YouTube Analytics API**
1. Configure OAuth consent screen:
- Go to **APIs & Services** → **OAuth consent screen**
- Select **External** and create
- Fill in app name and your email
- Skip scopes, then add yourself as a test user
- Leave the app in "Testing" mode (no verification needed)
1. Create OAuth credentials:
- Go to **APIs & Services** → **Credentials**
- Click **Create Credentials** → **OAuth client ID**how
- Select **Desktop app** as application type
- Download the JSON file
1. Configure ytstudio:

```bash
ytstudio init --client-secrets path/to/client_secret_<id>.json
ytstudio login
```

Credentials stored in `~/.config/ytstudio-cli/`.

### Headless Linux login

If you are logging in from a server without a browser, run:

```bash
ytstudio login --headless
```

The command prints a Google OAuth URL. Open that URL in a browser on any machine and approve
access. The browser will then fail to load a `127.0.0.1` page; this is expected. Copy the full URL
from the browser address bar, paste it back into the terminal, and ytstudio will finish the login.

## Managing multiple channels

If you run more than one YouTube channel, ytstudio stores each login under its own named profile.
Every command (`videos`, `analytics`, `comments`, ...) operates on the active channel.

```bash
ytstudio profile add work # authenticate a new channel and make it active
ytstudio profile add personal # add another
ytstudio profile list # show profiles; the active one is marked with *
ytstudio profile use work # switch the active profile
ytstudio profile status work # auth status for one profile (defaults to active)
ytstudio profile remove personal
```

For scripting you can override the active channel per command without switching:

```bash
YTSTUDIO_PROFILE=work ytstudio videos list
```

Credentials live in `~/.config/ytstudio-cli/profiles/<name>/` with owner-only (`600`) permissions.
The shared OAuth client secrets stay at the top level, so `ytstudio init` is only needed once.
A plain `ytstudio login` (no `profile add`) authenticates the active profile, which is `default`
on a fresh setup. Existing single-channel installs are migrated to the `default` profile
automatically on first run.

## Live broadcasts

Manage your YouTube livestream broadcasts from the CLI. This drives YouTube's
`liveBroadcasts` API (scheduling, starting, stopping, updating); the actual
video push to YouTube still goes through your encoder of choice (OBS, etc.).
## Documentation

```bash
ytstudio livestreams list --status upcoming # what is scheduled
ytstudio livestreams show <broadcast-id> # details for one broadcast
ytstudio livestreams show <id> --ingest # also fetch the bound stream's RTMP URL
ytstudio livestreams show <id> --show-key # reveal the stream key (treat as secret)
ytstudio livestreams schedule \
--title "Title" --scheduled-start 2026-06-01T19:00:00+02:00 # dry-run
ytstudio livestreams schedule ... --execute # actually create
ytstudio livestreams start <id> # transition to live
ytstudio livestreams start <id> --to testing # transition to the monitor stream first
ytstudio livestreams stop <id> # transition to complete
ytstudio livestreams update <id> --privacy unlisted --no-dvr # dry-run by default
```

Notes:

- `schedule` and `update` default to a dry-run preview; pass `--execute` to apply.
- `--show-key` on `show` reveals the bound stream key; without it the key is
redacted. Stream keys are credentials.
- `liveBroadcasts.insert`/`update` are write operations (~50 quota units each);
see [API quota](#api-quota) below.

## Uploading videos

Bulk-upload videos from a directory. Each video file needs a YAML sidecar with
the same basename describing its metadata.

```bash
yts videos upload ./outbox # dry-run preview
yts videos upload ./outbox --execute # actually upload
yts videos upload ./outbox --execute --max 3 # cap to N uploads (quota budget)
```

Directory layout:

```
outbox/
holiday.mp4
holiday.yaml # required
holiday.jpg # optional thumbnail (or .png), max 2 MB
```

Sidecar (`holiday.yaml`):

```yaml
title: Holiday recap 2026
description: |
Multi-line description.
privacy: private # private | unlisted | public
publish_at: 2026-06-01T10:00:00+02:00 # optional; forces privacy=private
tags: [travel, vlog]
category_id: "22" # YouTube category id; default 22 (People & Blogs)
default_language: nl
default_audio_language: nl
made_for_kids: false
```

After a successful upload the tool patches the sidecar with the resulting
`video_id` and `uploaded_at`, so re-running on the same directory is safe; only
sidecars without a `video_id` get uploaded.

Quota: `videos.insert` costs ~1600 quota units, so on the default 10k/day
quota you can upload ~6 videos per day. Use `--max` to cap a run explicitly.

Show what's scheduled for the future:

```bash
yts videos list --scheduled
```
Full documentation, including installation, OAuth setup, and per-command reference, lives at
[jdwit.github.io/ytstudio-cli](https://jdwit.github.io/ytstudio-cli/).

## Development

Expand All @@ -177,6 +49,13 @@ uv run pre-commit install

Run the suite manually with `uv run pytest` and `uv run pre-commit run --all-files`.

To preview the docs site locally, install the `docs` group and serve with mkdocs:

```bash
uv sync --group docs
uv run mkdocs serve # or: uv run mkdocs build
```

## API quota

The YouTube Data API enforces a default quota of 10_000 units per project per day. Most read
Expand Down
Binary file added ascii.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions docs/analytics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Analytics

`ytstudio analytics` queries the YouTube Analytics API: channel-wide totals,
per-video performance, and ad-hoc queries against the full metric and
dimension catalogue. Output defaults to a Rich table; pass `-o json` for
scripting.

## Common queries

```bash
ytstudio analytics overview # channel summary for the last 28 days
ytstudio analytics overview --days 7 # rolling 7-day window
ytstudio analytics video <video-id> # one video's recent performance
ytstudio analytics video <video-id> --days 90 # longer window for one video
```

## Custom queries

`analytics query` is a thin wrapper over the YouTube Analytics
`reports.query` endpoint, so anything you can express against that API is
reachable from the CLI.

```bash
ytstudio analytics query \
-m views,estimatedMinutesWatched,averageViewDuration \
-d day \
--start 2026-04-01 --end 2026-04-30

ytstudio analytics query \
-m views -d country \
--sort -views -n 10
```

Sort fields take a `-` prefix for descending. Use `-f key==value` (repeatable)
to filter, for example `-f video==dMH0bHeiRNg`.

Discover what is available:

```bash
ytstudio analytics metrics # list every metric the API exposes
ytstudio analytics dimensions # list every dimension the API exposes
```

Run `ytstudio analytics --help` (or open the
[Command reference](reference.md)) for every flag.

## Output

=== "Table"

```bash
ytstudio analytics overview
```

=== "JSON"

```bash
ytstudio analytics overview -o json | jq '.rows[0]'
```

## Quota

Analytics reads are cheap (1 unit per call against the Data API quota); you
can hit them often without worrying. See [API quota](api-quota.md) for the
full picture.
Loading