Install: pip install notebooklm-py==0.6.0 · PyPI
Breaking changes
⚠ BREAKING — exception hierarchy symmetry restored.
SourceNotFoundErrorandArtifactNotFoundErrornow inherit fromRPCError
in addition to their respective domain bases (SourceError,
ArtifactError), restoring symmetry withNotebookNotFoundErrorwhich has
mixed inRPCErrorsince the 0.5.x series. Combined with the new
NotFoundErrorumbrella (see Added below), the class declarations are
now:class NotebookNotFoundError(NotFoundError, RPCError, NotebookError): ... class SourceNotFoundError(NotFoundError, RPCError, SourceError): ... # new RPCError mixin in 0.6.0 class ArtifactNotFoundError(NotFoundError, RPCError, ArtifactError): ... # new RPCError mixin in 0.6.0Migration. Code that catches the broad
RPCErrorbefore a more
specificSourceNotFoundError/ArtifactNotFoundErrorclause now routes
"not found" through the broad branch instead of falling through to the
specific one. Reorder yourexceptclauses so the more specific exceptions
come first.The example below uses
client.sources.get_fulltext(...), which raises
SourceNotFoundErrorfor a missing source. (client.sources.get(...)
returnsNoneand does not raise, so it doesn't demonstrate the change.)# BEFORE — in 0.5.x this layout worked: SourceNotFoundError was NOT an # RPCError, so it fell through the broad `except RPCError` to the specific # handler. In 0.6.0 the broad handler catches it first, leaving the # specific `except SourceNotFoundError` clause unreachable. try: fulltext = await client.sources.get_fulltext(notebook_id, source_id) except RPCError as e: # ← in 0.6.0 this also catches SourceNotFoundError handle_rpc_failure(e) except SourceNotFoundError: # ← in 0.6.0 this branch becomes unreachable handle_missing_source() # AFTER — put the specific exception first so the broad branch only sees # other RPC failures. try: fulltext = await client.sources.get_fulltext(notebook_id, source_id) except SourceNotFoundError: handle_missing_source() except RPCError as e: handle_rpc_failure(e)Code that catches
SourceNotFoundError/ArtifactNotFoundErrordirectly,
or catches via the domain bases (SourceError,ArtifactError), or via the
sharedNotebookLMErrorbase, continues to behave exactly as before. Only
theRPCError-before-specific ordering is affected.
SourceNotFoundError.__init__andArtifactNotFoundError.__init__also
now accept keyword-onlymethod_id/raw_responseparameters (forwarded
to theRPCErrorparent), matching theNotebookNotFoundErrorsignature.
All positional call sites remain source-compatible.
notebooklm source stale <ID>now follows the standard CLI exit-code convention by default. Exit0indicates the freshness check succeeded (regardless of whether the source is fresh or stale); exit1indicates an error. Previously the command used an inverted predicate (0= stale,1= fresh) so the shell idiomif notebooklm source stale ID; then refresh; fiworked naturally. Migration: scripts that depended on the inverted predicate can opt back into the legacy semantics with the new--exit-on-staleflag (if notebooklm source stale --exit-on-stale ID; then refresh; fi). Scripts written for the new default should branch on the JSONstale/freshfields or stdout text. Seedocs/cli-exit-codes.mdfor the full rationale + the newExit code semanticssummary.NotebookLMClient.rpc_call(...)no longer acceptssource_path,_is_retry, oroperation_variant— the three kwargs deprecated in v0.5.0 (docs/improvement.md§7.4,docs/deprecations.md) were removed after one MINOR cycle. The public escape hatch's primary contract (client.rpc_call(method, params)) is unchanged and the default-shape call keeps working with no migration. Migration:- Keyword callers: drop the removed kwarg from the call. The previous default-shape behavior (
source_path="/",_is_retry=False,operation_variant=None) is now what every call gets unconditionally —source_pathwas a leaky internal seam,_is_retrywas an internal retry-loop flag, andoperation_variantis part of the mutating-RPC idempotency registry. Calls that genuinely needed a non-"/"source_pathor a specificoperation_variantwere already on the wrong layer; build a typed method on a sub-client instead, or open an issue describing the workflow. - Positional callers (rare): the positional order of the remaining parameters is
(method, params, allow_null, *, disable_internal_retries=...), so a previously-positionalsource_path/_is_retryargument now binds to a different parameter slot. A pre-cutclient.rpc_call(method, params, "/", True)(which passedsource_path="/",allow_null=True) becomesclient.rpc_call(method, params, allow_null=True)after the cut — switch to keyword arguments forallow_nullto avoid this footgun. - There is no public replacement for the removed internal-only kwargs (
_is_retry,operation_variant); they were never part of the supported surface in the first place.
- Keyword callers: drop the removed kwarg from the call. The previous default-shape behavior (
source add --urlrejects internal hosts by default (SSRF guard).localhost,127.0.0.1, RFC-1918, and link-local URLs — and any non-http(s)scheme — are now refused before ingestion. Migration: pass the new--allow-internalflag to ingest an internalhttp(s)URL intentionally (the scheme allowlist still applies). Full detail in Security below (#1114).sourceCLI--jsonoutput shape changed.source get --jsonnow emits the bare kind value ("type": "url") instead of the leaked Python enum repr ("type": "SourceType.URL"), andsource fulltext --jsonemits a fixed{source_id, title, kind, content, url, char_count}payload instead of a rawasdict(SourceFulltext)dump. Migration:--jsonconsumers parsingsource get'stypefield, or relying on extrafulltextkeys, must update. Full detail in Fixed below (#1129).- Post-parse CLI validation errors exit
1(was2) and print a JSON envelope on stdout under--json. Fordownloadflag conflicts,generatevalidation,research wait --cited-only, andask --new+--conversation-id, a--jsoninvocation now emits{"error": true, "code": "VALIDATION_ERROR", ...}on stdout and exits1instead of Click's stderr usage text + exit2. Text-mode behavior is unchanged. Migration: automation parsing these--jsonfailures should branch on exit1+ the JSON body. Full detail in Changed below (ADR-015; #1112, #1115, #1117).
Added
notebooklm source stale --exit-on-staleflag — opt-in back-compat for the legacy inverted-predicate exit codes (0= stale,1= fresh). The default behavior is now the standard CLI convention (see Breaking changes above); pass--exit-on-staleto keepif notebooklm source stale --exit-on-stale ID; then refresh; fishell idioms working.Exit code semanticssummary section indocs/cli-exit-codes.md. A normative one-line table —0= succeeded as documented,1= failed or queried target not found,2= Click parser-time error — backing the convention every command obeys outside the documented intentional exceptions. Cross-references the existing tables and ADR-015.NotFoundErrorcross-domain umbrella exception. CatchNotFoundErrorto handle any "resource not found" case across notebooks, sources, and artifacts in oneexceptclause — replacingexcept (NotebookNotFoundError, SourceNotFoundError, ArtifactNotFoundError):.NotebookNotFoundError,SourceNotFoundError, andArtifactNotFoundErrorall inherit fromNotFoundError. The umbrella itself is additive; the asymmetric inheritance noted on its original introduction has been resolved in the same release — all three subclasses also mix inRPCError(see Breaking changes above for theexcept-ordering migration).notebooklm notebook delete --json(#1167).notebook deletewas the last delete command (and the onlylist/create/metadatasibling) without a JSON envelope — passing--jsoncrashed withNo such option. It now emits the typed success/cancel envelope, refuses to prompt in--jsonmode (requiring--yes, else aVALIDATION_ERRORenvelope + exit1), and surfacescontext_cleared: truewhen the deleted notebook was the active context (#1193).notebooklm skill install --dry-run/--no-clobber/--force(#1109). Project-scope installs now classify each target as create / up-to-date / overwrite. A target that would be overwritten with different content exits1and lists the conflicts unless--force(overwrite) or--no-clobber(skip differing, still create missing) is passed;--dry-runpreviews intended writes without touching disk. Writes go through an atomic temp-file +os.replaceso a crash can't leave a partialSKILL.md. User scope keeps the historical always-overwrite behavior (the new flags error when paired with--scope user).GenerationStatus.is_removed+status="removed"(#1168). A delisted or quota-removed artifact now reportsstatus="removed"(is_removed=True) instead of a synthesized"failed", so callers can distinguish a transient list omission from a server-marked FAILED artifact.is_failedstaysFalsefor a removal;is_rate_limitedstill treats a quota-worded removal as retryable, and CLI exit behavior is unchanged (#1195).- Structured media-timeout diagnostics. When an accepted media task (audio / video / cinematic-video / infographic / slide-deck) stays queued or running past the
--wait/wait_for_completionbudget, the artifact APIs now raise a typed timeout exception that preserves the last poll-status transition and media-not-ready metadata (also surfaced in--json) instead of a bare timeout (#1094).
Changed
- Media
--waitdefault timeouts raised.generate audio --waitnow defaults to 1200 s (#1140) and the video / cinematic-video wait defaults were increased to match empirical generation durations (#1088, #1094), so long generations no longer time out before the artifact is ready under default settings.docs/now documents the media wait budgets and the manualartifact waitrecovery path. notebooklm doctorexits1when any check fails (#1160). It previously builtstatus: "fail"entries but always exited0, so CI health checks,set -escripts, and monitoring probes read a broken install as green. Overall health is now computed from the final check states (after any--fix) and the process exits1if any check still fails (warnings stay non-fatal). The exit happens after the payload/table is emitted, so machine-readable--jsonoutput is unaffected;doctorprofile JSON errors are also now wrapped in the typed envelope (#1179, #1146).- Post-parse CLI validation errors emit the typed JSON envelope under
--json(ADR-015).downloadflag conflicts (--force+--no-clobber,--latest+--earliest,--all+--artifact),generatevalidation (cinematic--format/--styleconflicts, invalid--language/NOTEBOOKLM_HL),research wait --cited-onlywithout--import-all, andask --new+--conversation-idnow route through{"error": true, "code": "VALIDATION_ERROR", ...}on stdout and exit1under--json, instead of Click's parser bypassing the envelope to exit2with usage text on stderr. Text-mode behavior (usage text, exit2) is unchanged. Flagged under Breaking changes above for--jsonautomation (#1112, #1115, #1117).
Fixed
notebooklm artifact delete <id> --jsonnow requires--yesbefore deleting (#1197). Without--yes, the command emits the typedVALIDATION_ERRORenvelope, includes"deleted": false, exits1, and leaves the artifact untouched, matching the other destructive delete commands.- HTML file uploads now fail client-side with a clear validation error (#1127).
notebooklm source add ./article.htmlandclient.sources.add_file(..., "article.html")previously reached NotebookLM's upload endpoint astext/htmland surfaced a cryptic upstream400 Bad Request. The upload pipeline now rejects.html/.htm/.xhtml/.xht/ HTML MIME uploads before registering a source, with guidance to convert the page to.txt,.md, or.pdf. notebooklm source fulltext -o FILEno longer silently overwrites existing files (#1173). Existing output paths now auto-rename by default (FILE->FILE (2), etc.); pass--forceto overwrite intentionally or--no-clobberto fail when the path already exists.sources.list()raises on a malformedGET_NOTEBOOKresponse under strict-decode (the default) (#1159). A drifted or error-enveloped response was previously folded into an empty list, so a sync script could conclude every source had vanished and re-add them all. The hand-rolled list-shape checks now honorNOTEBOOKLM_STRICT_DECODE(logging the drift warning, then raisingRPCError); a genuinely empty notebook (aNonesources slot) still returns[]. SetNOTEBOOKLM_STRICT_DECODE=0for the legacy warn-and-return-[]fallback (#1178).client.rpc_call(..., allow_null=True)raises on method-ID drift and anti-bot walls (#1158). The decoder gated its entire null-handling block behindnot allow_null, so opt-in null callers (CREATE_ARTIFACT,GENERATE_MIND_MAP,DELETE_SOURCE,GET_SUGGESTED_REPORTS, …) silently receivedNonewhen Google rotated a method ID or served a redirect / anti-bot page. An absent RPC ID (drift) and a body with no RPC frames (anti-bot wall) now always raise; only a present-but-nullwrb.frframe returnsNone. Null-result error messages now embed the discoveredfound_ids(#1176).- Auth-refresh replay no longer re-issues non-idempotent writes (#1157). After a mid-flight auth error (HTTP 401/403, or an auth-shaped decoded
RPCError) on a probe-then-create method (CREATE_NOTEBOOK,CREATE_ARTIFACT,CREATE_NOTE,ADD_SOURCE,SHARE_NOTEBOOK,GENERATE_MIND_MAP), the refresh-and-retry path could duplicate the resource, invite email, or generation quota when the error landed after the server committed the write. Both replay paths (theAuthRefreshMiddleware401/403 leg and theRpcExecutordecode-time leg) now honor the effectivedisable_internal_retriesclassification and propagate the original auth error so the caller's probe-then-create wrapper can disambiguate a commit-lost write (#1177). client.notes.createraises whenCREATE_NOTEreturns no usable note id (#1162). It previously fell through to a success-shapedNote(id="")that was never finalized viaUPDATE_NOTEor persisted server-side, so any later operation keyed on the empty id silently misbehaved. It now raisesRPCError, matching the siblingadd_source/notebooks.createpaths (#1186).- Stale authed-POST envelope rebuilt after a
401 → refresh → 429 → retryflow (#1096). The terminal freshness guard's snapshot-equality short-circuit could POST the pre-refresh URL / headers / body against the refreshed cookie jar; the envelope is now rebuilt from a freshly captured auth snapshot on every terminal attempt (byte-identical on the happy path, load-bearing on the post-refresh retry). NotebookLMClient.close(drain=True)no longer hangs on in-flight artifact polls (#1161). Registered drain hooks (which cancel polls parked inoperation_scope) now fire before the drain wait, soclose()short-circuits a pending poll instead of blocking up to the poll's own 300 s timeout (#1182).Kernel.open()closes the httpx client if the open-time cookie snapshot raises (#1163). A failure while capturing the open snapshot previously propagated with a live, never-closed client (Python skips__aexit__after a failed__aenter__), leaking the connection pool.open()nowaclose()s the partial client and resets it so a retry rebuilds cleanly (#1187).- RPC concurrency semaphore gains the loop-affinity guard + close→reopen reset its siblings already have (#1169). The per-client
max_concurrent_rpcssemaphore was the only loop-bound primitive without an affinity guard or reset, so reopening a capped client on a different event loop reused the stale semaphore and could raise "bound to a different event loop" or mispark waiters on Python 3.10/3.11 (masked on 3.12+). It is now guarded by the bound-loop assertion and discarded on any bound-loop change (#1196). - New conversations are serialized per notebook (#1144). Concurrent
chat.ask()calls with noconversation_idagainst the same notebook are serialized so they no longer race to create duplicate server-side conversations. - Auth-refresh lock released if the lock-wait metric raises (#1164).
await_refreshrecorded the lock-wait metric betweenacquire()and thetry, so a metric-side exception left the auth-refresh lock held forever, deadlocking every subsequent refresh. The metric call moved inside thetry/finally: release(), matching the siblingupdate_auth_tokens(#1188). - Source-upload registration fails closed on an unparseable source id (#1143). The resumable-upload path now raises instead of silently accepting a response it can't parse a source id from, while still tolerating the legacy filename-first row shapes.
- Artifact-generation defaults and null responses hardened (#1063, #1088). Omitting infographic options on the Python
client.artifacts.generate_*calls now sends concrete visual defaults (matching the CLI) instead of producing a nullCREATE_ARTIFACTresult, and a null artifact-generation response is now classified asArtifactFeatureUnavailableError. sourcecommand--jsonoutput shape corrected and stabilized (#1129).source get --jsonpreviously leaked the Python enum repr ("type": "SourceType.URL") and now emits the bare kind value ("type": "url");source fulltext --jsonnow emits a fixed{source_id, title, kind, content, url, char_count}payload instead of a rawasdict(SourceFulltext)dump, and its-oenvelope gains akindfield.--jsonconsumers that parsedsource get'stypefield or relied on extrafulltextkeys must update (flagged under Breaking changes above). Shared serializers keep the shape consistent across the source subcommands going forward.notebooklm source add -(stdin) rejects a non-text--type. Piping content from stdin with an explicit non-text source type now fails with a clear validation error instead of mis-routing the content.notebooklm agent showroutes errors to stderr (#1175) so they no longer pollute stdout.- Auth-error classification hardened (#1142) — empty RPC code labels no longer slip past the auth-error matcher.
- Malformed
batchexecutechunk records are now counted (#1141) rather than silently dropped, so theclient.metricssurface reflects partial-response drift.
Removed
NotebookLMClient.rpc_call(source_path=...),NotebookLMClient.rpc_call(_is_retry=...),NotebookLMClient.rpc_call(operation_variant=...)— see Breaking changes above. The correspondingDeprecationWarningemitters inclient.pyand thetests/unit/test_rpc_call_public_surface.pywarning-surface tests were retired in the same change.
Security
- SSRF guard on
source add --url(#1114). The prefix-onlystartswith(("http://", "https://"))check was replaced with a structuralurlsplitparse + scheme allowlist (http/httpsonly) plus a private / loopback / link-local IP guard and alocalhost-literal guard. Behavior change:http://localhost,http://127.0.0.1, RFC-1918 hosts, andhttp://169.254.169.254are now rejected by default — pass the new--allow-internalflag to ingest an internal URL intentionally (the scheme allowlist still applies). DNS is never resolved at validation time. Flagged under Breaking changes above. - Resumable upload URLs validated and redacted (#1130). The server-returned upload session / cancel URLs are validated before use and redacted in error and log output so a credentialed upload URL can't leak.
- Artifact download allowlist validated by hostname (#1172). Download host-allowlisting now parses the URL hostname structurally instead of matching a string prefix, closing a bypass where a crafted URL (including encoded-slash hosts, hardened further in #1199) could satisfy a prefix check while pointing at an untrusted host.
httpx/urllib3logs redacted for library consumers (#1166).configure_logging()now attaches a logger-levelRedactingFilterto thehttpxandurllib3loggers at import, so a consumer who enables httpx DEBUG (e.g.logging.basicConfig(level=logging.DEBUG)) no longer sees the session id in?f.sid=...request lines. Pure defense-in-depth — no handler is added, so consumers who never enable those loggers see no behavior change (#1191).- Bare CSRF / session-id token values redacted in logs (#1165). The scrubber now redacts bare
SNlM0e(CSRF) andFdrFJe(session-id)WIZ_global_datamarkers, thecsrf=form alias, and standaloneAF1_QpN-CSRF tokens — credential-equivalent shapes that previously passed throughscrub_secrets()unredacted (#1189). - Playwright login subprocess output sanitized (#1111).
ensure_chromium_installednow strips ANSI control sequences and redacts inherited environment-variable secret values (including JSON-nested leaves such asNOTEBOOKLM_AUTH_JSON) from captured subprocess stderr/stdout before surfacing install diagnostics (meta-audit G4).