Youtube publisher skill

Upload videos to YouTube (single video or a numbered series), set titles + descriptions + custom thumbnails, build a playlist, and order it.

by timscheuerai·MIT license·★ 37 Stars on the repo·GitHub ↗

Use now

Inside this skill

SKILL.md · the author's own words
# youtube-publisher

The YouTube publishing pipeline. Takes raw video files on disk and ends
with a fully-published series: titles in the right scheme, descriptions
with the right CTA, custom on-brand thumbnails on every video, and a
properly-ordered playlist. Idempotent state files make every step
re-runnable so future series ship in minutes, not hours.

---

## SETUP (run this once)

YouTube publishing needs your own Google Cloud project + OAuth. There is
no shared key — this talks to *your* channel.

1. **Google Cloud project** → enable **YouTube Data API v3**.

Word-for-word excerpt, not a result. Read the full SKILL.md ↓

Files of Youtube publisher

timscheuerai/main1 file shown
SKILL.md
Show the full text310 lines

youtube-publisher

The YouTube publishing pipeline. Takes raw video files on disk and ends with a fully-published series: titles in the right scheme, descriptions with the right CTA, custom on-brand thumbnails on every video, and a properly-ordered playlist. Idempotent state files make every step re-runnable so future series ship in minutes, not hours.


SETUP (run this once)

YouTube publishing needs your own Google Cloud project + OAuth. There is no shared key — this talks to your channel.

  1. Google Cloud project → enable YouTube Data API v3.
  2. OAuth Desktop client → download client_secrets.json (keep it out of git — see .gitignore).
  3. Verify the channel at https://www.youtube.com/verify (phone + SMS). Required before custom thumbnails work, and before description links become clickable.
  4. Fill the placeholders used in the schemes below: <YOUR_BRAND>, <YOUR_DOMAIN>.
  5. The first real API call pops a browser for OAuth and caches the token locally (token.json, auto-refreshed, gitignored).

This skill documents the pipeline (upload → render thumbnails → finalize → playlist) using the YouTube Data API v3. The two scripts referenced below (upload.py, finalize.py) are small, resumable, idempotent wrappers — set them up once in <YOUR_YT_SCRIPTS>/ (or have the agent build them from this spec) and reuse them per series.


When to use

Trigger on:

  • "Upload these videos to YouTube"
  • "Publish the [series name] videos"
  • "Make a YT playlist for X"
  • "Set custom thumbnails on these videos"
  • "Update YT titles / descriptions"
  • "Redo the thumbnails"
  • User types /youtube-publisher

Skip for:

  • Editing raw footage → /video-use
  • Building motion-graphics launch videos → /launch-video
  • Writing the YouTube script itself → /youtube-script
  • Designing the thumbnail art → /youtube-thumbnail (interview) or /graphics-designer (tutorial / solo). This skill sets the thumbnail; those make it.

Pipeline shape

<YOUR_YT_SCRIPTS>/
├── upload.py            # resumable uploads, OAuth flow, idempotent (uploaded.json)
├── finalize.py          # thumbnails + titles + descriptions + playlist (finalize-state.json)
├── metadata.json        # per-video title/description/tags for the UPLOAD step
├── client_secrets.json  # OAuth Desktop client (gitignored, one-time setup)
├── token.json           # cached OAuth token (gitignored, auto-refreshed)
├── uploaded.json        # idempotency: file → {video_id, video_url, ...}
└── finalize-state.json  # idempotency: {thumbnails, titles, descriptions, playlist_id, playlist_items}

Keep the upload-time metadata (per-video tags/description footers) and the finalize-time meta (thumbnail design fields) separate. They reference the same video_id after upload.

Source videos large enough to need Git LFS (*.mp4, *.mov) should be tracked before committing — GitHub rejects files >100 MB without it.


Definition of done · HARD GATE

A series is NOT done until all six are true:

  1. Source video files in place (Git LFS if >100 MB)
  2. Each video uploaded to YouTube, video_id captured in uploaded.json
  3. Each video's title set per the scheme below
  4. Each video's description set per the scheme below (CTA + playlist link for series videos)
  5. Each video has a custom thumbnail pushed
  6. The series playlist exists and contains all videos in the intended order

