[0.4.1] - 2026-05-11
Compatibility note. Despite a few additive items (
notebooklm auth refreshCLI,keepalive=constructor argument onNotebookLMClient,NOTEBOOKLM_REFRESH_CMDenv var, two new dataclass fields), 0.4.1 is shipped as a patch release because the dominant work — and the reason to ship now — is auth/cookie stability remediation. Bumping to v0.5.0 would force the long-deferred removal of v0.3-era deprecated APIs (see Stability) earlier than scheduled; we'd rather keep that change isolated from the auth-keepalive work. All additive items are backward compatible — existing code keeps working without changes.
Added
notebooklm auth refreshCLI command - One-shot keepalive that opens a session, triggers the layer-1 SIDTS rotation poke againstaccounts.google.com, persists the rotated cookies tostorage_state.json, and exits. Designed to be scheduled by the OS (launchd / systemd / cron / Task Scheduler / k8s CronJob) to keep an idle profile from staling out between user-driven calls. Pairs naturally with--quietfor log-only-on-error cron output. Requires file/profile-backed authentication — explicitly refuses to run whenNOTEBOOKLM_AUTH_JSONis set (no writable backing store). Seedocs/troubleshooting.mdfor per-OS scheduler recipes (#336).- Periodic keepalive task on
NotebookLMClient- Long-lived clients (agents, workers, multi-hourasync withblocks) can opt into a background task that periodically POSTsRotateCookiesto drive__Secure-1PSIDTSrotation, then persists rotated cookies tostorage_state.jsonimmediately so a crash doesn't lose the freshness. Disabled by default — passkeepalive=<seconds>toNotebookLMClient(...)orNotebookLMClient.from_storage(...)to enable. Values belowkeepalive_min_interval(default 60 s) are clamped up to that floor. The loop swallows transient errors at DEBUG and continues; cancellation on__aexit__is clean. Persistence runs off-loop viaasyncio.to_threadso the loop never blocks on disk I/O. Closes the gap left by the per-call layer-1 poke for clients that never re-callfetch_tokens(#297, #312, #341). - Auto-refresh on auth expiry -
fetch_tokensnow optionally runs a user-provided shell command when a Google session cookie has expired, reloads cookies from the same storage path, and retries once. Opt in by setting theNOTEBOOKLM_REFRESH_CMDenvironment variable to a command that rewritesstorage_state.json(e.g. a sync script reading from a cookie vault). Refresh commands receiveNOTEBOOKLM_REFRESH_STORAGE_PATHandNOTEBOOKLM_REFRESH_PROFILEso profile-aware scripts can target the active auth file. Covers every CLI entry point without changing the public API. Retry guards prevent refresh loops (#336). examples/refresh_browser_cookies.py- SampleNOTEBOOKLM_REFRESH_CMDscript that re-extracts cookies from a live local browser vianotebooklm login --browser-cookies. Provides a recovery path for unattended automation when the in-process keepalive isn't enough (idle gaps, force-logout, password change).Source.created_atandGenerationStatus.urlpublic dataclass fields -Source.created_atis now populated for both nested and deeply-nested response paths.GenerationStatus.urlis now populated bypoll_statusfor media artifact types (audio, video, infographic, slide-deck PDF) so callers can stream the asset as soon as the status flips to ready (#349, #356).ALLOWED_COOKIE_DOMAINSextended for sibling Google products - The browser-cookie import path now accepts cookies from Google's sibling product domains, restoring--browser-cookiesflows for users whose active Google session lives on a sibling surface rather thannotebooklm.google.comdirectly (#362).
Fixed
- Cookies could silently stale out under sustained use -
fetch_tokensnow POSTs tohttps://accounts.google.com/RotateCookies(Chrome's dedicated unsigned rotation endpoint) before hittingnotebooklm.google.comto drive__Secure-1PSIDTS/__Secure-3PSIDTSrotation. Empirically validated against both DBSC-bound (Playwright-minted) and unbound (Firefox-imported) profiles. RPC traffic againstnotebooklm.google.comalone does not appear to trigger rotation, so a keepalive that hit NotebookLM alone could silently stale out. The rotatedSet-Cookielands in the livehttpxjar and is persisted viasave_cookies_to_storage()along thefetch_tokens_with_domains/AuthTokens.from_storagepaths. A 60 s mtime guard rate-limits the layer-1 poke — the POST is skipped when storage was recently rotated. Failures log at DEBUG and never abort token fetch. Disable withNOTEBOOKLM_DISABLE_KEEPALIVE_POKE=1(e.g. networks that blockaccounts.google.com). Closes #312 (#345, #346). - Concurrent
RotateCookiespoke stampede - The 60 s mtime guard only debounces sequential invocations; underasyncio.gatherfan-out, parallel CLI loops, or MCP worker pools, all callers see the same stalestorage_state.jsonmtime and stampede the POST. Three layered protections inside_poke_session: a per-event-loop, per-storage-path async lock registry plus a sync state lock for in-process dedup (anasyncio.gatherof 10 fires exactly one POST), a non-blockingLOCK_EX | LOCK_NBflock on the new.storage_state.json.rotate.locksentinel for cross-process dedup (parallel CLI loops / MCP workers skip silently when another process is rotating), and a failure-stampede protection where the timestamp updates regardless of POST outcome — so a 15 s timeout against a hungaccounts.google.comdoesn't let 10 fanned-out callers each wait the full timeout. The layer-2 keepalive loop now calls the bare_rotate_cookiesdirectly (it's already self-paced viakeepalive_min_interval) andNOTEBOOKLM_DISABLE_KEEPALIVE_POKEcontinues to disable both layers (#347, #348). Notebook.sources_countparsed but never surfaced - Thesources_countfield on the publicNotebookdataclass is now populated fromdata[1]on both LIST and GET notebook shapes; previously it always read as0regardless of actual source count (#350).Artifact.urlunpopulated for media artifacts - Theurlfield on the publicArtifactdataclass is now populated for media types (audio, video, infographic; slide-deck exposes the PDF URL — usedownload_slide_deck(output_format="pptx")for PPTX) so callers no longer need to drop down todownload_*to obtain the asset URL (#349, #356).- Cross-process and refresh-path save races - Close lifecycle and refresh-path saves now serialize correctly with the keepalive writer; concurrent writers no longer overwrite each other's rotated cookies (#344).
- Keepalive ↔ close serialization; stop mutating caller
Auth- The keepalive task no longer races with__aexit__, and no longer mutates theAuthinstance the caller passed in. Callers that share anAuthacross multiple clients now get the isolation the API documented (#343). - Snapshot keepalive cookie jar; normalize explicit
storage_path- The keepalive task now snapshots the livehttpxjar before writing (avoiding torn writes when an RPC is mid-flight); an explicitstorage_path=argument toNotebookLMClientis normalized onto theAuthinstance so the keepalive task writes to the file the caller actually pointed at (#342). - Per-domain cookie scoping on file upload - File-upload requests now send only cookies whose
Domainattribute applies to the upload host, instead of the full jar. Prevents upload rejection when the jar mixes cookies forgoogle.com,notebooklm.google.com, andgoogleusercontent.com(#373, #374). - Two-tier cookie validation pre-flight - Auth loaders now distinguish "missing-but-recoverable" from "fatal" cookie states before attempting an RPC, surfacing clearer errors and avoiding doomed requests against Google's identity surface (#372).
- Preserve cookie attributes on load -
Domain,Path,Secure,HttpOnly, andSameSiteattributes round-trip through storage load, restoring behaviors that depended on cross-host scoping (#365, #368). - Unify flat-cookie selection across loaders - Legacy flat-cookie and modern Playwright storage shapes now share a single selection contract; subtle mismatches between the two paths are eliminated (#375, #376).
- Tolerate non-numeric / out-of-range timestamp values on dataclasses -
Notebook.created_at,Source.created_at, andArtifact.created_atnow catchTypeError,ValueError,OSError, andOverflowErrorfromdatetime.fromtimestampand resolve toNoneinstead of raising on edge-case server responses (#357). examples/refresh_browser_cookies.py--profileplacement - The example invoked... login --browser-cookies <b> --profile <p>but--profileis a top-level Click option and was rejected afterlogin(Error: No such option: --profile). Now invokes... --profile <p> login --browser-cookies <b>and works end-to-end against profile-backed storage.
Infrastructure
- Consolidated URL extraction -
_extract_artifact_url, per-type extractors (audio/video/infographic/slide-deck), and_is_valid_artifact_urlmoved totypes.py. Readiness checks,Artifact.url,GenerationStatus.url, and the download paths now share one URL-selection contract:mp4quality-4 > anymp4> first valid URL for video.SourcesAPI.get_fulltextfixed for YouTube fulltext URLs atmetadata[5][0]along the way (#349, #356). - Removed redundant
ArtifactsAPIURL helpers - Private_is_valid_media_urland_find_infographic_urlshim methods removed; tests now exercise the canonicaltypes.pyhelpers (#358). - E2E
--profilepytest flag -pytest --profile <name>scopes the E2E notebook ID cache to a named profile, so parallel multi-profile test runs don't collide on the cached notebook fixture (#340).