Skip to main content

notebooklm-easy

A synchronous, one-call-per-task convenience wrapper around notebooklm-py — the unofficial Python API for Google NotebookLM / Gemini Notebook.

notebooklm-py itself is fully async and, for every generated artifact (podcast, video, quiz, ...), a three-step dance: generate_*() kicks it off, wait_for_completion() polls it, and a matching download_*() fetches the finished file. That's the right shape for a library embedded in an existing async app. This wrapper is for the much more common case — a plain script — and does two things:

  1. No async/await. Every method is a normal blocking call. You don't write async def main(): ...; asyncio.run(main()) at all.
  2. One call per artifact, not three. nlm.audio(nb.id, "podcast.mp3") generates, waits, and downloads in a single line.

It does not reimplement anything — auth, RPC calls, polling, and downloads are all still done by notebooklm-py itself. This is a thin facade over it. Anything it doesn't wrap is still reachable (see Escape hatch).

Install

pip install notebooklm-py notebooklm-easy   # or: pip install -e . from this folder

notebooklm-easy depends on notebooklm-py and does not vendor it — you're always running the real, upstream library underneath.

You still need to authenticate once with notebooklm-py's own CLI (this wrapper doesn't change auth at all — see its Installation guide for the full picture):

pip install "notebooklm-py[browser]"
notebooklm login                      # opens a browser once
notebooklm auth check --test --json   # confirms it worked

After that, notebooklm_easy.NotebookLM() (no arguments) picks up the saved session automatically, exactly like NotebookLMClient.from_storage() does.

Quick start

from notebooklm_easy import NotebookLM

with NotebookLM() as nlm:
    # Reuses a notebook with this exact title if one exists, else creates it
    nb = nlm.get_or_create_notebook("Research")

    # Add sources — URLs and local file paths, auto-detected, uploaded
    # concurrently, and waited-on by default so the next call can use them
    nlm.add_sources(nb.id, [
        "https://en.wikipedia.org/wiki/Artificial_intelligence",
        "./paper.pdf",
    ])

    # Ask a question — returns just the answer text
    print(nlm.ask(nb.id, "What are the key themes?"))

    # Generate + wait + download, in one call each
    nlm.audio(nb.id, "podcast.mp3", instructions="make it engaging")
    nlm.quiz(nb.id, "quiz.json", difficulty="hard")
    nlm.report(nb.id, "briefing.md", format="briefing_doc")

That's the whole shape of the library: a NotebookLM object, used as a context manager (or with explicit .close()), with one method per thing you want to do.

Auth / multiple accounts

NotebookLM(...) accepts every keyword NotebookLMClient.from_storage() does, and passes them straight through — most commonly profile for switching between notebooklm login --profile <name> accounts, and backend="android" to use the Android gRPC backend instead of the default web one:

with NotebookLM(profile="work") as nlm:
    ...

Notebooks

nlm.create_notebook("Title")          # always creates a new one
nlm.get_or_create_notebook("Title")   # reuses an existing notebook with this exact title
nlm.list_notebooks()                  # -> list[Notebook]
nlm.delete_notebook(notebook_id)

Sources

# One source — auto-detects URL (http/https, including YouTube) vs. an
# existing local file path (PDF, text, Markdown, Word, EPUB, audio, video,
# image — whatever notebooklm-py's add_file supports).
nlm.add_source(nb.id, "https://example.com")
nlm.add_source(nb.id, "./report.pdf")

# Several at once, uploaded concurrently, in the same order back:
sources = nlm.add_sources(nb.id, ["https://a.example", "./b.pdf", "https://youtube.com/watch?v=..."])

# Raw pasted text has no URL/path to auto-detect, so it's its own method:
nlm.add_text(nb.id, "My Notes", "... text content ...")