Do not declare "done" until you can paste the playlist URL and every video's URL with the new title + thumbnail visible in Studio.


Title scheme

Topic first, series second. Topic wins for click-through; series tag is identification. Use the middot separator (no em dash, no hyphen, no colon — brand rule).

Series video:
  CLI Install · <Series Name>
  MCP Server · <Series Name>
  Connections · <Series Name>
  ...

Standalone / non-series:
  <YOUR_BRAND> Demo · <one-line tagline>

For a standalone flagship video, the tagline can live in the title instead of a series tag.


Description scheme

Terse. CTA first. YouTube collapses descriptions after the first ~150 chars, so the link to <YOUR_DOMAIN> goes in the very first line.

Series videos:

Try <YOUR_BRAND> free → <YOUR_DOMAIN>

Full <Series> playlist → https://www.youtube.com/playlist?list=<playlist_id>

Standalone:

Try <YOUR_BRAND> free → <YOUR_DOMAIN>

That's it. Don't pad with chapter timestamps, social links, hashtag walls. If the user wants more (timestamps from /youtube-description, related links), add them explicitly — never default to bloat.

Gotcha: Links in descriptions render as plain text until Google finishes a one-time identity verification on the channel (~24h). After that they're clickable automatically. Nothing to script — just warn the user once.


Thumbnail

Set the thumbnail produced by /youtube-thumbnail (interview/podcast) or /graphics-designer (tutorial/solo). One unified design per series — geometry stays identical, only the per-video kicker + title swap. Render at 1280×720, push via youtube.thumbnails().set() in the finalize step.


Workflow

1. Stage the videos

Drop the cut files into your series folder, numbered for order (NN-topic.mov). Track large files with Git LFS first if you version them:

git lfs track "media/<series-slug>/*.mp4" "media/<series-slug>/*.mov"
git add .gitattributes

Files >100 MB will be rejected by GitHub without LFS, so set this up first.

2. Configure upload metadata

Edit metadata.json. A _defaults block (category, privacy, tags, descriptionFooter) applies to every video; the videos[] array carries per-video title/description/tags + the file path.

Default privacy: unlisted. Always. Public-flip happens after the user reviews the live videos in Studio.

3. Upload
python3 <YOUR_YT_SCRIPTS>/upload.py --dry-run --all   # preview
python3 <YOUR_YT_SCRIPTS>/upload.py --only 0          # one as a smoke test
python3 <YOUR_YT_SCRIPTS>/upload.py --all             # the rest

First real call pops a browser for OAuth on the channel-owner Google account. Token caches in token.json. Resumable uploads, 8 MB chunks, 500/502/503/504 auto-retry with a short sleep.

Quota: 1,600 units/upload × default 10k/day = 6 uploads/day. For 7+ videos either spread across a day boundary or request a quota increase via the YouTube API quota form (~1-3 weeks).

4. Render thumbnails

Produce them with /youtube-thumbnail or /graphics-designer. Required per video at finalize time: slug, video_id (from uploaded.json), kicker, title, yt_title. Sanity-check every PNG before pushing — the visual review here is the only thing between you and a bad thumbnail going live. Specifically check:

  • Long single-word titles fit the column
  • Kicker reads cleanly
  • Logo + wordmark visible, NOT cropped
  • The graphic is centered, not bleeding off
5. Finalize
python3 <YOUR_YT_SCRIPTS>/finalize.py --dry-run    # preview
python3 <YOUR_YT_SCRIPTS>/finalize.py              # ship

Does, in order:

  1. Thumbnails — youtube.thumbnails().set() per video
  2. Titles — youtube.videos().update() snippet
  3. Descriptions — youtube.videos().update() snippet
  4. Playlist — creates if missing, adds each video at position: idx

Flags:

  • --only-thumbnails / --only-titles / --only-descriptions / --only-playlist
  • --force — re-run actions already recorded in finalize-state.json
  • --delay-between N — seconds between thumbnail uploads (avoids rate limit)
  • --dry-run
6. Public flip

Last step the user controls. Either:

  • Studio → each video → Visibility → Public (manual review)
  • Or extend finalize.py with a --make-public flag that calls videos.update with status.privacyStatus: "public" per video

Default to the manual review — the user wants to confirm the actual live thumbnail and metadata before publishing.


