youtube-scrape
Scrape a YouTube channel or video — fetches all video metadata via YouTube Data API, downloads transcripts with yt-dlp, chunks for search, and upserts into the videos table. Use when scraping a channe
Files1
View on GitHubSKILL.mdcursor/youtube-scrape/SKILL.md
GitHubScrape YouTube videos: $ARGUMENTS
$ARGUMENTS should be one of:
- A YouTube channel handle (e.g.
@AlanHirsch) or channel ID (e.g.UCxVxcTULO9cFU6SB9qVaisQ) --video VIDEO_IDto scrape a single video--refreshto re-fetch stats for all existing videos without re-downloading transcripts- Empty — ask the user for the channel handle
Before Starting
- Confirm
YOUTUBE_API_KEYis set in.env.local— if not, tell the user to add it - Confirm
yt-dlpis installed (which yt-dlp) — if not, tell the user to install it (pip install yt-dlp) - Read
{{SCHEMA_PATH}}for thevideos,videoSeriestable definitions - Read
{{SCRIPTS_DIR}}/youtube-scrape.tsto understand the existing scrape script
Pipeline Stages
Stage 1 — Fetch All Videos from Channel
- Resolve channel ID: If given a handle (
@Name), call the YouTube Data API v3channels?part=id&forHandle=@Nameto get theUC...channel ID - Derive uploads playlist: Replace
UCprefix withUUto get the uploads playlist ID - Paginate playlist: Call
playlistItems.listwithmaxResults=50, followingnextPageTokenuntil exhausted - Enrich with details: Call
videos.listin batches of 50 (part=snippet,contentDetails,statistics) to get full metadata - Delta check: Compare fetched
video_idlist against existingexternal_idvalues in thevideostable — only download transcripts for new videos
Stage 2 — Download & Parse Transcripts
For each new video (not already in DB with a transcript):
- Run
yt-dlp --write-auto-subs --sub-lang en --skip-download -o "data/vtt/%(id)s" "https://youtube.com/watch?v=VIDEO_ID"with a 60-second timeout - Parse the resulting
.en.vttfile:- Strip VTT headers, cue IDs, positioning metadata
- Deduplicate repeated lines (auto-subs repeat heavily)
- Extract
{ start, end, text }segments - Concatenate into
fullText
- If no VTT file produced, set transcript to
nulland continue
Stage 3 — Chunk Transcripts for Search
Split each transcript into chunks for embedding/search:
- Target: 500 words per chunk
- Overlap: 75 words
- Stride: 425 words (500 - 75)
- Each chunk gets:
chunk_index,text,start_time,end_time,timestamp_url(deep-link to that point in the video) - Absorb tiny final chunks (< 75 words) into the previous chunk
Store chunks in a JSON field or separate table as appropriate.
Stage 4 — Fetch Comments (Optional)
Call commentThreads.list with maxResults=100&order=relevance, paginating with nextPageToken:
- Handle 403 gracefully (comments disabled) — return
[] - Extract:
comment_id,author,text,like_count,published_at - Per-video errors should not crash the pipeline
Database Mapping
Map YouTube fields to the existing videos table:
| YouTube Field | DB Column |
|---|---|
item.id |
external_id |
item.snippet.title |
title |
item.snippet.title (slugified) |
slug |
item.snippet.description |
description |
https://youtube.com/watch?v={id} |
video_url |
item.snippet.thumbnails.maxres?.url |
thumbnail_url |
item.contentDetails.duration (parsed) |
duration_seconds |
item.statistics.viewCount |
view_count |
item.statistics.likeCount |
like_count |
item.statistics.commentCount |
comment_count |
item.snippet.tags |
tags (jsonb) |
item.snippet.publishedAt |
published_at |
| Parsed transcript fullText | transcript |
'youtube' |
hosting_provider |
<iframe> embed string |
embed_code |
'published' |
status |
Key Design Rules
- Idempotent — Use upserts keyed on
external_id. Safe to re-run. - Resume-safe — Skip transcript download if VTT file already exists in
data/vtt/ - Incremental — Only download transcripts for new videos. Always refresh stats.
- Graceful degradation — 403 returns
[]; per-video errors don't crash the pipeline - All timestamps UTC — ISO 8601 throughout
- Batch operations — 50 per YouTube API call; small delays between yt-dlp downloads
Running
pnpm youtube:scrape -- --channel @AlanHirsch
pnpm youtube:scrape -- --video dQw4w9WgXcQ
pnpm youtube:scrape -- --refresh
Output Format
## YouTube Scrape Report
### Channel: [name] ([handle])
### Videos found: N total, M new
### Stage 1 — Metadata: OK
- Fetched: N videos
- New (not in DB): M
### Stage 2 — Transcripts: OK
- Downloaded: X
- Skipped (already exists): Y
- Failed (no subs): Z
### Stage 3 — Chunks: OK
- Total chunks: N across M videos
### Stage 4 — Comments: OK / SKIPPED
- Fetched: N comments across M videos
### Database:
- Upserted: N videos
- Stats refreshed: M videos
### Warnings:
- [any non-blocking issues]
Quota Notes
- YouTube Data API v3 free tier: 10,000 units/day
playlistItems.list= 1 unit,videos.list= 1 unit,commentThreads.list= 1 unit- A channel with 100 videos uses ~104 units per full run
- yt-dlp has no official rate limit but add small delays between downloads
Error Handling
- Missing
YOUTUBE_API_KEY→ stop immediately with instructions - Missing
yt-dlp→ stop immediately with install instructions - Individual video transcript failure → log warning, continue pipeline
- Comments disabled (403) → return empty array, continue
- API quota exceeded (403 with reason
quotaExceeded) → stop and report progress so far