youtube-grab
Take any YouTube URL — single video, channel, or playlist — and bulk-add the constituent videos to a NotebookLM library. Wraps yt-dlp for URL extraction (metadata-only, no downloads) and notebooklm source add for ingest.
Pre-flight checks
- NotebookLM session valid (
Integrations/NotebookLM.mdstatus: active). Abort with retry message if not. .venv/bin/yt-dlpexists. If missing, surface install command:uv pip install --python .venv/bin/python yt-dlp.urlis parseable by yt-dlp (test with--simulate --no-warnings). Reject if not.
Procedure
1. Detect URL type
Use a fast yt-dlp probe:
.venv/bin/yt-dlp --flat-playlist --no-warnings -O "%(_type)s" --playlist-items 1 "{url}"
- Output
playlist→ channel or playlist (multi-video) - Output
url(or empty) → single video - Output blank line / error → invalid URL; abort
2. Extract URLs
Single video:
.venv/bin/yt-dlp --flat-playlist --print "%(id)s|%(title)s" "{url}"
Channel or playlist (with limit):
# Channel handle → use the /videos tab if not already specified
URL="{url}"
if <a class="wikilink wikilink-broken" href="#">"$URL" == *"/@"* && "$URL" != *"/videos"* && "$URL" != *"/playlists"*</a>; then
URL="${URL%/}/videos"
fi
.venv/bin/yt-dlp --flat-playlist --print "%(id)s|%(title)s" --playlist-items "1-{limit}" "$URL"
Channel or playlist (no limit / "all"):
Same as above without --playlist-items. Warn before proceeding if count > 50 — surface to Adam.
Each output line: videoId|videoTitle. Reconstruct full URLs as https://youtube.com/watch?v={videoId}.
3. Resolve target library
If library_slug provided AND Wiki/Libraries/{library_slug}.md exists → use it.
If new_library_name provided → invoke notebooklm-create-library with:
display_name: {new_library_name}slug: kebab-cased version ofnew_library_namesubject_kind: topic(default — caller can override)
If neither: surface to caller (slash command) to prompt Adam.
4. Confirm before bulk-adding
If extracted count > 5: report the URLs with titles and ask Adam to confirm (via the slash command's UI). Adam can pick a subset.
For each URL after confirmation:
- Call
.venv/bin/notebooklm source add {notebooklm_id} youtube "https://youtube.com/watch?v={video_id}". - Capture returned
source_id(or error). - Sleep
delay_seconds(default 2.0) before next call to avoid rate-limit.
Track added: [{source_id, video_id, title}] and failed: [{url, reason}].
5. Sync the library
After all adds, invoke notebooklm-sync for library_slug. This:
- Reconciles the Sources table in
Wiki/Libraries/{slug}.mdwith NotebookLM's source list. - Refreshes the Cached Summary (now includes the new content).
6. Append Log entry
- agent:
ea-orchestrator - skill:
youtube-grab - input:
url={url}, type={detected_type}, limit={limit}, library={library_slug} - output:
added={count}, failed={count}, library={library_path} - artifacts:
<a class="wikilink wikilink-broken" href="#">Wiki/Libraries/{library_slug}</a> - notes: list any failed URLs with reasons
CLI command map
| Operation | Command |
|---|---|
| Detect type | .venv/bin/yt-dlp --flat-playlist --no-warnings -O "%(_type)s" --playlist-items 1 "{url}" |
| List videos (limit N) | .venv/bin/yt-dlp --flat-playlist --print "%(id)s|%(title)s" --playlist-items 1-{N} "{url}" |
| List all | same without --playlist-items |
| Add to NLM | .venv/bin/notebooklm source add {nb_id} youtube "{video_url}" |
Channel URL handling
yt-dlp treats youtube.com/@Handle as the channel home (mixed playlists tab). For "last N videos by upload date", append /videos:
- ✅
https://youtube.com/@AlexHormozi/videos - ⚠️
https://youtube.com/@AlexHormozi— works but may include shorts / live streams in unexpected order
The skill auto-appends /videos for @handle URLs that don't already specify a tab.
Failure modes
- yt-dlp can't parse URL — abort early with the URL pattern that worked best as a hint.
- NotebookLM rate-limit — back off (double
delay_seconds), retry once, then surface partial completion. - Source already in library — NotebookLM dedupes by URL; skill should treat this as success (not failure) and note
already_presentinadded. - Channel returns 0 videos — surface; likely a region-block or wrong URL form. Suggest appending
/videosif not already there. - JS runtime warnings from yt-dlp — informational; don't fail on them. Future:
brew install denoto silence.
Hard rules
- Never download the actual video —
--flat-playlistmode only extracts URLs + lightweight metadata. No bandwidth, no disk. - Always confirm before bulk-adding > 5 videos — rate-limit cost + library quality matters.
- Default
delay_seconds: 2.0— keeps NotebookLM happy. - Never push to a library that's
subject_kind: brandwithout confirmation — brand libraries should be tightly curated.
Future extensions (out of scope for v1)
- Filter by duration (
--match-filter "duration > 600"to skip shorts). - Filter by date range (
--dateafter 20260101). - Ingest playlists into multiple libraries based on title regex.
- Ingest non-YouTube media (Vimeo, Twitter, podcasts) — yt-dlp supports these; skill name would generalize to
media-grab.