Known gotchas (each one bites at least once)

Custom thumbnails require channel verification. First attempt returns 403 The authenticated user doesn't have permissions to upload and set custom video thumbnails. Fix: user goes to https://www.youtube.com/verify, enters phone + SMS code. ~2 min. After that, thumbnails work.

Thumbnail upload rate limit. YouTube caps thumbnails.set per channel in a rolling window. A burst of 4-5 back-to-back returns 429 The user has uploaded too many thumbnails recently. The state file preserves what succeeded; pass --delay-between 120 (2 min between uploads) when re-running. Window is opaque but often clears in ~1-2h, sometimes resets at midnight PT. Fallback when blocked: open Finder + the Studio edit tabs and drag-drop manually — Studio's upload path isn't subject to the same limit.

open -R output/<series>-00-<slug>.png
for vid in <id1> <id2> ...; do open "https://studio.youtube.com/video/$vid/edit"; done

Identity verification for clickable links. First time you put URLs in any description, Google triggers a one-time channel identity verification (~24h). Until done, links render as plain text. Nothing to script.

Daily upload quota. 10k units/day default → 6 uploads/day. 7+ videos crosses a day boundary. Have the script catch quota errors and break the loop with a clear message — resume tomorrow.

Position-0 truthy check. if state["playlist_items"].get(vid): treats a position-0 entry as not-in-playlist because 0 is falsy. Use if vid in state["playlist_items"]: instead.


Don'ts

  • Don't restart from scratch when state exists. uploaded.json and finalize-state.json make the whole pipeline idempotent. Re-running any step is a no-op for what's already done. Use --force only when re-pushing because the artifact actually changed.
  • Don't push thumbnails in a tight loop. The rate limit will bite. Use --delay-between 120 for batches; fall back to Studio drag-drop if the API is already cooling off.
  • Don't write rich descriptions. Terse CTA + playlist link is the whole brief. Timestamps, social links, hashtag walls without the user asking is the kind of bloat that reads as AI slop.
  • Don't auto-flip to public. Default is unlisted until the user manually reviews each video in Studio. Public flip is an explicit ask.
  • Don't change the thumbnail design without showing renders first. Iterate visually, get a thumbs-up, then ship.
  • Don't add an em dash anywhere. Brand rule. Title separator is the middot (·).
  • Don't bypass the channel verification step. Tell the user to verify at youtube.com/verify before the first thumbnail upload, not after the first 403.
  • Don't commit client_secrets.json or token.json. They're in .gitignore already; double-check before any git add -A.

See also

  • Thumbnail art: /youtube-thumbnail (interview) · /graphics-designer (tutorial/solo)
  • Description + chapters: /youtube-description
  • Script before filming: /youtube-script
  • Raw footage edit: /video-use
