2020from typing import Any
2121
2222import click
23+ import httpx
2324from rich .table import Table
2425
25- from ..auth import AuthTokens
26+ from ..auth import (
27+ ALLOWED_COOKIE_DOMAINS ,
28+ GOOGLE_REGIONAL_CCTLDS ,
29+ AuthTokens ,
30+ convert_rookiepy_cookies_to_storage_state ,
31+ extract_cookies_from_storage ,
32+ fetch_tokens ,
33+ )
2634from ..client import NotebookLMClient
2735from ..paths import (
2836 get_browser_profile_dir ,
6573 " 4. Check if notebooklm.google.com is accessible in your browser"
6674)
6775
76+ # Maps user-facing browser names to rookiepy function names.
77+ _ROOKIEPY_BROWSER_ALIASES : dict [str , str ] = {
78+ "arc" : "arc" ,
79+ "brave" : "brave" ,
80+ "chrome" : "chrome" ,
81+ "chromium" : "chromium" ,
82+ "edge" : "edge" ,
83+ "firefox" : "firefox" ,
84+ "ie" : "ie" ,
85+ "librewolf" : "librewolf" ,
86+ "octo" : "octo" ,
87+ "opera" : "opera" ,
88+ "opera-gx" : "opera_gx" ,
89+ "opera_gx" : "opera_gx" ,
90+ "safari" : "safari" ,
91+ "vivaldi" : "vivaldi" ,
92+ "zen" : "zen" ,
93+ }
94+
95+
96+ def _handle_rookiepy_error (e : Exception , browser_name : str ) -> None :
97+ """Print a user-friendly error for rookiepy exceptions."""
98+ msg = str (e ).lower ()
99+ if "lock" in msg or "database" in msg :
100+ console .print (
101+ f"[red]Could not read { browser_name } cookies: browser database is locked.[/red]\n "
102+ "Close your browser and try again."
103+ )
104+ elif "permission" in msg or "access" in msg :
105+ console .print (
106+ f"[red]Permission denied reading { browser_name } cookies.[/red]\n "
107+ "You may need to grant Terminal/Python access to your browser profile directory."
108+ )
109+ elif "keychain" in msg or "decrypt" in msg :
110+ console .print (
111+ f"[red]Could not decrypt { browser_name } cookies.[/red]\n "
112+ "On macOS, allow Keychain access when prompted, or try a different browser."
113+ )
114+ else :
115+ console .print (f"[red]Failed to read cookies from { browser_name } :[/red] { e } " )
116+
117+
118+ def _login_with_browser_cookies (storage_path : Path , browser_name : str ) -> None :
119+ """Extract Google cookies from an installed browser via rookiepy.
120+
121+ Args:
122+ storage_path: Where to write storage_state.json.
123+ browser_name: "auto" to use rookiepy.load(), or a specific browser name.
124+ """
125+ try :
126+ import rookiepy
127+ except ImportError :
128+ console .print (
129+ "[red]rookiepy is not installed.[/red]\n "
130+ "Install it with:\n "
131+ " pip install 'notebooklm-py[cookies]'\n "
132+ "or directly:\n "
133+ " pip install rookiepy"
134+ )
135+ raise SystemExit (1 ) from None
136+
137+ # Build domains list including base and regional Google domains for rookiepy
138+ domains = list (ALLOWED_COOKIE_DOMAINS )
139+ # Add regional Google auth domains (e.g., .google.co.uk, .google.com.sg)
140+ for cctld in GOOGLE_REGIONAL_CCTLDS :
141+ domain = f".google.{ cctld } "
142+ if domain not in domains :
143+ domains .append (domain )
144+
145+ if browser_name == "auto" :
146+ console .print ("[yellow]Reading cookies from installed browser (auto-detect)...[/yellow]" )
147+ try :
148+ raw_cookies = rookiepy .load (domains = domains )
149+ except (OSError , RuntimeError ) as e :
150+ # OSError: file access issues (locked DB, permission denied)
151+ # RuntimeError: decryption/keychain errors
152+ _handle_rookiepy_error (e , "auto-detect" )
153+ raise SystemExit (1 ) from None
154+ else :
155+ canonical = _ROOKIEPY_BROWSER_ALIASES .get (browser_name .lower ())
156+ if canonical is None :
157+ console .print (
158+ f"[red]Unknown browser: '{ browser_name } '[/red]\n "
159+ f"Supported: { ', ' .join (sorted (_ROOKIEPY_BROWSER_ALIASES ))} "
160+ )
161+ raise SystemExit (1 )
162+ console .print (f"[yellow]Reading cookies from { browser_name } ...[/yellow]" )
163+ browser_fn = getattr (rookiepy , canonical , None )
164+ if browser_fn is None or not callable (browser_fn ):
165+ console .print (
166+ f"[red]rookiepy does not support '{ canonical } ' on this platform.[/red]\n "
167+ "Check that rookiepy is properly installed: pip install rookiepy"
168+ )
169+ raise SystemExit (1 )
170+ try :
171+ raw_cookies = browser_fn (domains = domains )
172+ except (OSError , RuntimeError ) as e :
173+ # OSError: file access issues (locked DB, permission denied)
174+ # RuntimeError: decryption/keychain errors
175+ _handle_rookiepy_error (e , browser_name )
176+ raise SystemExit (1 ) from None
177+
178+ storage_state = convert_rookiepy_cookies_to_storage_state (raw_cookies )
179+ try :
180+ cookies = extract_cookies_from_storage (storage_state ) # validates SID is present
181+ except ValueError as e :
182+ console .print (
183+ "[red]No valid Google authentication cookies found.[/red]\n "
184+ f"{ e } \n \n "
185+ "Make sure you are logged into Google in your browser."
186+ )
187+ raise SystemExit (1 ) from None
188+
189+ # Create parent directory (avoid mode= on Windows to prevent ACL issues)
190+ try :
191+ storage_path .parent .mkdir (parents = True , exist_ok = True )
192+ storage_path .write_text (
193+ json .dumps (storage_state , indent = 2 , ensure_ascii = False ), encoding = "utf-8"
194+ )
195+ if sys .platform != "win32" :
196+ # On Unix: ensure both directory and file have restrictive permissions
197+ storage_path .parent .chmod (0o700 )
198+ storage_path .chmod (0o600 )
199+ except OSError as e :
200+ logger .error ("Failed to save authentication to %s: %s" , storage_path , e )
201+ console .print (
202+ f"[red]Failed to save authentication to { storage_path } .[/red]\n " f"Details: { e } "
203+ )
204+ raise SystemExit (1 ) from None
205+
206+ console .print (f"\n [green]Authentication saved to:[/green] { storage_path } " )
207+
208+ # Verify that cookies work — reuse cookies extracted above (no redundant disk read)
209+ try :
210+ run_async (fetch_tokens (cookies ))
211+ logger .info ("Cookies verified successfully" )
212+ console .print ("[green]Cookies verified successfully.[/green]" )
213+ except ValueError as e :
214+ # Cookie validation failed - the extracted cookies are invalid
215+ logger .error ("Extracted cookies are invalid: %s" , e )
216+ console .print (
217+ "[red]Warning: Extracted cookies failed validation.[/red]\n "
218+ "The cookies may be expired or malformed.\n "
219+ f"Error: { e } \n \n "
220+ "Saved anyway, but you may need to re-run login if these are invalid."
221+ )
222+ except httpx .RequestError as e :
223+ # Network error - can't verify but cookies might be OK
224+ logger .warning ("Could not verify cookies due to network error: %s" , e )
225+ console .print (
226+ "[yellow]Warning: Could not verify cookies (network issue).[/yellow]\n "
227+ "Cookies saved but may not be working.\n "
228+ "Try running 'notebooklm ask' to test authentication."
229+ )
230+ except Exception as e :
231+ # Unexpected error - log it fully
232+ logger .warning ("Unexpected error verifying cookies: %s: %s" , type (e ).__name__ , e )
233+ console .print (
234+ f"[yellow]Warning: Unexpected error during verification: { e } [/yellow]\n "
235+ "Cookies saved but please verify with 'notebooklm auth check --test'"
236+ )
237+
238+ _sync_server_language_to_config ()
239+
68240
69241def _sync_server_language_to_config () -> None :
70242 """Fetch server language setting and persist to local config.
@@ -184,7 +356,19 @@ def register_session_commands(cli):
184356 default = "chromium" ,
185357 help = "Browser to use for login (default: chromium). Use 'msedge' for Microsoft Edge." ,
186358 )
187- def login (storage , browser ):
359+ @click .option (
360+ "--browser-cookies" ,
361+ "browser_cookies" ,
362+ default = None ,
363+ is_flag = False ,
364+ flag_value = "auto" ,
365+ help = (
366+ "Read cookies from an installed browser instead of launching Playwright. "
367+ "Optionally specify browser: chrome, firefox, brave, edge, safari, arc, ... "
368+ "Requires: pip install 'notebooklm[cookies]'"
369+ ),
370+ )
371+ def login (storage , browser , browser_cookies ):
188372 """Log in to NotebookLM via browser.
189373
190374 Opens a browser window for Google login. After logging in,
@@ -207,6 +391,12 @@ def login(storage, browser):
207391 )
208392 raise SystemExit (1 )
209393
394+ # rookiepy fast-path: skip Playwright entirely
395+ if browser_cookies is not None :
396+ resolved_storage = Path (storage ) if storage else get_storage_path ()
397+ _login_with_browser_cookies (resolved_storage , browser_cookies )
398+ return
399+
210400 storage_path = Path (storage ) if storage else get_storage_path ()
211401 browser_profile = get_browser_profile_dir ()
212402 if sys .platform == "win32" :
0 commit comments