Files
teng-lin--notebooklm-py/docs/troubleshooting.md
T
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

838 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Troubleshooting
**Status:** Active
**Last Updated:** 2026-07-04
Common issues, known limitations, and workarounds for `notebooklm-py`.
## Common Errors
### Authentication Errors
**First step:** Run `notebooklm auth check` to diagnose auth issues:
```bash
notebooklm auth check # Quick local validation
notebooklm auth check --test # Full validation with network test
notebooklm auth check --json # Machine-readable output for CI/CD
```
This shows:
- Storage file location and validity
- Which cookies are present and their domains
- Whether NOTEBOOKLM_AUTH_JSON or NOTEBOOKLM_HOME is being used
- (With `--test`) Whether token fetch succeeds
#### Automatic Token Refresh
The client **automatically refreshes** CSRF tokens when authentication errors are detected. This happens transparently:
- When an RPC call fails with an auth error, the client:
1. Fetches fresh CSRF token and session ID from the NotebookLM homepage
2. Waits briefly to avoid rate limiting
3. Retries the failed request once
- Concurrent requests share a single refresh task to prevent token thrashing
- If refresh fails, the original error is raised with the refresh failure as cause
This means most "CSRF token expired" errors resolve automatically.
#### Cookie freshness for long-running / unattended use
Google rotates `__Secure-1PSIDTS` (the freshness partner of `__Secure-1PSID`) on its own cadence; the on-disk `Expires` field is **not** a reliable predictor of server-side validity. The library handles freshness in layered fallbacks, ordered cheapest to heaviest:
1. **Per-call rotation poke** (default ON) — every `fetch_tokens` makes a best-effort POST to `accounts.google.com/RotateCookies`. Disable with `NOTEBOOKLM_DISABLE_KEEPALIVE_POKE=1`.
2. **Periodic background poke** — pass `keepalive=<seconds>` to `NotebookLMClient` for clients held open for hours.
3. **Layer-3 headless re-auth** — explicit Python opt-in via `await client.refresh_auth(allow_headless=True)`, or automatic mid-RPC opt-in with `NOTEBOOKLM_HEADLESS_REAUTH=1`. This drives the persisted browser profile, or attaches to a loopback Chrome DevTools endpoint from `NOTEBOOKLM_HEADLESS_REAUTH_CDP_URL`. Treat CDP as account-equivalent: only use `127.0.0.1` / `localhost`, never a remote browser.
4. **Layer-4 master-token re-mint** — when a `master_token.json` sits beside the profile's `storage_state.json` (the `[headless]` extra; `notebooklm login --master-token`), a fully-expired session re-mints fresh cookies from the durable master token in-process, after layers 13 are exhausted. This is what makes long-lived headless workers self-heal without a browser. See [installation.md#d-headless-server-or-ci](installation.md#d-headless-server-or-ci).
5. **External recovery script**`NOTEBOOKLM_REFRESH_CMD` runs when auth has fully expired, then retries once.
6. **Manual re-login**`notebooklm login` (or `notebooklm login --master-token-refresh` to force a master-token re-mint; cold-dead cookies at process start need this).
7. **External scheduler**`notebooklm auth refresh` driven by cron / launchd / systemd / Task Scheduler / k8s CronJob, for idle profiles with no Python process running. Recommended cadence: 1520 minutes.
> **Master-token troubleshooting:** `MasterTokenError: ... re-bootstrap` means the master token was revoked (password change / Google security action) — re-run `notebooklm login --master-token`. `... needs gpsoauth` means the `[headless]` extra isn't installed (`pip install "notebooklm-py[headless]"`). A minted jar "missing required cookies" indicates a MergeSession change — file an issue.
>
> **"This browser or app may not be secure" during `--master-token` sign-in:** Google blocks sign-in inside the automated browser the auto-capture launches. The client drops the obvious automation flags, but Google may still block — use one of the reliable paths instead:
> - **Attach to your own Chrome (recommended):** quit Chrome, relaunch it with `--remote-debugging-port=9222`, then `notebooklm login --master-token --account you@gmail.com --cdp-url http://127.0.0.1:9222`. It opens an EmbeddedSetup tab in your real (non-automated) browser, so Google allows sign-in, and scrapes the `oauth_token`.
> - **Capture the token manually:** in a normal browser sign in at `accounts.google.com/EmbeddedSetup`, copy the `oauth_token` cookie (DevTools → Application → Cookies → accounts.google.com), then `notebooklm login --master-token --account you@gmail.com --oauth-token <value>`. The `oauth_token` is single-use and short-lived — use it immediately.
Most users only need layer 1 — it's on by default and requires no configuration. For the full strategy (trade-offs between layers, including Python kwargs like `keepalive_min_interval` and environment variables like `NOTEBOOKLM_REFRESH_CMD_USE_SHELL`, and ready-to-paste launchd / systemd / cron / Task Scheduler / k8s CronJob recipes), see **[docs/auth-cookie-lifecycle.md#tldr](auth-cookie-lifecycle.md#tldr)** for a quick orientation, then [§4 The recovery ladder](auth-cookie-lifecycle.md#4--the-recovery-ladder) for the per-layer deep dive.
#### macOS: `--browser-cookies` prompts for your password
On macOS, Chrome (and Edge / Brave / Opera) encrypts its cookies file with a key stored in the **macOS Keychain** under the entry `Chrome Safe Storage`. By default that entry's ACL only allows `Google Chrome.app` itself to read the key without prompting; any other process — Python, Terminal, cron, an editor — gets a "wants to use the *Chrome Safe Storage* key" dialog. This is how macOS Keychain protects local data and applies to every cookie-extraction tool (`rookiepy`, `browser-cookie3`, `pycookiecheat`), not just `notebooklm-py`.
Workarounds, ordered by hassle:
1. **Click "Always Allow" in the prompt.** Adds the calling Python interpreter to the Keychain entry's ACL so subsequent runs of *that exact binary* should stop prompting. Caveat: rebuilding your venv (e.g. `uv venv` again) usually changes the interpreter path and you'll be re-prompted once for the new path.
2. **Use Touch ID instead of typing the password.** macOS Sonoma+ accepts Touch ID for Keychain dialogs — see *System Settings → Touch ID & Password*.
3. **Pre-unlock the login keychain in your shell** (best for cron jobs after one initial interactive run):
```bash
security unlock-keychain ~/Library/Keychains/login.keychain-db
```
Prompts once for your login password, then any process in the same login session can read entries you've already approved without re-prompting until the keychain auto-locks.
4. **Use Firefox as the cookie source.** Firefox stores cookies in a plain SQLite DB (no Keychain), so `notebooklm login --browser-cookies firefox` runs with **no prompt at all** — provided you're logged into Google in Firefox.
```bash
notebooklm login --browser-cookies firefox
```
This is the simplest answer for unattended macOS use.
**Firefox Multi-Account Containers note.** If your Google session
lives inside a container, unscoped `--browser-cookies firefox` will
merge cookies from every container into one jar (see issues
[#366](https://github.com/teng-lin/notebooklm-py/issues/366) /
[#367](https://github.com/teng-lin/notebooklm-py/issues/367)) and
produce an inconsistent session. Use the explicit container syntax:
```bash
notebooklm login --browser-cookies 'firefox::Work' # named container
notebooklm login --browser-cookies 'firefox::none' # no-container default
```
When a container is in use, the unscoped form also emits a yellow
warning pointing at this syntax.
5. **Truly headless servers.** `--browser-cookies` is not the right tool — there's no live browser to extract from. Either re-extract on a workstation and ship `storage_state.json` to the server, or accept that human interaction is needed when cookies finally expire.
Quick diagnostic:
```bash
security find-generic-password -s 'Chrome Safe Storage' -a 'Chrome' -w >/dev/null && echo OK || echo "ACL or lock issue"
```
Prints `OK` without prompting → keychain is unlocked and your user has access; the prompt you saw is the per-binary ACL re-asking for a new caller (your Python). Click *Always Allow* once and that binary is permanently approved. If it prompts → run `security unlock-keychain` first.
#### Windows: `Missing required cookies: __Secure-1PSIDTS` after login, and `--browser-cookies` "Could not decrypt"
On Windows, both credential paths can leave you without `__Secure-1PSIDTS` (the rotating freshness partner of `__Secure-1PSID` that every real RPC needs — see [Automatic Token Refresh](#automatic-token-refresh) above), so `notebooklm login` reports success but `notebooklm list` then fails with `Missing required cookies: __Secure-1PSIDTS` (issue [#1753](https://github.com/teng-lin/notebooklm-py/issues/1753)). Two distinct causes are in play:
- **`notebooklm login --browser chrome` (Playwright flow).** The interactive browser completes Google sign-in, but Google may serve an automation-detected session *without* the token-binding cookie (and sometimes without the secondary-binding cookies `OSID` / `APISID` + `SAPISID` the automatic `RotateCookies` recovery needs to re-mint it). When that happens the saved `storage_state.json` is genuinely incomplete and re-running the same flow reproduces it.
- **`notebooklm login --browser-cookies chrome` (or `edge`) → `Could not decrypt chrome cookies`.** Chrome 127+ (and current Edge) protect the cookie database with **App-Bound Encryption (ABE)**: the decryption key is bound to the browser process via a Windows service, so no external process can read it. This blocks every cookie-extraction library (`rookiepy`, `browser-cookie3`, `pycookiecheat`), not just `notebooklm-py`. There is no flag that bypasses ABE.
Note that `notebooklm doctor` may still say the auth check passed with an older client — the check historically only looked for `SID`. Current versions surface a **warn** row when `__Secure-1PSIDTS` is missing (`auth check --test` has always reported the real error). Trust `notebooklm auth check --test` / `notebooklm list` over a green `doctor` for "is this session actually usable".
Workarounds, most reliable first:
1. **Use Firefox as the cookie source.** Firefox stores cookies in a plain SQLite DB — **no App-Bound Encryption** — so extraction just works. Sign in to Google in Firefox, then:
```bash
notebooklm login --browser-cookies firefox
```
(If your Google session lives in a Multi-Account Containers tab, use the explicit `firefox::Container` / `firefox::none` syntax — see the [macOS section above](#macos---browser-cookies-prompts-for-your-password) for the container notes.) This is the simplest fix for the ABE case.
2. **Set up a master token (best for unattended / long-lived use).** `notebooklm login --master-token` (needs the `[headless]` extra: `pip install "notebooklm-py[headless]"`) stores a durable `master_token.json` beside your profile. When cookies are missing or fully expired, the client re-mints a complete, fresh cookie jar — including `__Secure-1PSIDTS` — from the master token in-process, so it does not depend on what the browser login happened to hand back. If Google blocks sign-in inside the automated capture window ("This browser or app may not be secure"), use the CDP-attach or manual `oauth_token` variants described in the [master-token troubleshooting note](#cookie-freshness-for-long-running--unattended-use) above. See also [installation.md#d-headless-server-or-ci](installation.md#d-headless-server-or-ci).
3. **Retry the Playwright login on a fresh profile.** Sometimes a stale persistent profile is the culprit rather than automation detection:
```bash
notebooklm login --fresh
```
If three attempts (normal, `--fresh` + password, `--fresh` + passkey) all reproduce the missing cookie, treat it as the automation-detection case and switch to Firefox or a master token above.
#### "Unauthorized" or redirect to login page
**Cause:** Session cookies expired (happens every few weeks).
**Note:** Automatic token refresh handles CSRF/session ID expiration. This error only occurs when the underlying cookies (set during `notebooklm login`) have fully expired.
**Solution:**
```bash
notebooklm login
```
#### "Failed to extract CSRF token (SNlM0e)" / "CSRF token not found in HTML"
**Cause:** The CSRF token (`SNlM0e`) couldn't be extracted from the NotebookLM page response. The exact wording depends on which code path raised it:
- `Failed to extract CSRF token (SNlM0e). Page structure may have changed or authentication expired. Preview: '...'` — raised by `refresh_auth()` when the WIZ_global_data extraction fails ([`client.py`](../src/notebooklm/client.py)).
- `CSRF token not found in HTML. Final URL: <url> This may indicate the page structure has changed.` — raised by the lower-level extractor when no auth redirect was detected ([`auth.py`](../src/notebooklm/auth.py)).
- `Failed to extract 'SNlM0e' from NotebookLM HTML response. This usually means Google changed the page structure. Preview: '...'` — raised as `AuthExtractionError` directly (rare; usually wrapped by one of the messages above) ([`exceptions.py`](../src/notebooklm/exceptions.py)).
A related auth-redirect message — `Authentication expired. Run 'notebooklm login' to re-authenticate.` (or `Authentication expired or invalid. ...`) — surfaces the same root cause when the page redirected to Google's login flow.
**Note:** These errors should rarely surface, since the client automatically retries with a fresh CSRF token on auth failures (see *Automatic Token Refresh* above). When one does reach you, the automatic refresh also failed.
**Solution (if auto-refresh fails):**
```python
# In Python — manual refresh
await client.refresh_auth()
```
Or re-run `notebooklm login` if session cookies are also expired. If the failure persists across re-login, the page structure has likely changed — file an issue and include the `Preview:` snippet from the error.
#### "NotebookLM redirected this request to its region / anti-abuse access gate"
**Cause:** The request to `notebooklm.google.com` was redirected to **`notebooklm.google/?location=unsupported`** — Google's region / anti-abuse risk-control gate (the marketing/landing page, which has no CSRF token). This is **not** a library bug, expired login, or page-structure change, and **re-running `notebooklm login` will not fix it** (the cookies are fine). It is driven by the *access environment*, not just the account's country, and fires even for accounts in supported regions when Google sees:
- a **VPN / proxy / datacenter / shared IP** (especially previously-abused ones),
- an **IP ↔ timezone ↔ browser-language mismatch**, or
- a **non-browser / automated access pattern** (a raw HTTP client without a real browser fingerprint).
**Confirm:** open `https://notebooklm.google.com` in a normal browser, signed in to the same account, on the same network. If it also redirects to `notebooklm.google/?location=unsupported`, the gate is environmental.
**Solution:** access from a **residential connection in a supported region**, keep your system **timezone/language consistent** with the IP's country, and avoid shared/datacenter VPN exit IPs. When the trigger is the **non-browser fingerprint** (a raw HTTP client) rather than the IP, the opt-in browser-TLS-impersonation transport can help: set `NOTEBOOKLM_TRANSPORT=curl_cffi` (requires the `curl_cffi` package) so requests carry a real browser's TLS fingerprint. (See issue [#1630](https://github.com/teng-lin/notebooklm-py/issues/1630).)
#### Browser opens but login fails
**Cause:** Google detecting automation and blocking login.
**Solution:**
1. Delete the browser profile: `rm -rf ~/.notebooklm/profiles/<profile>/browser_profile/` (or `~/.notebooklm/profiles/default/browser_profile/` for the default profile)
2. Run `notebooklm login` again
3. Complete any CAPTCHA or security challenges Google presents
4. Ensure you're using a real mouse/keyboard (not pasting credentials via script)
#### "Login not detected within 5 minutes" (especially on macOS)
**Cause:** The bundled Chromium that `notebooklm login` launches by default opened a fresh, signed-out browser, and its login-detection wait timed out — common on macOS where bundled Chromium can also be flaky (macOS 15+).
**Solution:** If you are already signed in to Google in **system Chrome**, retry with that browser so the existing session is reused instead of starting a fresh sign-in:
```bash
notebooklm login --browser chrome --storage <path>
```
`--browser chrome` drives your installed Google Chrome (with its signed-in profile), which usually detects the account immediately and sidesteps bundled-Chromium issues. `--browser msedge` is the equivalent for organizations that require Microsoft Edge for SSO.
### RPC Errors
#### "RPCError: No result found for RPC ID: XyZ123"
**Cause:** The RPC method ID may have changed (Google updates these periodically), or:
- Rate limiting from Google
- Account quota exceeded
- API restrictions
**Diagnosis:**
```bash
# Enable debug mode to see what RPC IDs the server returns
NOTEBOOKLM_DEBUG_RPC=1 notebooklm <your-command>
```
This will show output like:
```
DEBUG: Looking for RPC ID: Ljjv0c
DEBUG: Found RPC IDs in response: ['NewId123']
```
If the IDs don't match, the method ID has changed. Report the new ID in a GitHub issue.
**Workaround:**
- Wait 5-10 minutes and retry
- Try with fewer sources selected
- Reduce generation frequency
#### RPC method ID rotated by Google — self-patch with `NOTEBOOKLM_RPC_OVERRIDES`
Google rotates undocumented batchexecute method IDs without warning. When
this happens, `notebooklm-py` raises `UnknownRPCMethodError` with the new ID
the server now uses (see the previous section's diagnosis recipe). Rather
than waiting for a release, you can patch the mapping for your process with
the `NOTEBOOKLM_RPC_OVERRIDES` environment variable.
**Format:** JSON object mapping `RPCMethod` member names (the Python enum
member name, not the obfuscated value) to the override RPC ID:
```bash
export NOTEBOOKLM_RPC_OVERRIDES='{"LIST_NOTEBOOKS": "newId123", "CREATE_NOTEBOOK": "newId456"}'
notebooklm list
```
Or in Python:
```python
import os
os.environ["NOTEBOOKLM_RPC_OVERRIDES"] = '{"LIST_NOTEBOOKS": "newId123"}'
from notebooklm import NotebookLMClient
# Subsequent client calls send the override IDs on the wire.
```
**Behavior:**
- The override is applied at BOTH the URL `rpcids=` query parameter AND the
request body `f.req` payload, so the wire format stays consistent.
- The override is gated on the configured base host being a known Google
NotebookLM endpoint (`notebooklm.google.com` or
`notebooklm.cloud.google.com`). Overrides do NOT apply to non-Google
hosts, so this env var cannot be weaponised to leak custom RPC IDs to a
hostile endpoint.
- Method names not listed in the override map continue to use the canonical
IDs from `notebooklm.rpc.types.RPCMethod`.
- Malformed input (invalid JSON, top-level array, etc.) is logged at
`WARNING` and treated as no overrides.
- The first time a distinct override set is applied in a process, the
mapping is logged at `INFO` so you can confirm the config you intended is
live.
**Discovering the new ID:** see the `NOTEBOOKLM_DEBUG_RPC=1` recipe above —
the `Found RPC IDs in response: [...]` line tells you what the server is
now returning. Cross-reference against the call site that failed.
Please also report the rotated IDs in a GitHub issue so the canonical
mapping in `src/notebooklm/rpc/types.py` can be updated for everyone.
#### How to get the full response preview from an RPCError
`RPCError.raw_response` is truncated to **80 chars + `"..."`** by default so
error messages stay readable in logs and CLI output. When you need the
full body to diagnose schema drift or a malformed response, opt in:
```bash
NOTEBOOKLM_DEBUG=1 notebooklm <your-command>
```
Or in Python, set the env var before instantiating the client:
```python
import os
os.environ["NOTEBOOKLM_DEBUG"] = "1"
from notebooklm import NotebookLMClient
# Subsequent RPCError instances will carry the full untruncated body.
```
The value must be exactly `"1"` — `"0"`, `"true"`, etc. are treated as
unset (still truncated).
#### "RPCError: [3]" (Invalid argument) / "UserDisplayableError"
**Cause:** Google's API rejected the request. Common cases:
- Invalid parameters or a not-found resource ID
- Account quota exceeded (for `create`, status `[3]` is also the notebook-limit signal)
- Rate limiting
**Solution:**
- Check that notebook/source IDs are valid
- Add delays between operations (see Rate Limiting section)
**If it only affects _write_ operations** (`create`, `source add`, `generate`) while reads (`list`, `ask`) keep working — **and the web UI still works** — the likely cause is that Google changed the request payload (wire format) for those RPCs and `notebooklm-py` is still sending the old shape. Google rolls these out gradually, so it can hit some accounts before others.
> **First, rule out a mis-decoded success.** Re-run the failing action, then check `notebooklm list`: if the resource was actually **created** despite the error, it's a *response*-decoding issue (share the **Response** below). If it was **not** created, Google rejected our **request** (share the **Payload** below).
**Help us fix it — share the web UI's payload (no cookies needed).** Either option below leaks nothing: cookies, the `at=` CSRF token, and `Set-Cookie` live in request/response *headers*, never inside the `f.req` payload.
> **⚠️ Never paste the raw `.har` itself.** A HAR contains your cookies, the `at=` CSRF token, and the full NotebookLM page HTML — which embeds API keys, the CSRF token, and your account email. Only ever share the **scrubber's output** below (or the single `f.req` line from Option B). The scrubber processes only `/batchexecute` calls and redacts every value, so the page HTML never reaches its output.
**Option A — thorough, auto-scrubbed (recommended; captures every RPC + its response).**
This walkthrough takes ~2 minutes. You never copy a cookie, a token, or the raw HAR — a small bundled script does the redaction for you.
1. **Open DevTools on the Network tab.** In Chrome/Edge press <kbd>F12</kbd> (or <kbd>Cmd</kbd>+<kbd>Opt</kbd>+<kbd>I</kbd> on macOS); in Firefox press <kbd>F12</kbd>. Click the **Network** tab at the top of the panel.
2. **Arm the capture.** Tick **Preserve log** (Chrome) / **Persist Logs** (Firefox) so a page reload doesn't wipe the capture, and confirm the round **● Record** button is red (it usually is by default).
3. **Reproduce the failure.** In the NotebookLM tab, perform the exact action that fails — e.g. create a notebook, or add a source. You'll see `batchexecute?rpcids=…` rows appear in the Network list. You can stop as soon as the action errors.
4. **Export the session to a file:**
- **Chrome/Edge:** click the ⤓ **Export HAR…** download icon in the Network toolbar (or right-click any row → **Save all as HAR with content**).
- **Firefox:** click the ⚙️ gear / **…** menu in the Network toolbar → **Save All As HAR**.
Save it as `capture.har`. (The "with content" variant matters — it's what includes the *response* bodies the scrubber reports.)
5. **Scrub it.** From your `notebooklm-py` checkout, run the bundled script (stdlib-only — no install needed):
```console
$ python scripts/scrub_rpc_har.py capture.har
NotebookLM RPC capture — string values → <str:N>; cookies / headers / at= / Set-Cookie never read:
CCqFvf (CREATE_NOTEBOOK)
request : ["<str:7>",null,null,[2],[1]]
response: HTTP 200 | status_code=[3] | result=null
1 call(s). Safe to share — no cookies / CSRF / session tokens are present (they live in headers, which this tool never reads).
```
Narrow to a single RPC with `--rpcid` if the capture is noisy:
```console
$ python scripts/scrub_rpc_har.py capture.har --rpcid CCqFvf
```
The script reads **only** each request's `f.req` field and the response body — never the headers/cookies arrays, never the non-`batchexecute` page HTML — and replaces every text value with its length (`<str:7>`). It refuses to print if any raw string ever slips through, so the output is safe by construction.
6. **Paste that output** into the issue. Read it back first as a sanity check: every value should be `<str:N>`, never readable text.
- `status_code=[3]` with `result=null` → Google rejected our **request** (a payload/wire-format change). This is what we need to fix it.
- A non-null `result` → the call actually worked and this is a **response**-decode issue; share it just the same.
**Option B — quick, one RPC by hand (no script).** Use this if you can't run the script.
1. In DevTools → **Network**, click the `batchexecute?rpcids=…` POST for the failing call (e.g. `rpcids=CCqFvf` for create, `izAoDd` for add-source).
2. Open the **Payload** tab (Chrome) / **Request** tab (Firefox), copy **only** the `f.req` value — **not** the `at=` field beside it, and don't open the **Cookies**/**Headers** tabs.
3. Replace any free text (title, URL) with `REDACTED` and paste it. We diff it against what the library sends — `["<title>", null, null, [2], [1]]` for create — and update the payload.
### Generation Failures
#### Audio/Video generation is refused immediately
**Cause:** NotebookLM refused the generation kickoff synchronously (often quota,
feature availability, rate limiting, or an RPC shape drift). In v0.8.0 the
Python API raises instead of returning `None`.
**What to do:**
```bash
# Let the CLI surface the typed error envelope / message
notebooklm generate audio --wait --json
# If generation was accepted and you have a task id, poll manually
notebooklm artifact poll <task_id> --json
```
In Python, catch `RateLimitError`, `ArtifactFeatureUnavailableError`, or
`RPCError` depending on the failure. If kickoff succeeds and later polling
times out, use the timeout guidance below.
#### Audio/Video task times out as pending or in progress
**Cause:** NotebookLM accepted the generation task, but the upstream media queue
did not reach a terminal state before your wait budget. For media artifacts, the
SDK also keeps polling if NotebookLM marks the row completed before the media
URL is populated.
**Solution:**
- Increase the wait budget with `--timeout` or the Python
`wait_for_completion(..., timeout=...)` argument.
- For `generate <kind> --wait`, the built-in media defaults are 1200s for
audio, 1800s for standard video, and 3600s for cinematic video; pass a
larger `--timeout` if your account's media queue is slower.
- `artifact wait` is intentionally generic and still defaults to 300s; when
waiting manually on a media task ID, pass the matching media timeout.
- Catch `ArtifactPendingTimeoutError` to retry queued tasks separately from
`ArtifactInProgressTimeoutError`, which means the task started but did not
finish before the timeout.
- Log `exc.status_history` and `exc.status_transitions` for upstream queueing
diagnostics instead of parsing the exception message.
#### Mind map or data table "generates" but doesn't appear
**Cause:** Generation may silently fail without error.
**Solution:**
- Wait 60 seconds and check `artifact list`
- Try regenerating with different/fewer sources
### File Upload Issues
#### HTML/XHTML files are rejected before upload
**Cause:** NotebookLM's file-upload endpoint rejects HTML-family uploads, even
though the web UI may accept pasted rich text.
**Solution:** Convert saved web pages to text, Markdown, or PDF before adding
them with your preferred extractor:
```bash
notebooklm source add ./article.txt
```
You can also pipe extracted text through stdin:
```bash
python extract_article_text.py ./article.html | notebooklm source add - --type text --title "Article"
```
#### Text/Markdown upload succeeds but processing/content is wrong
**Cause:** The upload was accepted, but NotebookLM processed unexpected content
or reported a source-processing error. Current `add_file()` returns a `Source`;
missing or untrusted source IDs raise `SourceAddError` instead of returning
`None`.
**Workaround:** When you control the text, bypass file-type inference and use
`add_text`:
```bash
# Instead of: notebooklm source add ./notes.txt
# Do:
notebooklm source add - --type text --title "My Notes" < ./notes.txt
```
Or in Python:
```python
content = Path("notes.txt").read_text()
await client.sources.add_text(nb_id, "My Notes", content)
```
#### Large files time out
**Cause:** Files over ~20MB may exceed upload timeout.
**Solution:** Split large documents or use text extraction locally.
---
### Protected Website Content Issues
#### X.com / Twitter content incorrectly parsed as error page
**Symptoms:**
- Source title shows "Fixing X.com Privacy Errors" or similar error message
- Generated content discusses browser extensions instead of the actual article
- Source appears to process successfully but contains wrong content
**Cause:** X.com (Twitter) has aggressive anti-scraping protections. When NotebookLM attempts to fetch the URL, it receives an error page or compatibility warning instead of the actual content.
**Solution - Use `bird` CLI to pre-fetch content:**
The `bird` CLI can fetch X.com content and output clean markdown:
```bash
# Step 1: Install bird (macOS/Linux)
brew install steipete/tap/bird
# Step 2: Fetch X.com content as markdown
bird read "https://x.com/username/status/1234567890" > article.md
# Step 3: Add the local markdown file to NotebookLM
notebooklm source add ./article.md
```
**Alternative methods:**
**Using browser automation:**
```bash
# If you have playwright/browser-use available
# Fetch content via browser and save as markdown
```
**Manual extraction:**
1. Open the X.com post in a browser
2. Copy the text content
3. Save to a `.md` file
4. Add the file to NotebookLM
**Verification:**
Always verify the source was correctly parsed:
```bash
notebooklm source list
# Check that the title matches the actual article, not an error message
```
If the title contains error-related text, remove the source and use the pre-fetch method:
```bash
# Remove incorrectly parsed source
notebooklm source delete <source_id>
# Or, if you only have the exact title:
notebooklm source delete-by-title "Exact Source Title"
# Then re-add using the bird CLI method above
```
**Other affected sites:**
- Some paywalled news sites
- Sites requiring JavaScript execution for content
- Sites with aggressive bot detection
---
## Known Limitations
### Rate Limiting
Google enforces strict rate limits on the batchexecute endpoint.
**Symptoms:**
- `RateLimitError` in Python, or CLI JSON with `code: "RATE_LIMITED"`
- `RPCError` with ID `R7cb6c`
- `UserDisplayableError` with code `[3]`
**Best Practices:**
**CLI:** Use `--retry` for automatic exponential backoff:
```bash
notebooklm generate audio --retry 3 # Retry up to 3 times on rate limit
notebooklm generate video --retry 5 # Works with most generate commands
```
*Note: `generate mind-map` is synchronous and does not accept the `--retry` option. All other `generate` subcommands support `--retry`.*
**Python:**
```python
import asyncio
from notebooklm import RPCError
from notebooklm.artifacts import with_rate_limit_retry
# Add delays between intensive operations
for url in urls:
await client.sources.add_url(nb_id, url)
await asyncio.sleep(2) # 2 second delay
# Use the shared generation retry policy when starting artifacts
status = await with_rate_limit_retry(
lambda: client.artifacts.generate_audio(nb_id),
max_retries=3,
)
# For non-artifact RPC calls, retry by passing a fresh callable each attempt
async def retry_rpc_call(make_call, max_retries=3):
for attempt in range(max_retries + 1):
try:
return await make_call()
except RPCError:
if attempt >= max_retries:
raise
await asyncio.sleep(2**attempt)
notebook = await retry_rpc_call(lambda: client.notebooks.create("Research Notes"))
```
### Starting a brand-new conversation (resolves the older issue #659 workaround)
`client.chat.ask(notebook_id, question)` with `conversation_id=None`
attaches the question to the user's **current** conversation on the
notebook — by design. The SDK still fetches the server-recorded
conversation_id via `hPTbtc` after the ask and returns it on
`AskResult.conversation_id`, so follow-ups using that id work
correctly.
To force a brand-new server-side conversation, delete the existing
one first — this mirrors the web UI's "Delete history" button:
```python
last_conv_id = await client.chat.get_conversation_id(nb_id)
if last_conv_id:
await client.chat.delete_conversation(nb_id, last_conv_id)
result = await client.chat.ask(nb_id, "Start fresh")
```
Or via the CLI (prompts for confirmation; `-y` skips):
```bash
notebooklm ask --new -y "Start fresh"
```
**This is destructive: deleted turns are not recoverable.** The CLI
shows the conversation's short id in the prompt and defaults to "No".
`--json` implies `--yes` so scripted callers don't hang on stdin.
**History:** Before the SDK gained `delete_conversation` it had no way
to honor the "fresh conversation" intent — both the SDK and the CLI's
`--new` flag would silently extend the most-recent conversation, so
users worked around it by creating a new notebook for each thread.
The `J7Gthc` RPC was reverse-engineered from the web UI's "Delete
history" button and removes the need for that workaround.
### Quota Restrictions
Some features have daily/hourly quotas:
- **Audio Overviews:** Limited generations per day per account
- **Video Overviews:** More restricted than audio
- **Deep Research:** Consumes significant backend resources
### Download Requirements
Artifact downloads (audio, video, images) use `httpx` with cookies from your storage state. **Playwright is NOT required for downloads**—only for the initial `notebooklm login`.
If downloads fail with authentication errors:
**Solution:** Ensure your authentication is valid:
```bash
# Re-authenticate if cookies have expired
notebooklm login
# Or copy a fresh storage_state.json from another machine
```
**Custom auth paths:** When using `from_storage(path=...)` or `from_storage(profile="work")`,
artifact downloads automatically use the same storage path for cookie authentication.
If you are on an older version where downloads fail with "Storage file not found" pointing
to the default location, upgrade or set `NOTEBOOKLM_HOME` as a workaround.
### URL Expiry
Download URLs for audio/video are temporary:
- Expire within hours
- Always fetch fresh URLs before downloading:
```python
# Get fresh artifact list before download
artifacts = await client.artifacts.list(nb_id)
audio = next(a for a in artifacts if a.kind == "audio")
# Use audio.url immediately
```
---
## Platform-Specific Issues
### Linux
**Playwright missing dependencies:**
```bash
playwright install-deps chromium
```
**`playwright install chromium` fails with `TypeError: onExit is not a function`:**
This is an environment-specific Playwright install failure that has been observed with some newer Playwright builds on Linux. `notebooklm-py` only needs a working browser install for `notebooklm login`; the workaround is to install a known-good Playwright version in a clean virtual environment.
**Workaround** (intentionally uses `pip` rather than the canonical `uv sync --frozen` flow from [installation.md#e-contributor](installation.md#e-contributor) — this workaround needs to *override* the `playwright>=1.40.0` constraint to a specific older version, which `uv sync --frozen` would refuse):
```bash
python -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install "playwright==1.57.0"
python -m playwright install chromium
pip install -e ".[all]"
```
**Why this order matters:**
- `python -m playwright ...` ensures you use the Playwright module from the active virtual environment
- installing the browser before `pip install -e ".[all]"` avoids picking up an older broken global `playwright` executable
- if you already have another `playwright` on your system, verify with `which playwright` after activation
- using `pip` here (not `uv sync --frozen`) is deliberate: this workaround needs to override the project's resolved `playwright` version with a specific older release, which the locked `uv` flow would block
If you need a non-editable install from Git instead of a local checkout, replace the last step with:
```bash
pip install "git+https://github.com/<your-user>/notebooklm-py@<branch>"
```
**No display available (headless server):**
- Browser login requires a display
- Authenticate on a machine with GUI, then copy `storage_state.json`
### macOS
**Chromium not opening:**
```bash
# Re-install Playwright browsers
playwright install chromium
```
**Security warning about Chromium:**
- Allow in System Preferences → Security & Privacy
### Windows
**CLI hangs indefinitely (issue #75):**
On certain Windows environments (particularly when running inside Sandboxie or similar sandboxing software), the CLI may hang indefinitely at startup. This is caused by the default `ProactorEventLoop` blocking at the IOCP (I/O Completion Ports) layer.
**Symptoms:**
- CLI starts but never responds
- Process appears frozen with no output
- Happens consistently in sandboxed environments
**Solution:** The library automatically sets `WindowsSelectorEventLoopPolicy` at CLI startup to avoid this issue. If you're using the Python API directly and encounter hanging, add this before any async code:
```python
import asyncio
import sys
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
```
**Unicode encoding errors on non-English Windows (issue #75, #80):**
Windows systems with non-English locales (Chinese cp950, Japanese cp932, etc.) may fail with `UnicodeEncodeError` when outputting Unicode characters like checkmarks (✓) or emojis.
**Symptoms:**
- `UnicodeEncodeError: 'cp950' codec can't encode character`
- Error occurs when printing status output with Rich tables
**Solution:** The library automatically sets `PYTHONUTF8=1` at CLI startup. For Python API usage, either:
1. Set `PYTHONUTF8=1` environment variable before running
2. Run Python with `-X utf8` flag: `python -X utf8 your_script.py`
**Path issues:**
- Use forward slashes or raw strings: `r"C:\path\to\file"`
- Ensure `~` expansion works: use `Path.home()` in Python
### WSL
**Browser opens in Windows, not WSL:**
- This is expected behavior
- Storage file is saved in WSL filesystem
---
## Debugging Tips
### Logging Configuration
`notebooklm-py` provides structured logging to help debug issues. The variables below are the logging-relevant subset; for the full environment-variable reference (storage, profile, network, decoder strictness, RPC overrides, etc.) and precedence rules, see [docs/configuration.md#environment-variables](configuration.md#environment-variables).
**Environment Variables (logging-specific):**
| Variable | Default | Effect |
|----------|---------|--------|
| `NOTEBOOKLM_LOG_LEVEL` | `WARNING` | Set to `DEBUG`, `INFO`, `WARNING`, or `ERROR` |
| `NOTEBOOKLM_DEBUG_RPC` | (unset) | Legacy: Set to `1` to enable `DEBUG` level |
| `NOTEBOOKLM_DEBUG` | (unset) | Set to `1` to preserve the full raw RPC response body on `RPCError.raw_response` (default: truncated to 80 chars + `"..."`) |
**When to use each level:**
```bash
# WARNING (default): Only show warnings and errors
notebooklm list
# INFO: Show major operations (good for scripts/automation)
NOTEBOOKLM_LOG_LEVEL=INFO notebooklm source add https://example.com
# Output:
# 14:23:45 INFO [notebooklm._sources] Adding URL source: https://example.com
# DEBUG: Show all RPC calls with timing (for troubleshooting API issues)
NOTEBOOKLM_LOG_LEVEL=DEBUG notebooklm list
# Output:
# 14:23:45 DEBUG [notebooklm._core] RPC LIST_NOTEBOOKS starting
# 14:23:46 DEBUG [notebooklm._core] RPC LIST_NOTEBOOKS completed in 0.842s
```
**Programmatic use:**
```python
import logging
import os
# Set before importing notebooklm
os.environ["NOTEBOOKLM_LOG_LEVEL"] = "DEBUG"
from notebooklm import NotebookLMClient
# Now all notebooklm operations will log at DEBUG level
```
### Test Basic Operations
Start simple to isolate issues:
```bash
# 1. Can you list notebooks?
notebooklm list
# 2. Can you create a notebook?
notebooklm create "Test"
# 3. Can you add a source?
notebooklm source add "https://example.com"
```
### Network Debugging
If you suspect network issues:
```python
import httpx
# Test basic connectivity
async with httpx.AsyncClient() as client:
r = await client.get("https://notebooklm.google.com")
print(r.status_code) # Should be 200 or 302
```
---
## Adapter-specific issues (MCP server, REST server)
The MCP and REST servers have their own setup and failure modes documented with
the features:
- **MCP server / remote connector** (stdio & HTTP transports, self-hosted OAuth,
Cloudflare / Tailscale tunnels): see [mcp-guide.md#troubleshooting](mcp-guide.md#troubleshooting)
and [deploy/README.md](../deploy/README.md).
- **REST server** (`notebooklm-server`): a non-loopback bind refuses to start
without `NOTEBOOKLM_SERVER_TOKEN`; every `/v1` request needs the bearer (401
otherwise). See [installation.md#rest-api-server](installation.md#rest-api-server).
- **`curl_cffi` transport**: `NOTEBOOKLM_TRANSPORT=curl_cffi` requires the
`curl_cffi` package; see the region / anti-abuse gate section above.
## Getting Help
1. Check this troubleshooting guide
2. Search [existing issues](https://github.com/teng-lin/notebooklm-py/issues)
3. Open a new issue with:
- Command/code that failed
- Full error message
- Python version (`python --version`)
- Library version (`notebooklm --version`)
- Operating system