All three default to wait=True — they block until NotebookLM finishes processing the source, so the very next ask() or generate_* call sees it. Pass wait=False to fire-and-forget instead (matching notebooklm-py's own default).

Chat

answer = nlm.ask(nb.id, "Summarize this")                  # -> str, just the answer
result = nlm.ask_full(nb.id, "Summarize this")              # -> AskResult (citations, conversation_id, ...)

Generating content

One method per artifact type. Each does generate_* -> wait_for_completion -> download_* and returns the path it wrote to. Every enum-typed option (format, difficulty, style, ...) accepts a plain, case-insensitive string as well as the real notebooklm.types enum member — "hard" and QuizDifficulty.HARD both work.

nlm.audio(nb.id, "podcast.mp3", instructions="make it fun", format="deep_dive", length="short")
nlm.video(nb.id, "overview.mp4", format="explainer", style="whiteboard")
nlm.cinematic_video(nb.id, "documentary.mp4", instructions="documentary-style summary")
nlm.report(nb.id, "briefing.md", format="briefing_doc")       # or study_guide / blog_post / concept_explanation / custom
nlm.study_guide(nb.id, "study_guide.md")
nlm.quiz(nb.id, "quiz.json", difficulty="hard", quantity="more", output_format="json")   # or markdown / html
nlm.flashcards(nb.id, "cards.json", difficulty="easy")
nlm.slide_deck(nb.id, "slides.pdf", format="presenter_slides", output_format="pdf")       # or pptx
nlm.infographic(nb.id, "infographic.png", orientation="portrait", style="bento_grid")
nlm.data_table(nb.id, "data.csv", instructions="compare key concepts")
nlm.mind_map(nb.id, "mindmap.json", kind="interactive")        # or "note_backed"

Every one of these takes source_ids=[...] to scope generation to specific sources (default: all of them), and timeout=<seconds> to override how long it waits before giving up (defaults are generous — 300s for most types, 600s for video/slide-deck/audio, which tend to run longer).

Handling failure

If generation finishes in a failed or removed (delisted, e.g. after a daily quota rejection) state, these methods raise GenerationFailedError instead of silently returning a broken path:

from notebooklm_easy import GenerationFailedError

try:
    nlm.audio(nb.id, "podcast.mp3")
except GenerationFailedError as e:
    print(e.status.error, e.status.error_code)   # the raw GenerationStatus is on .status

Every other error — auth problems, rate limits, network issues, a bad notebook id — is exactly whatever notebooklm-py itself raises (see its exceptions.py), unchanged. This wrapper doesn't catch or reshape those.

Escape hatch

Anything this wrapper doesn't cover — sharing, labels, research, notes, collections, settings, the raw backend-selected wire access, or a generate_*/download_* combination not wrapped as its own method — is still one call away. Every NotebookLM instance exposes the real async client:

with NotebookLM() as nlm:
    # nlm.client is the actual notebooklm.NotebookLMClient — every
    # namespace (sharing, labels, research, notes, collections, raw, ...)
    # is there, unwrapped.
    coro = nlm.client.sharing.set_public(nb.id, view_level="editor")
    nlm.run(coro)   # runs it on the same background loop as everything else

Important: don't await nlm.client.<...>(...) yourself from a different asyncio context (e.g. inside your own async def) — always go through nlm.run(coro). notebooklm-py's client is loop-bound (it asserts every call happens on the exact event loop it was opened on); NotebookLM opens it on a dedicated background loop and run() is what submits work to that same loop correctly. See notebooklm_easy/_loop.py for the full rationale.

If your program is already async end-to-end, skip this wrapper entirely and use notebooklm.NotebookLMClient directly — that's what it's for.

Using this inside a bot (or any other async app)

NotebookLM is synchronous — every method blocks the calling thread until it's done (ask() for a few seconds, a generated artifact for a minute or more). That's fine in a plain script, but calling it directly inside an async def handler of an asyncio-based bot (kurigram/pyrogram, aiogram, discord.py, ...) freezes that bot's entire event loop — every other chat/user — for as long as the call takes, since asyncio is single-threaded.

The fix: create one NotebookLM() for the whole bot process (not one per request — see why in the example), and reach every blocking call through asyncio.to_thread(...) from inside your async def handlers, so it runs on a worker thread instead of the bot's event loop. examples/telegram_notes_bot.py is a complete, runnable example of exactly this: a kurigram/pyrogram bot with a /notes <youtube_link> command that adds the video as a source and returns notes, done correctly.

Testing without live Google auth

tests/test_smoke.py exercises the whole wrapper (notebook get-or-create, source auto-detection, bulk add, chat, one-call generate+wait+download, failure handling, enum coercion, and — importantly — that every call really does run on the one background loop) against the real installed notebooklm-py types, with only NotebookLMClient.from_storage swapped for a fake that never touches the network. Useful both as a regression test and as a template if you want to unit-test your own code that uses this wrapper without a live Google session.

pip install pytest
pytest tests/

Metadata

Release files for notebooklm-easy 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for notebooklm-easy 0.1.0
File Size Uploaded
notebooklm_easy-0.1.0.tar.gz 18.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for notebooklm-easy 0.1.0
File Interpreter ABI Platform
notebooklm_easy-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 32.3 kB

Release files / notebooklm_easy-0.1.0.tar.gz

Download URL notebooklm_easy-0.1.0.tar.gz
Size 18.4 kB
Tags Source
SHA-256 checksum
How to use checksums
bed4dae62b6117fa2bb1e91b96670307726f8e563a8bc5d80d3322fa527dd0b4
BLAKE2b-256 checksum
How to use checksums
f02bc01b08fbf8c0225479ad9eefca08ca6d238da4a9bca751f02e4c4b969a42
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.11.5

Release files / notebooklm_easy-0.1.0-py3-none-any.whl

Download URL notebooklm_easy-0.1.0-py3-none-any.whl
Size 13.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
74c1b51509957c63ff874bcb61898b08daa2e67a45aff127f1fcd0048a22be7f
BLAKE2b-256 checksum
How to use checksums
8d620bc49bb4a2684023686b309cd0d89237766dd7f9a8e062db1a21152738e3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.11.5

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page