notebooklm-py
Libraryteng-lin/notebooklm-py
Unofficial Python API and CLI for Google NotebookLM.
Overview
Provides full programmatic access to NotebookLM features, including batch downloads, podcast generation, and agent integration. Supports Python, CLI, and AI agents like Claude Code.
README Preview
# notebooklm-py\n\n \n\n\n**A Comprehensive NotebookLM Skill & Unofficial Python API.** Full programmatic access to NotebookLM's features—including capabilities the web UI doesn't expose—via Python, CLI, and AI agents like Claude Code, Codex, and OpenClaw.\n\n[](https://pypi.org/project/notebooklm-py/)\n[](https://pypi.org/project/notebooklm-py/)\n[](https://opensource.org/licenses/MIT)\n[](https://github.com/teng-lin/notebooklm-py/actions/workflows/test.yml)\n\n \n\n\n**Source & Development**: \n\n> **⚠️ Unofficial Library - Use at Your Own Risk**\n>\n> This library uses **undocumented Google APIs** that can change without notice.\n>\n> - **Not affiliated with Google** - This is a community project\n> - **APIs may break** - Google can change internal endpoints anytime\n> - **Rate limits apply** - Heavy usage may be throttled\n>\n> Best for prototypes, research, and personal projects. See [Troubleshooting](docs/troubleshooting.md) for debugging tips.\n\n## What You Can Build\n\n🤖 **AI Agent Tools** - Integrate NotebookLM into Claude Code, Codex, and other LLM agents. Ships with a root [NotebookLM skill](SKILL.md) for GitHub and `npx skills add` discovery, local `notebooklm skill install` support for Claude Code and `.agents` skill directories, and repo-level Codex guidance in [`AGENTS.md`](AGENTS.md).\n\n📚 **Research Automation** - Bulk-import sources (URLs, PDFs, YouTube, Google Drive), run web/Drive research queries with auto-import, and extract insights programmatically. Build repeatable research pipelines.\n\n🎙️ **Content Generation** - Generate Audio Overviews (podcasts), videos, slide decks, quizzes, flashcards, infographics, data tables, mind maps, and study guides. Full control over formats, styles, and output.\n\n📥 **Downloads & Export** - Download all generated artifacts locally (MP3, MP4, PDF, PNG, CSV, JSON, Markdown). Export to Google Docs/Sheets. **Features the web UI doesn't offer**: batch downloads, quiz/flashcard export in multiple formats, mi
FAQ (5)
TroubleshootingHow to fix "ModuleNotFoundError: No module named 'typing_extensions'" when running notebooklm-py 0.8.5?
notebooklm-py 0.8.5 imports typing_extensions at runtime but fails to declare it as a dependency, so clean installs via pipx/uv/mise crash on startup. Workaround: install typing_extensions into the same isolated environment, e.g. pipx inject notebooklm-py typing_extensions or uv pip install --python <notebooklm-py venv>/bin/python typing_extensions. Permanent fix: upgrade to a release containing PR #2500, which declares typing_extensions as an unconditional dependency and adds CI checks for clean installs.
TroubleshootingHow to refactor repeated loop-affinity code into a reusable Python mixin?
Create a LoopBoundPrimitive base class with a set_bound_loop method that stores the loop and fires a hook _on_loop_rebind(old_loop, new_loop) only when the loop actually changes. Subclasses can override the hook for clear-on-rebind behaviour. This consolidates duplicated bookkeeping across multiple components. Example: src/notebooklm/_loop_bound.py.
TroubleshootingHow to fix TargetClosedError on context.close() during notebooklm master-token login on Windows?
In notebooklm-py 0.8.5 on Windows, notebooklm login --master-token --browser chrome captures the oauth_token successfully, but teardown in src/notebooklm/_browser/oauth_token.py raises TargetClosedError at context.close() inside the finally block (because Chrome already closed itself). The exception escapes finally, so the captured token is discarded and master_token.json is never written. Workaround: patch oauth_token.py to wrap page.close(), context.close(), and browser_obj.close() each in try/except Exception: pass, then rerun the login command — it should report Master-token login OK. A permanent fix is in PR #2522; upgrade notebooklm-py once it is merged/released instead of keeping the local patch.
TroubleshootingHow do I prevent headless re-auth from replacing storage_state.json with an expired Google SID cookie?
This happens because Google now returns an HTTP 200 signed-out landing page for GET / on notebook.google.com, while the capture logic only checks that the browser is on the app host and has a URL-scoped SID cookie. An expired SID therefore passes validation and overwrites storage_state.json.
Immediate workaround: disable headless re-auth so the expired browser profile cannot be captured:
unset NOTEBOOKLM_HEADLESS_REAUTH
# or
export NOTEBOOKLM_HEADLESS_REAUTH=0
Also ensure allow_headless=False, then restore a valid storage_state.json or authenticate again.
Code fix: in src/notebooklm/_browser/browser_capture.py, update both the headless path around _settle_capture_candidate/_captured_sid_is_usable and the CDP path to verify session liveness before writing storage. Either require the SNlM0e token on the captured app page, or perform the /login redirect check: a live session redirects back to the app, while a signed-out session goes to accounts.google.com. On failure, call the same refusal path used for SID-less captures, such as _refuse_incomplete_capture, and retain the existing authentication state. Add a regression test using a token-less app landing with an expired SID in the browser context.
TroubleshootingWhy does notebooklm-py title lookup fail for notebook titles containing the Turkish capital letter İ?
This is a known bug (affects main @ 1d8920f, MCP _resolve_by_title and _normalize_for_match): str.casefold() converts 'İ' (U+0130) into two characters, 'i' + U+0307 (combining dot above), so a lowercase query like istanbul never matches the folded title İstanbul notları, raising NotebookNotFoundError. Workaround: type the title with its original exact characters, including the capital 'İ' (e.g., query İstanbul instead of istanbul) — exact-character queries match because both sides fold identically. Permanent fix: strip combining marks during normalization, e.g. in _normalize_for_match use unicodedata.normalize('NFKD', s) and remove characters where unicodedata.combining(c) is true before/after casefold(), so 'İ' folds to plain 'i'.