1---
2name: youtube-publisher
3description: Upload videos to YouTube (single video or a numbered series), set titles + descriptions + custom thumbnails, build a playlist, and order it. End-to-end pipeline using the YouTube Data API v3 with resumable uploads, OAuth token caching, and idempotent state. Use when asked to "upload to YouTube", "publish these videos", "make a YT series", "set up a YT playlist", "redo thumbnails", or "/youtube-publisher".
4---
5 
6# youtube-publisher
7 
8The YouTube publishing pipeline. Takes raw video files on disk and ends
9with a fully-published series: titles in the right scheme, descriptions
10with the right CTA, custom on-brand thumbnails on every video, and a
11properly-ordered playlist. Idempotent state files make every step
12re-runnable so future series ship in minutes, not hours.
13 
14---
15 
16## SETUP (run this once)
17 
18YouTube publishing needs your own Google Cloud project + OAuth. There is
19no shared key — this talks to *your* channel.
20 
211. **Google Cloud project** → enable **YouTube Data API v3**.
222. **OAuth Desktop client** → download `client_secrets.json` (keep it
23 out of git — see `.gitignore`).
243. **Verify the channel** at https://www.youtube.com/verify (phone +
25 SMS). Required before custom thumbnails work, and before description
26 links become clickable.
274. Fill the placeholders used in the schemes below: `<YOUR_BRAND>`,
28 `<YOUR_DOMAIN>`.
295. The first real API call pops a browser for OAuth and caches the token
30 locally (`token.json`, auto-refreshed, gitignored).
31 
32This skill documents the pipeline (upload → render thumbnails → finalize
33→ playlist) using the YouTube Data API v3. The two scripts referenced
34below (`upload.py`, `finalize.py`) are small, resumable, idempotent
35wrappers — set them up once in `<YOUR_YT_SCRIPTS>/` (or have the agent
36build them from this spec) and reuse them per series.
37 
38---
39 
40## When to use
41 
42Trigger on:
43- "Upload these videos to YouTube"
44- "Publish the [series name] videos"
45- "Make a YT playlist for X"
46- "Set custom thumbnails on these videos"
47- "Update YT titles / descriptions"
48- "Redo the thumbnails"
49- User types `/youtube-publisher`
50 
51Skip for:
52- Editing raw footage → `/video-use`
53- Building motion-graphics launch videos → `/launch-video`
54- Writing the YouTube script itself → `/youtube-script`
55- Designing the thumbnail art → `/youtube-thumbnail` (interview) or
56 `/graphics-designer` (tutorial / solo). This skill *sets* the
57 thumbnail; those *make* it.
58 
59---
60 
61## Pipeline shape
62 
63```
64<YOUR_YT_SCRIPTS>/
65├── upload.py # resumable uploads, OAuth flow, idempotent (uploaded.json)
66├── finalize.py # thumbnails + titles + descriptions + playlist (finalize-state.json)
67├── metadata.json # per-video title/description/tags for the UPLOAD step
68├── client_secrets.json # OAuth Desktop client (gitignored, one-time setup)
69├── token.json # cached OAuth token (gitignored, auto-refreshed)
70├── uploaded.json # idempotency: file → {video_id, video_url, ...}
71└── finalize-state.json # idempotency: {thumbnails, titles, descriptions, playlist_id, playlist_items}
72```
73 
74Keep the **upload-time** metadata (per-video tags/description footers)
75and the **finalize-time** meta (thumbnail design fields) separate. They
76reference the same `video_id` after upload.
77 
78Source videos large enough to need Git LFS (`*.mp4`, `*.mov`) should be
79tracked before committing — GitHub rejects files >100 MB without it.
80 
81---
82 
83## Definition of done · HARD GATE
84 
85A series is NOT done until **all six** are true:
86 
871. Source video files in place (Git LFS if >100 MB)
882. Each video uploaded to YouTube, `video_id` captured in `uploaded.json`
893. Each video's **title** set per the scheme below
904. Each video's **description** set per the scheme below (CTA + playlist link for series videos)
915. Each video has a custom **thumbnail** pushed
926. The series **playlist** exists and contains all videos in the intended order
93 
94Do not declare "done" until you can paste the playlist URL and every
95video's URL with the new title + thumbnail visible in Studio.
96 
97---
98 
99## Title scheme
100 
101**Topic first, series second.** Topic wins for click-through; series tag
102is identification. Use the middot separator (no em dash, no hyphen, no
103colon — brand rule).
104 
105```
106Series video:
107 CLI Install · <Series Name>
108 MCP Server · <Series Name>
109 Connections · <Series Name>
110 ...
111 
112Standalone / non-series:
113 <YOUR_BRAND> Demo · <one-line tagline>
114```
115 
116For a standalone flagship video, the tagline can live in the title
117instead of a series tag.
118 
119---
120 
121## Description scheme
122 
123**Terse. CTA first.** YouTube collapses descriptions after the first
124~150 chars, so the link to `<YOUR_DOMAIN>` goes in the very first line.
125 
126Series videos:
127```
128Try <YOUR_BRAND> free → <YOUR_DOMAIN>
129 
130Full <Series> playlist → https://www.youtube.com/playlist?list=<playlist_id>
131```
132 
133Standalone:
134```
135Try <YOUR_BRAND> free → <YOUR_DOMAIN>
136```
137 
138That's it. Don't pad with chapter timestamps, social links, hashtag
139walls. If the user wants more (timestamps from `/youtube-description`,
140related links), add them explicitly — never default to bloat.
141 
142**Gotcha:** Links in descriptions render as plain text until Google
143finishes a one-time **identity verification** on the channel (~24h).
144After that they're clickable automatically. Nothing to script — just
145warn the user once.
146 
147---
148 
149## Thumbnail
150 
151Set the thumbnail produced by `/youtube-thumbnail` (interview/podcast) or
152`/graphics-designer` (tutorial/solo). One unified design per series —
153geometry stays identical, only the per-video kicker + title swap. Render
154at 1280×720, push via `youtube.thumbnails().set()` in the finalize step.
155 
156---
157 
158## Workflow
159 
160### 1. Stage the videos
161 
162Drop the cut files into your series folder, numbered for order
163(`NN-topic.mov`). Track large files with Git LFS first if you version
164them:
165 
166```bash
167git lfs track "media/<series-slug>/*.mp4" "media/<series-slug>/*.mov"
168git add .gitattributes
169```
170 
171Files >100 MB **will** be rejected by GitHub without LFS, so set this up
172first.
173 
174### 2. Configure upload metadata
175 
176Edit `metadata.json`. A `_defaults` block (category, privacy, tags,
177descriptionFooter) applies to every video; the `videos[]` array carries
178per-video title/description/tags + the `file` path.
179 
180**Default privacy: `unlisted`.** Always. Public-flip happens after the
181user reviews the live videos in Studio.
182 
183### 3. Upload
184 
185```bash
186python3 <YOUR_YT_SCRIPTS>/upload.py --dry-run --all # preview
187python3 <YOUR_YT_SCRIPTS>/upload.py --only 0 # one as a smoke test
188python3 <YOUR_YT_SCRIPTS>/upload.py --all # the rest
189```
190 
191First real call pops a browser for OAuth on the channel-owner Google
192account. Token caches in `token.json`. Resumable uploads, 8 MB chunks,
193500/502/503/504 auto-retry with a short sleep.
194 
195**Quota:** 1,600 units/upload × default 10k/day = 6 uploads/day. For 7+
196videos either spread across a day boundary or request a quota increase
197via the YouTube API quota form (~1-3 weeks).
198 
199### 4. Render thumbnails
200 
201Produce them with `/youtube-thumbnail` or `/graphics-designer`. Required
202per video at finalize time: `slug`, `video_id` (from `uploaded.json`),
203`kicker`, `title`, `yt_title`. Sanity-check every PNG before pushing —
204the visual review here is the only thing between you and a bad thumbnail
205going live. Specifically check:
206- Long single-word titles fit the column
207- Kicker reads cleanly
208- Logo + wordmark visible, NOT cropped
209- The graphic is centered, not bleeding off
210 
211### 5. Finalize
212 
213```bash
214python3 <YOUR_YT_SCRIPTS>/finalize.py --dry-run # preview
215python3 <YOUR_YT_SCRIPTS>/finalize.py # ship
216```
217 
218Does, in order:
2191. **Thumbnails** — `youtube.thumbnails().set()` per video
2202. **Titles** — `youtube.videos().update()` snippet
2213. **Descriptions** — `youtube.videos().update()` snippet
2224. **Playlist** — creates if missing, adds each video at `position: idx`
223 
224Flags:
225- `--only-thumbnails` / `--only-titles` / `--only-descriptions` / `--only-playlist`
226- `--force` — re-run actions already recorded in `finalize-state.json`
227- `--delay-between N` — seconds between thumbnail uploads (avoids rate limit)
228- `--dry-run`
229 
230### 6. Public flip
231 
232Last step the user controls. Either:
233- Studio → each video → Visibility → Public (manual review)
234- Or extend `finalize.py` with a `--make-public` flag that calls
235 `videos.update` with `status.privacyStatus: "public"` per video
236 
237Default to the manual review — the user wants to confirm the actual live
238thumbnail and metadata before publishing.
239 
240---
241 
242## Known gotchas (each one bites at least once)
243 
244**Custom thumbnails require channel verification.** First attempt returns
245`403 The authenticated user doesn't have permissions to upload and set
246custom video thumbnails.` Fix: user goes to
247https://www.youtube.com/verify, enters phone + SMS code. ~2 min. After
248that, thumbnails work.
249 
250**Thumbnail upload rate limit.** YouTube caps `thumbnails.set` per
251channel in a rolling window. A burst of 4-5 back-to-back returns `429 The
252user has uploaded too many thumbnails recently`. The state file preserves
253what succeeded; pass `--delay-between 120` (2 min between uploads) when
254re-running. Window is opaque but often clears in ~1-2h, sometimes resets
255at midnight PT. **Fallback when blocked:** open Finder + the Studio edit
256tabs and drag-drop manually — Studio's upload path isn't subject to the
257same limit.
258 
259```bash
260open -R output/<series>-00-<slug>.png
261for vid in <id1> <id2> ...; do open "https://studio.youtube.com/video/$vid/edit"; done
262```
263 
264**Identity verification for clickable links.** First time you put URLs in
265any description, Google triggers a one-time channel identity verification
266(~24h). Until done, links render as plain text. Nothing to script.
267 
268**Daily upload quota.** 10k units/day default → 6 uploads/day. 7+ videos
269crosses a day boundary. Have the script catch quota errors and break the
270loop with a clear message — resume tomorrow.
271 
272**Position-0 truthy check.** `if state["playlist_items"].get(vid):`
273treats a position-0 entry as not-in-playlist because `0` is falsy. Use
274`if vid in state["playlist_items"]:` instead.
275 
276---
277 
278## Don'ts
279 
280- **Don't restart from scratch when state exists.** `uploaded.json` and
281 `finalize-state.json` make the whole pipeline idempotent. Re-running
282 any step is a no-op for what's already done. Use `--force` only when
283 re-pushing because the artifact actually changed.
284- **Don't push thumbnails in a tight loop.** The rate limit will bite.
285 Use `--delay-between 120` for batches; fall back to Studio drag-drop if
286 the API is already cooling off.
287- **Don't write rich descriptions.** Terse CTA + playlist link is the
288 whole brief. Timestamps, social links, hashtag walls without the user
289 asking is the kind of bloat that reads as AI slop.
290- **Don't auto-flip to public.** Default is `unlisted` until the user
291 manually reviews each video in Studio. Public flip is an explicit ask.
292- **Don't change the thumbnail design without showing renders first.**
293 Iterate visually, get a thumbs-up, then ship.
294- **Don't add an em dash anywhere.** Brand rule. Title separator is the
295 middot (`·`).
296- **Don't bypass the channel verification step.** Tell the user to verify
297 at youtube.com/verify *before* the first thumbnail upload, not after
298 the first 403.
299- **Don't commit `client_secrets.json` or `token.json`.** They're in
300 `.gitignore` already; double-check before any `git add -A`.
301 
302---
303 
304## See also
305 
306- Thumbnail art: `/youtube-thumbnail` (interview) · `/graphics-designer` (tutorial/solo)
307- Description + chapters: `/youtube-description`
308- Script before filming: `/youtube-script`
309- Raw footage edit: `/video-use`
310 

