Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Query Google NotebookLM notebooks from Claude Code for source-grounded, citation-backed answers from Gemini.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/browser_session.py
1#!/usr/bin/env python32"""3Browser Session Management for NotebookLM4Individual browser session for persistent NotebookLM conversations5Based on the original NotebookLM API implementation6"""78import time9import sys10from typing import Any, Dict, Optional11from pathlib import Path1213from patchright.sync_api import BrowserContext, Page1415# Add parent directory to path16sys.path.insert(0, str(Path(__file__).parent))1718from browser_utils import StealthUtils192021class BrowserSession:22"""23Represents a single persistent browser session for NotebookLM2425Each session gets its own Page (tab) within a shared BrowserContext,26allowing for contextual conversations where NotebookLM remembers27previous messages.28"""2930def __init__(self, session_id: str, context: BrowserContext, notebook_url: str):31"""32Initialize a new browser session3334Args:35session_id: Unique identifier for this session36context: Browser context (shared or dedicated)37notebook_url: Target NotebookLM URL for this session38"""39self.id = session_id40self.created_at = time.time()41self.last_activity = time.time()42self.message_count = 043self.notebook_url = notebook_url44self.context = context45self.page = None46self.stealth = StealthUtils()4748# Initialize the session49self._initialize()5051def _initialize(self):52"""Initialize the browser session and navigate to NotebookLM"""53print(f"π Creating session {self.id}...")5455# Create new page (tab) in context56self.page = self.context.new_page()57print(f" π Navigating to NotebookLM...")5859try:60# Navigate to notebook61self.page.goto(self.notebook_url, wait_until="domcontentloaded", timeout=30000)6263# Check if login is needed64if "accounts.google.com" in self.page.url:65raise RuntimeError("Authentication required. Please run auth_manager.py setup first.")6667# Wait for page to be ready68self._wait_for_ready()6970# Simulate human inspection71self.stealth.random_mouse_movement(self.page)72self.stealth.random_delay(300, 600)7374print(f"β Session {self.id} ready!")7576except Exception as e:77print(f"β Failed to initialize session: {e}")78if self.page:79self.page.close()80raise8182def _wait_for_ready(self):83"""Wait for NotebookLM page to be ready"""84try:85# Wait for chat input86self.page.wait_for_selector("textarea.query-box-input", timeout=10000, state="visible")87except Exception:88# Try alternative selector89self.page.wait_for_selector('textarea[aria-label="Feld fΓΌr Anfragen"]', timeout=5000, state="visible")9091def ask(self, question: str) -> Dict[str, Any]:92"""93Ask a question in this session9495Args:96question: The question to ask9798Returns:99Dict with status, question, answer, session_id100"""101try:102self.last_activity = time.time()103self.message_count += 1104105print(f"π¬ [{self.id}] Asking: {question}")106107# Snapshot current answer to detect new response108previous_answer = self._snapshot_latest_response()109110# Find chat input111chat_input_selector = "textarea.query-box-input"112try:113self.page.wait_for_selector(chat_input_selector, timeout=5000, state="visible")114except Exception:115chat_input_selector = 'textarea[aria-label="Feld fΓΌr Anfragen"]'116self.page.wait_for_selector(chat_input_selector, timeout=5000, state="visible")117118# Click and type with human-like behavior119self.stealth.realistic_click(self.page, chat_input_selector)120self.stealth.human_type(self.page, chat_input_selector, question)121122# Small pause before submit123self.stealth.random_delay(300, 800)124125# Submit126self.page.keyboard.press("Enter")127128# Wait for response129print(" β³ Waiting for response...")130self.stealth.random_delay(1500, 3000)131132# Get new answer133answer = self._wait_for_latest_answer(previous_answer)134135if not answer:136raise Exception("Empty response from NotebookLM")137138print(f" β Got response ({len(answer)} chars)")139140return {141"status": "success",142"question": question,143"answer": answer,144"session_id": self.id,145"notebook_url": self.notebook_url146}147148except Exception as e:149print(f" β Error: {e}")150return {151"status": "error",152"question": question,153"error": str(e),154"session_id": self.id155}156157def _snapshot_latest_response(self) -> Optional[str]:158"""Get the current latest response text"""159try:160# Use correct NotebookLM selector161responses = self.page.query_selector_all(".to-user-container .message-text-content")162if responses:163return responses[-1].inner_text()164except Exception:165pass166return None167168def _wait_for_latest_answer(self, previous_answer: Optional[str], timeout: int = 120) -> str:169"""Wait for and extract the new answer"""170start_time = time.time()171last_candidate = None172stable_count = 0173174while time.time() - start_time < timeout:175# Check if NotebookLM is still thinking (most reliable indicator)176try:177thinking_element = self.page.query_selector('div.thinking-message')178if thinking_element and thinking_element.is_visible():179time.sleep(0.5)180continue181except Exception:182pass183184try:185# Use correct NotebookLM selector186responses = self.page.query_selector_all(".to-user-container .message-text-content")187188if responses:189latest_text = responses[-1].inner_text().strip()190191# Check if it's a new response192if latest_text and latest_text != previous_answer:193# Check if text is stable (3 consecutive polls)194if latest_text == last_candidate:195stable_count += 1196if stable_count >= 3:197return latest_text198else:199stable_count = 1200last_candidate = latest_text201202except Exception:203pass204205time.sleep(0.5)206207raise TimeoutError(f"No response received within {timeout} seconds")208209def reset(self):210"""Reset the chat by reloading the page"""211print(f"π Resetting session {self.id}...")212213self.page.reload(wait_until="domcontentloaded")214self._wait_for_ready()215216previous_count = self.message_count217self.message_count = 0218self.last_activity = time.time()219220print(f"β Session reset (cleared {previous_count} messages)")221return previous_count222223def close(self):224"""Close this session and clean up resources"""225print(f"π Closing session {self.id}...")226227if self.page:228try:229self.page.close()230except Exception as e:231print(f" β οΈ Error closing page: {e}")232233print(f"β Session {self.id} closed")234235def get_info(self) -> Dict[str, Any]:236"""Get information about this session"""237return {238"id": self.id,239"created_at": self.created_at,240"last_activity": self.last_activity,241"age_seconds": time.time() - self.created_at,242"inactive_seconds": time.time() - self.last_activity,243"message_count": self.message_count,244"notebook_url": self.notebook_url245}246247def is_expired(self, timeout_seconds: int = 900) -> bool:248"""Check if session has expired (default: 15 minutes)"""249return (time.time() - self.last_activity) > timeout_seconds250251252if __name__ == "__main__":253# Example usage254print("Browser Session Module - Use ask_question.py for main interface")255print("This module provides low-level browser session management.")