Discussion

Alternatives

API and interface designGuides stable API and interface design. Use when designing APIs, module boundaries, or any public interface. Use when creating REST or GraphQL endpoints, defining type contracts between modules, or establishing boundaries between frontend and backend.Coding · MITContext7Pulls up-to-date, version-specific library docs and code examples into the prompt so the AI stops inventing old APIs.Coding · MITContext7 Documentation LookupFetch up-to-date documentation and code examples for any library, framework, SDK, CLI tool, or cloud service. Use whenever the user asks about a specific library — even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot — because training data may not reflect recent API changes or version updates. Always use for: API syntax questions, configuration options, version migration issues, "how do I" questions mentioning a library name, debugging that involves library-specific behavior, setup instructions, and CLI tool usage. Use even when you think you know the answer. Do not rely on training data for API details, signatures, or configuration options — they are frequently out of date. Prefer this over web search for library documentation.Coding · MITAdaptyv Bio Foundry APIHow to use the Adaptyv Bio Foundry API and Python SDK for protein experiment design, submission, and results retrieval. Use this skill whenever the user mentions Adaptyv, Foundry API, protein binding assays, protein screening experiments, BLI/SPR assays, thermostability assays, or wants to submit protein sequences for experimental characterization. Also trigger when code imports `adaptyv`, `adaptyv_sdk`, or `FoundryClient`, or references `foundry-api-public.adaptyvbio.com`.Science · MIT