deepdiver 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepdiver/__init__.py +38 -0
- deepdiver/content_processor.py +343 -0
- deepdiver/deepdive.py +801 -0
- deepdiver/deepdiver.yaml +79 -0
- deepdiver/notebooklm_automator.py +1441 -0
- deepdiver/podcast_manager.py +402 -0
- deepdiver/session_tracker.py +723 -0
- deepdiver-0.1.0.dist-info/METADATA +455 -0
- deepdiver-0.1.0.dist-info/RECORD +13 -0
- deepdiver-0.1.0.dist-info/WHEEL +5 -0
- deepdiver-0.1.0.dist-info/entry_points.txt +2 -0
- deepdiver-0.1.0.dist-info/licenses/LICENSE +21 -0
- deepdiver-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1441 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NotebookLM Automation Module
|
|
3
|
+
Part of DeepDiver - NotebookLM Podcast Automation System
|
|
4
|
+
|
|
5
|
+
This module handles browser automation for NotebookLM interactions,
|
|
6
|
+
including login, document upload, podcast generation, and file management.
|
|
7
|
+
|
|
8
|
+
Assembly Team: Jerry β‘, Nyro β οΈ, Aureon πΏ, JamAI πΈ, Synth π§΅
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import time
|
|
17
|
+
import requests
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Dict, List, Optional, Any
|
|
20
|
+
from urllib.parse import urljoin
|
|
21
|
+
|
|
22
|
+
import yaml
|
|
23
|
+
from playwright.async_api import async_playwright, Browser, BrowserContext, Page
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
27
|
+
# CDP URL RESOLUTION - Chrome DevTools Protocol
|
|
28
|
+
# β οΈ Nyro: Three-tier priority chain for multi-network support
|
|
29
|
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
30
|
+
|
|
31
|
+
def get_cdp_url(override: str = None, config_path: str = "deepdiver/deepdiver.yaml") -> str:
|
|
32
|
+
"""
|
|
33
|
+
Get CDP (Chrome DevTools Protocol) URL using priority chain
|
|
34
|
+
|
|
35
|
+
Priority order:
|
|
36
|
+
1. override parameter (highest - explicit function call)
|
|
37
|
+
2. DEEPDIVER_CDP_URL environment variable (session-specific)
|
|
38
|
+
3. CDP_URL from config file (persistent user config)
|
|
39
|
+
4. http://localhost:9222 (fallback default)
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
override: Explicit CDP URL (e.g., from --cdp-url flag)
|
|
43
|
+
config_path: Path to configuration file
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
CDP URL string
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
# Command-line override (highest priority)
|
|
50
|
+
get_cdp_url('http://192.168.1.100:9222')
|
|
51
|
+
|
|
52
|
+
# Environment variable
|
|
53
|
+
export DEEPDIVER_CDP_URL=http://10.0.0.5:9222
|
|
54
|
+
get_cdp_url() # β http://10.0.0.5:9222
|
|
55
|
+
|
|
56
|
+
# Config file
|
|
57
|
+
# deepdiver.yaml contains: CDP_URL: http://server:9222
|
|
58
|
+
get_cdp_url() # β http://server:9222
|
|
59
|
+
|
|
60
|
+
# Fallback
|
|
61
|
+
get_cdp_url() # β http://localhost:9222
|
|
62
|
+
"""
|
|
63
|
+
# Priority 1: Explicit override parameter
|
|
64
|
+
if override:
|
|
65
|
+
return override
|
|
66
|
+
|
|
67
|
+
# Priority 2: Environment variable
|
|
68
|
+
env_cdp = os.environ.get('DEEPDIVER_CDP_URL')
|
|
69
|
+
if env_cdp:
|
|
70
|
+
return env_cdp
|
|
71
|
+
|
|
72
|
+
# Priority 3: Config file
|
|
73
|
+
if os.path.exists(config_path):
|
|
74
|
+
try:
|
|
75
|
+
with open(config_path, 'r') as f:
|
|
76
|
+
config = yaml.safe_load(f)
|
|
77
|
+
if config and 'BROWSER_SETTINGS' in config:
|
|
78
|
+
cdp_url = config['BROWSER_SETTINGS'].get('cdp_url')
|
|
79
|
+
if cdp_url:
|
|
80
|
+
return cdp_url
|
|
81
|
+
except Exception:
|
|
82
|
+
pass # Fall through to default
|
|
83
|
+
|
|
84
|
+
# Priority 4: Default localhost (Chrome DevTools Protocol standard port)
|
|
85
|
+
return 'http://localhost:9222'
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
89
|
+
# CHROME CDP HELPER FUNCTIONS
|
|
90
|
+
# β οΈπΏπΈπ§΅ G.Music Assembly - Auto-launch Chrome for init
|
|
91
|
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
92
|
+
|
|
93
|
+
def find_chrome_executable() -> Optional[str]:
|
|
94
|
+
"""
|
|
95
|
+
Find Chrome/Chromium executable on the system
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
str: Chrome command name, or None if not found
|
|
99
|
+
"""
|
|
100
|
+
candidates = ['google-chrome', 'chromium', 'chromium-browser', 'chrome']
|
|
101
|
+
for cmd in candidates:
|
|
102
|
+
if shutil.which(cmd):
|
|
103
|
+
return cmd
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def check_chrome_cdp_running(cdp_url: str = 'http://localhost:9222') -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Check if Chrome CDP is running at specified URL
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
cdp_url: CDP URL to check (default: http://localhost:9222)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
bool: True if Chrome CDP is accessible, False otherwise
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
# Extract host and port from CDP URL
|
|
119
|
+
if '://' in cdp_url:
|
|
120
|
+
cdp_url = cdp_url.split('://')[1]
|
|
121
|
+
|
|
122
|
+
# Handle localhost vs IP
|
|
123
|
+
if cdp_url.startswith('localhost:'):
|
|
124
|
+
port = cdp_url.split(':')[1]
|
|
125
|
+
test_url = f'http://localhost:{port}/json/version'
|
|
126
|
+
else:
|
|
127
|
+
test_url = f'http://{cdp_url}/json/version'
|
|
128
|
+
|
|
129
|
+
response = requests.get(test_url, timeout=2)
|
|
130
|
+
return response.status_code == 200
|
|
131
|
+
except:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def launch_chrome_cdp(port: int = 9222, user_data_dir: str = None) -> bool:
|
|
136
|
+
"""
|
|
137
|
+
Launch Chrome with CDP enabled
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
port: CDP port number (default: 9222)
|
|
141
|
+
user_data_dir: Chrome user data directory
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
bool: True if Chrome launched successfully, False otherwise
|
|
145
|
+
"""
|
|
146
|
+
chrome_cmd = find_chrome_executable()
|
|
147
|
+
if not chrome_cmd:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
if user_data_dir is None:
|
|
151
|
+
user_data_dir = os.path.expanduser('~/.chrome-deepdiver')
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
subprocess.Popen([
|
|
155
|
+
chrome_cmd,
|
|
156
|
+
f'--remote-debugging-port={port}',
|
|
157
|
+
f'--user-data-dir={user_data_dir}'
|
|
158
|
+
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
159
|
+
|
|
160
|
+
# Wait for Chrome to start
|
|
161
|
+
time.sleep(3)
|
|
162
|
+
return check_chrome_cdp_running(f'http://localhost:{port}')
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class NotebookLMAutomator:
|
|
168
|
+
"""
|
|
169
|
+
Main automation class for NotebookLM interactions.
|
|
170
|
+
|
|
171
|
+
Handles browser automation, authentication, document upload,
|
|
172
|
+
podcast generation, and file management through Playwright.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, config_path: str = "deepdiver/deepdiver.yaml", cdp_url_override: str = None):
|
|
176
|
+
"""
|
|
177
|
+
Initialize the NotebookLM automator with configuration.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
config_path: Path to configuration file
|
|
181
|
+
cdp_url_override: Optional CDP URL override (highest priority)
|
|
182
|
+
"""
|
|
183
|
+
# Set up logger FIRST so other methods can use it
|
|
184
|
+
self.logger = self._setup_logging()
|
|
185
|
+
|
|
186
|
+
self.config_path = config_path
|
|
187
|
+
self.config = self._load_config(config_path)
|
|
188
|
+
self.browser: Optional[Browser] = None
|
|
189
|
+
self.context: Optional[BrowserContext] = None
|
|
190
|
+
self.page: Optional[Page] = None
|
|
191
|
+
|
|
192
|
+
# NotebookLM specific settings
|
|
193
|
+
self.base_url = self.config.get('NOTEBOOKLM_SETTINGS', {}).get('base_url', 'https://notebooklm.google.com')
|
|
194
|
+
|
|
195
|
+
# Browser settings - Use CDP URL priority chain
|
|
196
|
+
self.cdp_url = get_cdp_url(override=cdp_url_override, config_path=config_path)
|
|
197
|
+
self.user_data_dir = self.config.get('BROWSER_SETTINGS', {}).get('user_data_dir', '/tmp/chrome-deepdiver')
|
|
198
|
+
self.headless = self.config.get('BROWSER_SETTINGS', {}).get('headless', False)
|
|
199
|
+
|
|
200
|
+
# General timeout from browser settings (in seconds), converted to ms
|
|
201
|
+
self.timeout = self.config.get('BROWSER_SETTINGS', {}).get('timeout', 30) * 1000
|
|
202
|
+
|
|
203
|
+
self.logger.info("β οΈπΏπΈπ§΅ NotebookLMAutomator initialized")
|
|
204
|
+
self.logger.info(f"π CDP URL: {self.cdp_url}")
|
|
205
|
+
|
|
206
|
+
def _load_config(self, config_path: str) -> Dict[str, Any]:
|
|
207
|
+
"""Load configuration from YAML file."""
|
|
208
|
+
try:
|
|
209
|
+
with open(config_path, 'r') as f:
|
|
210
|
+
return yaml.safe_load(f)
|
|
211
|
+
except FileNotFoundError:
|
|
212
|
+
self.logger.warning(f"Configuration file {config_path} not found, using defaults")
|
|
213
|
+
return {}
|
|
214
|
+
except yaml.YAMLError as e:
|
|
215
|
+
self.logger.error(f"Error parsing configuration: {e}")
|
|
216
|
+
return {}
|
|
217
|
+
|
|
218
|
+
def _setup_logging(self) -> logging.Logger:
|
|
219
|
+
"""Set up logging configuration."""
|
|
220
|
+
logger = logging.getLogger('NotebookLMAutomator')
|
|
221
|
+
logger.setLevel(logging.INFO)
|
|
222
|
+
|
|
223
|
+
if not logger.handlers:
|
|
224
|
+
handler = logging.StreamHandler()
|
|
225
|
+
formatter = logging.Formatter(
|
|
226
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
227
|
+
)
|
|
228
|
+
handler.setFormatter(formatter)
|
|
229
|
+
logger.addHandler(handler)
|
|
230
|
+
|
|
231
|
+
return logger
|
|
232
|
+
|
|
233
|
+
async def connect_to_browser(self) -> bool:
|
|
234
|
+
"""
|
|
235
|
+
Connect to existing Chrome browser via Chrome DevTools Protocol.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
bool: True if connection successful, False otherwise
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
self.logger.info("π Connecting to Chrome browser via CDP...")
|
|
242
|
+
|
|
243
|
+
playwright = await async_playwright().start()
|
|
244
|
+
|
|
245
|
+
# Connect to existing browser
|
|
246
|
+
self.browser = await playwright.chromium.connect_over_cdp(self.cdp_url)
|
|
247
|
+
|
|
248
|
+
# Get the first available context
|
|
249
|
+
contexts = self.browser.contexts
|
|
250
|
+
if contexts:
|
|
251
|
+
self.context = contexts[0]
|
|
252
|
+
else:
|
|
253
|
+
self.context = await self.browser.new_context()
|
|
254
|
+
|
|
255
|
+
# Get the first available page or create new one
|
|
256
|
+
pages = self.context.pages
|
|
257
|
+
if pages:
|
|
258
|
+
self.page = pages[0]
|
|
259
|
+
else:
|
|
260
|
+
self.page = await self.context.new_page()
|
|
261
|
+
|
|
262
|
+
self.logger.info("β
Successfully connected to Chrome browser")
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
self.logger.error(f"β Failed to connect to browser: {e}")
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
async def navigate_to_notebooklm(self) -> bool:
|
|
270
|
+
"""
|
|
271
|
+
Navigate to NotebookLM and verify the page loaded correctly.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
bool: True if navigation successful, False otherwise
|
|
275
|
+
"""
|
|
276
|
+
try:
|
|
277
|
+
if not self.page:
|
|
278
|
+
self.logger.error("β No browser page available")
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
self.logger.info(f"π Navigating to {self.base_url}...")
|
|
282
|
+
# Use a longer timeout for navigation, and wait for a specific element
|
|
283
|
+
navigation_timeout = self.config.get('NOTEBOOKLM_SETTINGS', {}).get('login_timeout', 60) * 1000
|
|
284
|
+
|
|
285
|
+
await self.page.goto(self.base_url, timeout=navigation_timeout)
|
|
286
|
+
|
|
287
|
+
# Wait for a selector that indicates the main interface is loaded
|
|
288
|
+
ready_selector = 'button[aria-label="Create new notebook"]';
|
|
289
|
+
await self.page.wait_for_selector(ready_selector, timeout=navigation_timeout)
|
|
290
|
+
|
|
291
|
+
# Check if we're on the correct page
|
|
292
|
+
current_url = self.page.url
|
|
293
|
+
if 'notebooklm.google.com' in current_url:
|
|
294
|
+
self.logger.info("β
Successfully navigated to NotebookLM")
|
|
295
|
+
return True
|
|
296
|
+
else:
|
|
297
|
+
self.logger.warning(f"β οΈ Unexpected URL: {current_url}")
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
self.logger.error(f"β Failed to navigate to NotebookLM: {e}")
|
|
302
|
+
if self.page:
|
|
303
|
+
screenshot_path = "failed_navigation_screenshot.png"
|
|
304
|
+
await self.page.screenshot(path=screenshot_path)
|
|
305
|
+
self.logger.info(f"πΈ Screenshot saved to {screenshot_path}")
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
async def check_authentication(self) -> bool:
|
|
309
|
+
"""
|
|
310
|
+
Check if user is authenticated with Google account.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
bool: True if authenticated, False otherwise
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
if not self.page:
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
# Look for user profile indicators first, as they are a stronger signal
|
|
320
|
+
profile_indicators = [
|
|
321
|
+
'button[aria-label*="Google Account"]', # More specific
|
|
322
|
+
'button[data-testid="user-menu"]',
|
|
323
|
+
'.user-avatar',
|
|
324
|
+
'[data-cy="user-menu"]'
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
for selector in profile_indicators:
|
|
328
|
+
try:
|
|
329
|
+
element = await self.page.wait_for_selector(selector, timeout=5000)
|
|
330
|
+
if element:
|
|
331
|
+
self.logger.info("β
User appears to be authenticated")
|
|
332
|
+
return True
|
|
333
|
+
except:
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
# If no profile indicators are found, then check for sign-in buttons
|
|
337
|
+
auth_indicators = [
|
|
338
|
+
'button[data-testid="sign-in"]',
|
|
339
|
+
'button:has-text("Sign in")'
|
|
340
|
+
# Removed 'a[href*="accounts.google.com"]' as it can be a false positive
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
for selector in auth_indicators:
|
|
344
|
+
try:
|
|
345
|
+
element = await self.page.wait_for_selector(selector, timeout=5000)
|
|
346
|
+
if element:
|
|
347
|
+
self.logger.warning("β οΈ Authentication required - user not signed in")
|
|
348
|
+
if self.page:
|
|
349
|
+
screenshot_path = "auth_failed_screenshot.png"
|
|
350
|
+
await self.page.screenshot(path=screenshot_path)
|
|
351
|
+
self.logger.info(f"πΈ Screenshot saved to {screenshot_path}")
|
|
352
|
+
return False
|
|
353
|
+
except:
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
self.logger.warning("β οΈ Authentication status unclear, assuming authenticated for now.")
|
|
357
|
+
# If neither profile nor sign-in indicators are found, it's ambiguous.
|
|
358
|
+
# Let's assume the user is logged in and let the next steps fail if they are not.
|
|
359
|
+
# This is better than getting stuck in a loop here.
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
except Exception as e:
|
|
363
|
+
self.logger.error(f"β Error checking authentication: {e}")
|
|
364
|
+
if self.page:
|
|
365
|
+
screenshot_path = "auth_error_screenshot.png"
|
|
366
|
+
await self.page.screenshot(path=screenshot_path)
|
|
367
|
+
self.logger.info(f"πΈ Screenshot saved to {screenshot_path}")
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
async def upload_document(self, file_path: str, notebook_id: str = None) -> Optional[str]:
|
|
371
|
+
"""
|
|
372
|
+
Upload a document to NotebookLM.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
file_path (str): Path to the document to upload
|
|
376
|
+
notebook_id (str): Optional. If provided, add source to this existing notebook.
|
|
377
|
+
If None, create a new notebook (legacy behavior).
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Optional[str]: Notebook ID where document was uploaded, or None if upload failed
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
if not self.page:
|
|
384
|
+
self.logger.error("β No browser page available")
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
if not os.path.exists(file_path):
|
|
388
|
+
self.logger.error(f"β File not found: {file_path}")
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
self.logger.info(f"π Uploading document: {file_path}")
|
|
392
|
+
|
|
393
|
+
current_notebook_id = notebook_id
|
|
394
|
+
|
|
395
|
+
# If notebook_id provided, navigate to that notebook
|
|
396
|
+
if notebook_id:
|
|
397
|
+
self.logger.info(f"π Navigating to existing notebook: {notebook_id}")
|
|
398
|
+
if not await self.navigate_to_notebook(notebook_id=notebook_id):
|
|
399
|
+
self.logger.error(f"β Failed to navigate to notebook {notebook_id}")
|
|
400
|
+
return None
|
|
401
|
+
else:
|
|
402
|
+
# Check if we are on the main page by looking for "Recent notebooks"
|
|
403
|
+
try:
|
|
404
|
+
recent_notebooks_header = await self.page.is_visible('h2:has-text("Recent notebooks")')
|
|
405
|
+
except:
|
|
406
|
+
recent_notebooks_header = False
|
|
407
|
+
|
|
408
|
+
if recent_notebooks_header:
|
|
409
|
+
try:
|
|
410
|
+
self.logger.info("π On main page, creating a new notebook...")
|
|
411
|
+
|
|
412
|
+
# Use create_notebook() to capture notebook ID
|
|
413
|
+
notebook_data = await self.create_notebook()
|
|
414
|
+
if not notebook_data:
|
|
415
|
+
self.logger.error("β Failed to create new notebook")
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
current_notebook_id = notebook_data['id']
|
|
419
|
+
self.logger.info(f"β
New notebook created with ID: {current_notebook_id}")
|
|
420
|
+
|
|
421
|
+
except Exception as e:
|
|
422
|
+
self.logger.error(f"β Failed to create a new notebook: {e}")
|
|
423
|
+
if self.page:
|
|
424
|
+
await self.page.screenshot(path="create_notebook_failed.png")
|
|
425
|
+
self.logger.info("πΈ Screenshot saved to create_notebook_failed.png")
|
|
426
|
+
return None
|
|
427
|
+
else:
|
|
428
|
+
# Already in a notebook, try to extract ID from URL
|
|
429
|
+
current_url = self.page.url
|
|
430
|
+
if '/notebook/' in current_url:
|
|
431
|
+
parts = current_url.split('/notebook/')
|
|
432
|
+
if len(parts) > 1:
|
|
433
|
+
current_notebook_id = parts[1].split('?')[0].split('#')[0].split('/')[0]
|
|
434
|
+
self.logger.info(f"π Already in notebook: {current_notebook_id}")
|
|
435
|
+
else:
|
|
436
|
+
self.logger.warning("β οΈ Not on main page and not in a notebook, URL: {current_url}")
|
|
437
|
+
|
|
438
|
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
439
|
+
# SOURCES TAB NAVIGATION
|
|
440
|
+
# β οΈ Jerry: After first upload, NotebookLM switches to Chat tab
|
|
441
|
+
# We need to navigate back to Sources tab to find upload button
|
|
442
|
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
443
|
+
|
|
444
|
+
# Find Sources tab
|
|
445
|
+
sources_tab_selector = 'div[role="tab"]:has-text("Sources")'
|
|
446
|
+
try:
|
|
447
|
+
sources_tab = await self.page.wait_for_selector(sources_tab_selector, timeout=5000)
|
|
448
|
+
if sources_tab:
|
|
449
|
+
# Check if already active
|
|
450
|
+
is_active = await sources_tab.get_attribute('aria-selected')
|
|
451
|
+
if is_active != 'true':
|
|
452
|
+
self.logger.info("π Switching to Sources tab...")
|
|
453
|
+
await sources_tab.click()
|
|
454
|
+
await self.page.wait_for_timeout(500) # Wait for tab switch animation
|
|
455
|
+
self.logger.info("β
On Sources tab")
|
|
456
|
+
else:
|
|
457
|
+
self.logger.info("β
Already on Sources tab")
|
|
458
|
+
except Exception as e:
|
|
459
|
+
self.logger.warning(f"β οΈ Could not find Sources tab: {e}")
|
|
460
|
+
# Continue anyway - might already be on Sources tab
|
|
461
|
+
|
|
462
|
+
# Check if notebook already has sources - if so, click "+ Add" button first
|
|
463
|
+
# β οΈ Jerry: When sources exist, need to click Add button to show upload options
|
|
464
|
+
add_button_selectors = [
|
|
465
|
+
'button.add-source-button', # Specific class
|
|
466
|
+
'button[aria-label="Add source"]', # Exact aria-label
|
|
467
|
+
'button[mattooltip="Add source"]', # Mat tooltip
|
|
468
|
+
'button[mat-stroked-button]:has-text("Add")', # Mat stroked button with Add text
|
|
469
|
+
'button:has-text("Add")', # Fallback
|
|
470
|
+
]
|
|
471
|
+
|
|
472
|
+
for selector in add_button_selectors:
|
|
473
|
+
try:
|
|
474
|
+
add_button = await self.page.wait_for_selector(selector, timeout=2000)
|
|
475
|
+
if add_button:
|
|
476
|
+
is_visible = await add_button.is_visible()
|
|
477
|
+
if is_visible:
|
|
478
|
+
self.logger.info("β Clicking Add button to show source options...")
|
|
479
|
+
await add_button.click()
|
|
480
|
+
await self.page.wait_for_timeout(1000)
|
|
481
|
+
break
|
|
482
|
+
except:
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
# Now we should be inside a notebook on Sources tab, look for the upload button.
|
|
486
|
+
# β οΈ Nyro: Real NotebookLM upload button selector from Jerry β‘
|
|
487
|
+
upload_selectors = [
|
|
488
|
+
'button[xapscottyuploadertrigger]', # Primary upload trigger
|
|
489
|
+
'button[aria-label="Upload sources from your computer"]', # Upload button aria label
|
|
490
|
+
'mat-card.create-new-action-button', # Legacy selector
|
|
491
|
+
'button:has-text("Upload sources")', # Upload dialog button
|
|
492
|
+
'button:has-text("Add source")', # Alternative text
|
|
493
|
+
'mat-chip:has-text("Upload")', # Upload chip after Add button
|
|
494
|
+
'input[type="file"]', # Direct file input
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
upload_element = None
|
|
498
|
+
for selector in upload_selectors:
|
|
499
|
+
try:
|
|
500
|
+
# Use a longer timeout for finding the upload element
|
|
501
|
+
element = await self.page.wait_for_selector(selector, timeout=30000)
|
|
502
|
+
if element:
|
|
503
|
+
upload_element = element
|
|
504
|
+
break
|
|
505
|
+
except:
|
|
506
|
+
continue
|
|
507
|
+
|
|
508
|
+
if not upload_element:
|
|
509
|
+
self.logger.error("β Could not find upload element")
|
|
510
|
+
if self.page:
|
|
511
|
+
await self.page.screenshot(path="upload_element_not_found.png")
|
|
512
|
+
self.logger.info("πΈ Screenshot saved to upload_element_not_found.png")
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
self.logger.info(f"Found upload element: {upload_element}")
|
|
516
|
+
upload_element_tag_name = await upload_element.evaluate('el => el.tagName')
|
|
517
|
+
self.logger.info(f"Upload element tag name: {upload_element_tag_name}")
|
|
518
|
+
|
|
519
|
+
# Handle file input
|
|
520
|
+
if upload_element_tag_name == 'INPUT':
|
|
521
|
+
# Direct file input element
|
|
522
|
+
await upload_element.set_input_files(file_path)
|
|
523
|
+
else:
|
|
524
|
+
# Click upload button to activate file dialog
|
|
525
|
+
await upload_element.click()
|
|
526
|
+
await self.page.wait_for_timeout(1000)
|
|
527
|
+
|
|
528
|
+
# Find hidden file input (NotebookLM uses hidden input with aria-hidden="true")
|
|
529
|
+
# Don't wait for visibility - set files directly on hidden input
|
|
530
|
+
file_input_selectors = [
|
|
531
|
+
'input[type="file"][name="Filedata"]', # NotebookLM specific
|
|
532
|
+
'input[type="file"]', # Generic fallback
|
|
533
|
+
]
|
|
534
|
+
|
|
535
|
+
file_input = None
|
|
536
|
+
for selector in file_input_selectors:
|
|
537
|
+
try:
|
|
538
|
+
# Use query_selector to get element even if hidden
|
|
539
|
+
file_input = await self.page.query_selector(selector)
|
|
540
|
+
if file_input:
|
|
541
|
+
self.logger.info(f"β
Found file input: {selector}")
|
|
542
|
+
break
|
|
543
|
+
except:
|
|
544
|
+
continue
|
|
545
|
+
|
|
546
|
+
if file_input:
|
|
547
|
+
# Set files on hidden input directly
|
|
548
|
+
await file_input.set_input_files(file_path)
|
|
549
|
+
else:
|
|
550
|
+
self.logger.error("β Could not find file input element")
|
|
551
|
+
return None
|
|
552
|
+
|
|
553
|
+
# Wait for upload to complete
|
|
554
|
+
await self.page.wait_for_timeout(5000)
|
|
555
|
+
|
|
556
|
+
self.logger.info("β
Document upload completed")
|
|
557
|
+
self.logger.info(f"π Uploaded to notebook: {current_notebook_id}")
|
|
558
|
+
return current_notebook_id
|
|
559
|
+
|
|
560
|
+
except Exception as e:
|
|
561
|
+
self.logger.error(f"β Failed to upload document: {e}")
|
|
562
|
+
return None
|
|
563
|
+
|
|
564
|
+
async def add_url_source(self, url: str, notebook_id: str = None) -> Optional[str]:
|
|
565
|
+
"""
|
|
566
|
+
Add a URL source (website, YouTube, etc.) to NotebookLM.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
url (str): URL to add as source (SimExp session, website, YouTube, etc.)
|
|
570
|
+
notebook_id (str): Optional. If provided, add source to this existing notebook.
|
|
571
|
+
If None, create a new notebook.
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
Optional[str]: Notebook ID where URL was added, or None if add failed
|
|
575
|
+
"""
|
|
576
|
+
try:
|
|
577
|
+
if not self.page:
|
|
578
|
+
self.logger.error("β No browser page available")
|
|
579
|
+
return None
|
|
580
|
+
|
|
581
|
+
self.logger.info(f"π Adding URL source: {url}")
|
|
582
|
+
|
|
583
|
+
current_notebook_id = notebook_id
|
|
584
|
+
|
|
585
|
+
# Navigate to notebook or create new one (same logic as file upload)
|
|
586
|
+
if notebook_id:
|
|
587
|
+
self.logger.info(f"π Navigating to existing notebook: {notebook_id}")
|
|
588
|
+
if not await self.navigate_to_notebook(notebook_id=notebook_id):
|
|
589
|
+
self.logger.error(f"β Failed to navigate to notebook {notebook_id}")
|
|
590
|
+
return None
|
|
591
|
+
else:
|
|
592
|
+
# Check if on main page or in notebook
|
|
593
|
+
try:
|
|
594
|
+
recent_notebooks_header = await self.page.is_visible('h2:has-text("Recent notebooks")')
|
|
595
|
+
except:
|
|
596
|
+
recent_notebooks_header = False
|
|
597
|
+
|
|
598
|
+
if recent_notebooks_header:
|
|
599
|
+
# Create new notebook
|
|
600
|
+
notebook_data = await self.create_notebook()
|
|
601
|
+
if not notebook_data:
|
|
602
|
+
self.logger.error("β Failed to create new notebook")
|
|
603
|
+
return None
|
|
604
|
+
current_notebook_id = notebook_data['id']
|
|
605
|
+
else:
|
|
606
|
+
# Extract ID from current URL
|
|
607
|
+
current_url = self.page.url
|
|
608
|
+
if '/notebook/' in current_url:
|
|
609
|
+
parts = current_url.split('/notebook/')
|
|
610
|
+
if len(parts) > 1:
|
|
611
|
+
current_notebook_id = parts[1].split('?')[0].split('#')[0].split('/')[0]
|
|
612
|
+
|
|
613
|
+
# Check if "Add sources" dialog is already open (happens with new notebooks)
|
|
614
|
+
# If so, we can skip navigating to Sources tab
|
|
615
|
+
dialog_already_open = False
|
|
616
|
+
try:
|
|
617
|
+
existing_dialog = await self.page.query_selector('.cdk-overlay-pane')
|
|
618
|
+
if existing_dialog:
|
|
619
|
+
is_visible = await existing_dialog.is_visible()
|
|
620
|
+
if is_visible:
|
|
621
|
+
self.logger.info("β
Add sources dialog already open, skipping Sources tab navigation")
|
|
622
|
+
dialog_already_open = True
|
|
623
|
+
except:
|
|
624
|
+
pass
|
|
625
|
+
|
|
626
|
+
# Navigate to Sources tab only if dialog is not already open
|
|
627
|
+
if not dialog_already_open:
|
|
628
|
+
sources_tab_selector = 'div[role="tab"]:has-text("Sources")'
|
|
629
|
+
try:
|
|
630
|
+
sources_tab = await self.page.wait_for_selector(sources_tab_selector, timeout=5000)
|
|
631
|
+
if sources_tab:
|
|
632
|
+
is_active = await sources_tab.get_attribute('aria-selected')
|
|
633
|
+
if is_active != 'true':
|
|
634
|
+
self.logger.info("π Switching to Sources tab...")
|
|
635
|
+
await sources_tab.click()
|
|
636
|
+
await self.page.wait_for_timeout(500)
|
|
637
|
+
except Exception as e:
|
|
638
|
+
self.logger.warning(f"β οΈ Could not find Sources tab: {e}")
|
|
639
|
+
|
|
640
|
+
# Check if notebook already has sources - if so, click "+ Add" button first
|
|
641
|
+
# β οΈ Jerry: When sources exist, need to click Add button to show upload options
|
|
642
|
+
add_button_selectors = [
|
|
643
|
+
'button:has-text("Add")',
|
|
644
|
+
'button[aria-label*="Add"]',
|
|
645
|
+
'button:has-text("+ Add")'
|
|
646
|
+
]
|
|
647
|
+
|
|
648
|
+
for selector in add_button_selectors:
|
|
649
|
+
try:
|
|
650
|
+
add_button = await self.page.wait_for_selector(selector, timeout=2000)
|
|
651
|
+
if add_button:
|
|
652
|
+
is_visible = await add_button.is_visible()
|
|
653
|
+
if is_visible:
|
|
654
|
+
self.logger.info("β Clicking Add button to show source options...")
|
|
655
|
+
await add_button.click()
|
|
656
|
+
await self.page.wait_for_timeout(1000)
|
|
657
|
+
break
|
|
658
|
+
except:
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
# Detect URL type and select appropriate chip
|
|
662
|
+
# β οΈ Jerry: YouTube URLs need YouTube chip, others need Website chip
|
|
663
|
+
is_youtube = 'youtube.com' in url.lower() or 'youtu.be' in url.lower()
|
|
664
|
+
|
|
665
|
+
if is_youtube:
|
|
666
|
+
chip_type = "YouTube"
|
|
667
|
+
chip_selectors = [
|
|
668
|
+
'mat-chip:has-text("YouTube")',
|
|
669
|
+
'button:has-text("YouTube")',
|
|
670
|
+
'mat-chip:has(mat-icon:has-text("video_youtube"))',
|
|
671
|
+
'[aria-label*="YouTube"]'
|
|
672
|
+
]
|
|
673
|
+
else:
|
|
674
|
+
chip_type = "Website"
|
|
675
|
+
chip_selectors = [
|
|
676
|
+
'mat-chip:has-text("Website")',
|
|
677
|
+
'button:has-text("Website")',
|
|
678
|
+
'[aria-label*="Website"]'
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
self.logger.info(f"π Looking for {chip_type} chip...")
|
|
682
|
+
source_chip = None
|
|
683
|
+
for selector in chip_selectors:
|
|
684
|
+
try:
|
|
685
|
+
element = await self.page.wait_for_selector(selector, timeout=5000)
|
|
686
|
+
if element:
|
|
687
|
+
source_chip = element
|
|
688
|
+
self.logger.info(f"β
Found {chip_type} chip: {selector}")
|
|
689
|
+
break
|
|
690
|
+
except:
|
|
691
|
+
continue
|
|
692
|
+
|
|
693
|
+
if not source_chip:
|
|
694
|
+
self.logger.error(f"β Could not find {chip_type} chip")
|
|
695
|
+
return None
|
|
696
|
+
|
|
697
|
+
# Click the appropriate chip
|
|
698
|
+
self.logger.info(f"π±οΈ Clicking {chip_type} chip...")
|
|
699
|
+
await source_chip.click()
|
|
700
|
+
|
|
701
|
+
# Wait for dialog/modal to appear
|
|
702
|
+
self.logger.info("β³ Waiting for URL dialog to appear...")
|
|
703
|
+
dialog_appeared = False
|
|
704
|
+
dialog_selectors = [
|
|
705
|
+
'div[role="dialog"]',
|
|
706
|
+
'.cdk-overlay-pane',
|
|
707
|
+
'.mat-dialog-container',
|
|
708
|
+
'mat-dialog-container'
|
|
709
|
+
]
|
|
710
|
+
|
|
711
|
+
dialog = None
|
|
712
|
+
for selector in dialog_selectors:
|
|
713
|
+
try:
|
|
714
|
+
dialog = await self.page.wait_for_selector(selector, timeout=10000, state='visible')
|
|
715
|
+
if dialog:
|
|
716
|
+
dialog_appeared = True
|
|
717
|
+
self.logger.info(f"β
Dialog appeared: {selector}")
|
|
718
|
+
break
|
|
719
|
+
except:
|
|
720
|
+
continue
|
|
721
|
+
|
|
722
|
+
if not dialog_appeared:
|
|
723
|
+
self.logger.warning("β οΈ Dialog did not appear, will try to find input anyway...")
|
|
724
|
+
await self.page.wait_for_timeout(3000) # Wait a bit more
|
|
725
|
+
|
|
726
|
+
# Find URL input field - search more broadly including within dialog
|
|
727
|
+
self.logger.info("π Looking for URL input field...")
|
|
728
|
+
url_input_selectors = [
|
|
729
|
+
# NotebookLM uses a textarea for URL input!
|
|
730
|
+
'.cdk-overlay-pane textarea',
|
|
731
|
+
'div[role="dialog"] textarea',
|
|
732
|
+
'textarea[formcontrolname="newUrl"]',
|
|
733
|
+
'textarea#mat-input-0',
|
|
734
|
+
'textarea.text-area',
|
|
735
|
+
# Fallback to inputs
|
|
736
|
+
'.cdk-overlay-pane input',
|
|
737
|
+
'div[role="dialog"] input',
|
|
738
|
+
'.mat-dialog-container input',
|
|
739
|
+
# Then try specific attributes
|
|
740
|
+
'input[placeholder*="URL"]',
|
|
741
|
+
'textarea[placeholder*="URL"]',
|
|
742
|
+
'input[placeholder*="paste"]',
|
|
743
|
+
'textarea[placeholder*="paste"]',
|
|
744
|
+
]
|
|
745
|
+
|
|
746
|
+
url_input = None
|
|
747
|
+
for selector in url_input_selectors:
|
|
748
|
+
try:
|
|
749
|
+
elements = await self.page.query_selector_all(selector)
|
|
750
|
+
for element in elements:
|
|
751
|
+
try:
|
|
752
|
+
is_visible = await element.is_visible()
|
|
753
|
+
if is_visible:
|
|
754
|
+
# Check if it's in the dialog if we found one
|
|
755
|
+
if dialog_appeared and dialog:
|
|
756
|
+
# Try to see if this element is within the dialog
|
|
757
|
+
try:
|
|
758
|
+
bounding_box = await element.bounding_box()
|
|
759
|
+
if bounding_box:
|
|
760
|
+
url_input = element
|
|
761
|
+
self.logger.info(f"β
Found URL input in dialog: {selector}")
|
|
762
|
+
break
|
|
763
|
+
except:
|
|
764
|
+
pass
|
|
765
|
+
else:
|
|
766
|
+
# No dialog, just use first visible
|
|
767
|
+
url_input = element
|
|
768
|
+
self.logger.info(f"β
Found URL input: {selector}")
|
|
769
|
+
break
|
|
770
|
+
except:
|
|
771
|
+
continue
|
|
772
|
+
if url_input:
|
|
773
|
+
break
|
|
774
|
+
except:
|
|
775
|
+
continue
|
|
776
|
+
|
|
777
|
+
if not url_input:
|
|
778
|
+
self.logger.error("β Could not find URL input field")
|
|
779
|
+
# Save screenshot for debugging
|
|
780
|
+
await self.page.screenshot(path="debug/url_input_not_found.png")
|
|
781
|
+
self.logger.info("πΈ Screenshot saved: debug/url_input_not_found.png")
|
|
782
|
+
return None
|
|
783
|
+
|
|
784
|
+
# Enter URL
|
|
785
|
+
self.logger.info(f"β¨οΈ Typing URL: {url}")
|
|
786
|
+
await url_input.click()
|
|
787
|
+
await self.page.wait_for_timeout(500)
|
|
788
|
+
await url_input.fill(url) # Use fill instead of keyboard.type - it's faster and more reliable
|
|
789
|
+
await self.page.wait_for_timeout(1000)
|
|
790
|
+
|
|
791
|
+
# Click Insert button instead of pressing Enter
|
|
792
|
+
self.logger.info("π Looking for Insert button...")
|
|
793
|
+
insert_button_selectors = [
|
|
794
|
+
'button:has-text("Insert")',
|
|
795
|
+
'button.mdc-button:has-text("Insert")',
|
|
796
|
+
'.cdk-overlay-pane button:has-text("Insert")',
|
|
797
|
+
'button[type="submit"]',
|
|
798
|
+
'button:has(.mdc-button__label:has-text("Insert"))'
|
|
799
|
+
]
|
|
800
|
+
|
|
801
|
+
insert_button = None
|
|
802
|
+
for selector in insert_button_selectors:
|
|
803
|
+
try:
|
|
804
|
+
element = await self.page.wait_for_selector(selector, timeout=5000)
|
|
805
|
+
if element:
|
|
806
|
+
is_visible = await element.is_visible()
|
|
807
|
+
if is_visible:
|
|
808
|
+
insert_button = element
|
|
809
|
+
self.logger.info(f"β
Found Insert button: {selector}")
|
|
810
|
+
break
|
|
811
|
+
except:
|
|
812
|
+
continue
|
|
813
|
+
|
|
814
|
+
if insert_button:
|
|
815
|
+
self.logger.info("π±οΈ Clicking Insert button...")
|
|
816
|
+
await insert_button.click()
|
|
817
|
+
await self.page.wait_for_timeout(5000) # Wait for URL to be processed
|
|
818
|
+
else:
|
|
819
|
+
# Fallback: press Enter if Insert button not found
|
|
820
|
+
self.logger.warning("β οΈ Insert button not found, trying Enter key...")
|
|
821
|
+
await self.page.keyboard.press('Enter')
|
|
822
|
+
await self.page.wait_for_timeout(5000)
|
|
823
|
+
|
|
824
|
+
self.logger.info("β
URL source added successfully")
|
|
825
|
+
self.logger.info(f"π Added to notebook: {current_notebook_id}")
|
|
826
|
+
return current_notebook_id
|
|
827
|
+
|
|
828
|
+
except Exception as e:
|
|
829
|
+
self.logger.error(f"β Failed to add URL source: {e}")
|
|
830
|
+
return None
|
|
831
|
+
|
|
832
|
+
async def add_source(self, source: str, notebook_id: str = None) -> Optional[str]:
|
|
833
|
+
"""
|
|
834
|
+
Smart source addition - automatically detects source type and routes appropriately.
|
|
835
|
+
|
|
836
|
+
This is the recommended high-level method for adding any source to NotebookLM.
|
|
837
|
+
It intelligently detects whether the source is a URL or file path and calls
|
|
838
|
+
the appropriate underlying method.
|
|
839
|
+
|
|
840
|
+
Args:
|
|
841
|
+
source (str): Source to add - can be:
|
|
842
|
+
- URL (http://..., https://...)
|
|
843
|
+
- File path (relative or absolute)
|
|
844
|
+
notebook_id (str): Optional. If provided, add source to this existing notebook.
|
|
845
|
+
If None, create a new notebook.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
Optional[str]: Notebook ID where source was added, or None if add failed
|
|
849
|
+
|
|
850
|
+
Examples:
|
|
851
|
+
# Add URL source
|
|
852
|
+
await automator.add_source("https://example.com/article")
|
|
853
|
+
|
|
854
|
+
# Add file source
|
|
855
|
+
await automator.add_source("/path/to/document.pdf")
|
|
856
|
+
|
|
857
|
+
# Add to existing notebook
|
|
858
|
+
await automator.add_source("research.pdf", notebook_id="abc123")
|
|
859
|
+
"""
|
|
860
|
+
try:
|
|
861
|
+
# Detect source type by checking for URL prefix
|
|
862
|
+
if source.startswith(('http://', 'https://')):
|
|
863
|
+
self.logger.info(f"π Detected URL source: {source}")
|
|
864
|
+
return await self.add_url_source(source, notebook_id)
|
|
865
|
+
else:
|
|
866
|
+
self.logger.info(f"π Detected file source: {source}")
|
|
867
|
+
return await self.upload_document(source, notebook_id)
|
|
868
|
+
|
|
869
|
+
except Exception as e:
|
|
870
|
+
self.logger.error(f"β Failed to add source: {e}")
|
|
871
|
+
return None
|
|
872
|
+
|
|
873
|
+
async def generate_audio_overview(self, title: str = "Generated Podcast") -> bool:
|
|
874
|
+
"""
|
|
875
|
+
Generate an Audio Overview (podcast) from uploaded documents.
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
title (str): Title for the generated podcast
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
bool: True if generation successful, False otherwise
|
|
882
|
+
"""
|
|
883
|
+
try:
|
|
884
|
+
if not self.page:
|
|
885
|
+
self.logger.error("β No browser page available")
|
|
886
|
+
return False
|
|
887
|
+
|
|
888
|
+
self.logger.info(f"ποΈ Generating Audio Overview: {title}")
|
|
889
|
+
|
|
890
|
+
# Look for Audio Overview button
|
|
891
|
+
audio_selectors = [
|
|
892
|
+
'button:has-text("Audio Overview")',
|
|
893
|
+
'button:has-text("Generate Audio")',
|
|
894
|
+
'button:has-text("Create Podcast")',
|
|
895
|
+
'[data-testid="audio-overview"]',
|
|
896
|
+
'.audio-overview-button'
|
|
897
|
+
]
|
|
898
|
+
|
|
899
|
+
audio_button = None
|
|
900
|
+
for selector in audio_selectors:
|
|
901
|
+
try:
|
|
902
|
+
element = await self.page.wait_for_selector(selector, timeout=10000)
|
|
903
|
+
if element:
|
|
904
|
+
audio_button = element
|
|
905
|
+
break
|
|
906
|
+
except:
|
|
907
|
+
continue
|
|
908
|
+
|
|
909
|
+
if not audio_button:
|
|
910
|
+
self.logger.error("β Could not find Audio Overview button")
|
|
911
|
+
return False
|
|
912
|
+
|
|
913
|
+
# Click to start generation
|
|
914
|
+
await audio_button.click()
|
|
915
|
+
self.logger.info("π Audio Overview generation started...")
|
|
916
|
+
|
|
917
|
+
# Wait for generation to complete
|
|
918
|
+
# This timeout should be adjusted based on actual generation time
|
|
919
|
+
generation_timeout = self.config.get('NOTEBOOKLM_SETTINGS', {}).get('generation_timeout', 300000)
|
|
920
|
+
await self.page.wait_for_timeout(generation_timeout)
|
|
921
|
+
|
|
922
|
+
self.logger.info("β
Audio Overview generation completed")
|
|
923
|
+
return True
|
|
924
|
+
|
|
925
|
+
except Exception as e:
|
|
926
|
+
self.logger.error(f"β Failed to generate Audio Overview: {e}")
|
|
927
|
+
return False
|
|
928
|
+
|
|
929
|
+
async def download_audio(self, output_path: str) -> bool:
|
|
930
|
+
"""
|
|
931
|
+
Download the generated audio file.
|
|
932
|
+
|
|
933
|
+
Args:
|
|
934
|
+
output_path (str): Path where to save the audio file
|
|
935
|
+
|
|
936
|
+
Returns:
|
|
937
|
+
bool: True if download successful, False otherwise
|
|
938
|
+
"""
|
|
939
|
+
try:
|
|
940
|
+
if not self.page:
|
|
941
|
+
self.logger.error("β No browser page available")
|
|
942
|
+
return False
|
|
943
|
+
|
|
944
|
+
self.logger.info(f"β¬οΈ Downloading audio to: {output_path}")
|
|
945
|
+
|
|
946
|
+
# Look for download button
|
|
947
|
+
download_selectors = [
|
|
948
|
+
'button:has-text("Download")',
|
|
949
|
+
'button:has-text("Save")',
|
|
950
|
+
'a[download]',
|
|
951
|
+
'[data-testid="download-button"]',
|
|
952
|
+
'.download-button'
|
|
953
|
+
]
|
|
954
|
+
|
|
955
|
+
download_button = None
|
|
956
|
+
for selector in download_selectors:
|
|
957
|
+
try:
|
|
958
|
+
element = await self.page.wait_for_selector(selector, timeout=10000)
|
|
959
|
+
if element:
|
|
960
|
+
download_button = element
|
|
961
|
+
break
|
|
962
|
+
except:
|
|
963
|
+
continue
|
|
964
|
+
|
|
965
|
+
if not download_button:
|
|
966
|
+
self.logger.error("β Could not find download button")
|
|
967
|
+
return False
|
|
968
|
+
|
|
969
|
+
# Set up download handling
|
|
970
|
+
async with self.page.expect_download() as download_info:
|
|
971
|
+
await download_button.click()
|
|
972
|
+
|
|
973
|
+
download = await download_info.value
|
|
974
|
+
await download.save_as(output_path)
|
|
975
|
+
|
|
976
|
+
self.logger.info("β
Audio download completed")
|
|
977
|
+
return True
|
|
978
|
+
|
|
979
|
+
except Exception as e:
|
|
980
|
+
self.logger.error(f"β Failed to download audio: {e}")
|
|
981
|
+
return False
|
|
982
|
+
|
|
983
|
+
async def create_notebook(self) -> Optional[Dict[str, Any]]:
|
|
984
|
+
"""
|
|
985
|
+
Create a new notebook and capture its identity.
|
|
986
|
+
|
|
987
|
+
Returns:
|
|
988
|
+
Optional[Dict[str, Any]]: Notebook metadata including id, url, and created_at
|
|
989
|
+
Returns None if creation fails
|
|
990
|
+
"""
|
|
991
|
+
try:
|
|
992
|
+
if not self.page:
|
|
993
|
+
self.logger.error("β No browser page available")
|
|
994
|
+
return None
|
|
995
|
+
|
|
996
|
+
# Store the current URL to detect navigation
|
|
997
|
+
initial_url = self.page.url
|
|
998
|
+
|
|
999
|
+
# Multi-selector strategy for create button
|
|
1000
|
+
create_selectors = [
|
|
1001
|
+
'button[aria-label="Create new notebook"]',
|
|
1002
|
+
'button:has-text("Create new notebook")',
|
|
1003
|
+
'button:has-text("New notebook")',
|
|
1004
|
+
'[data-testid="create-notebook"]',
|
|
1005
|
+
'button.create-notebook-btn'
|
|
1006
|
+
]
|
|
1007
|
+
|
|
1008
|
+
create_button = None
|
|
1009
|
+
for selector in create_selectors:
|
|
1010
|
+
try:
|
|
1011
|
+
element = await self.page.wait_for_selector(selector, timeout=5000)
|
|
1012
|
+
if element:
|
|
1013
|
+
create_button = element
|
|
1014
|
+
self.logger.info(f"β
Found create button: {selector}")
|
|
1015
|
+
break
|
|
1016
|
+
except:
|
|
1017
|
+
continue
|
|
1018
|
+
|
|
1019
|
+
if not create_button:
|
|
1020
|
+
self.logger.error("β Could not find 'Create new notebook' button")
|
|
1021
|
+
screenshot_path = "debug/create_button_not_found.png"
|
|
1022
|
+
await self.page.screenshot(path=screenshot_path)
|
|
1023
|
+
self.logger.info(f"πΈ Screenshot saved to {screenshot_path}")
|
|
1024
|
+
return None
|
|
1025
|
+
|
|
1026
|
+
self.logger.info("π Creating new notebook...")
|
|
1027
|
+
await create_button.click()
|
|
1028
|
+
|
|
1029
|
+
# Wait for navigation and page load (using load instead of networkidle for better reliability)
|
|
1030
|
+
try:
|
|
1031
|
+
await self.page.wait_for_load_state('load', timeout=15000)
|
|
1032
|
+
except:
|
|
1033
|
+
# If load state times out, continue anyway - the navigation might still have worked
|
|
1034
|
+
self.logger.warning("β οΈ Load state timeout, but continuing...")
|
|
1035
|
+
pass
|
|
1036
|
+
|
|
1037
|
+
# Get the new URL
|
|
1038
|
+
new_url = self.page.url
|
|
1039
|
+
|
|
1040
|
+
# Verify we navigated away from the initial URL
|
|
1041
|
+
if new_url == initial_url:
|
|
1042
|
+
self.logger.warning("β οΈ URL did not change after clicking create button")
|
|
1043
|
+
# Wait a bit more and try again
|
|
1044
|
+
await asyncio.sleep(2)
|
|
1045
|
+
new_url = self.page.url
|
|
1046
|
+
|
|
1047
|
+
# Extract notebook ID from URL
|
|
1048
|
+
# Expected format: https://notebooklm.google.com/notebook/{notebook_id}
|
|
1049
|
+
notebook_id = None
|
|
1050
|
+
if '/notebook/' in new_url:
|
|
1051
|
+
parts = new_url.split('/notebook/')
|
|
1052
|
+
if len(parts) > 1:
|
|
1053
|
+
# Get the ID (might have query params, so split on ? first)
|
|
1054
|
+
notebook_id = parts[1].split('?')[0].split('#')[0]
|
|
1055
|
+
|
|
1056
|
+
if not notebook_id:
|
|
1057
|
+
self.logger.warning("β οΈ Could not extract notebook ID from URL")
|
|
1058
|
+
self.logger.info(f"Current URL: {new_url}")
|
|
1059
|
+
# Try alternative extraction methods
|
|
1060
|
+
# Some URLs might be like: /notebook/abc123/sources or /notebook/abc123/overview
|
|
1061
|
+
if '/notebook/' in new_url:
|
|
1062
|
+
path_parts = new_url.split('/')
|
|
1063
|
+
notebook_idx = path_parts.index('notebook')
|
|
1064
|
+
if len(path_parts) > notebook_idx + 1:
|
|
1065
|
+
notebook_id = path_parts[notebook_idx + 1]
|
|
1066
|
+
|
|
1067
|
+
# Wait for notebook UI to be ready
|
|
1068
|
+
try:
|
|
1069
|
+
await self.page.wait_for_selector('mat-card.create-new-action-button', timeout=15000)
|
|
1070
|
+
self.logger.info("β
Notebook UI loaded successfully")
|
|
1071
|
+
except:
|
|
1072
|
+
self.logger.warning("β οΈ Notebook UI selector not found, but continuing...")
|
|
1073
|
+
|
|
1074
|
+
# Create metadata object
|
|
1075
|
+
from datetime import datetime
|
|
1076
|
+
notebook_data = {
|
|
1077
|
+
'id': notebook_id or 'unknown',
|
|
1078
|
+
'url': new_url,
|
|
1079
|
+
'created_at': datetime.now().isoformat(),
|
|
1080
|
+
'title': 'Untitled Notebook', # Can be updated later
|
|
1081
|
+
'sources': [],
|
|
1082
|
+
'active': True
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
self.logger.info(f"β
Notebook created successfully!")
|
|
1086
|
+
self.logger.info(f"π Notebook ID: {notebook_data['id']}")
|
|
1087
|
+
self.logger.info(f"π Notebook URL: {notebook_data['url']}")
|
|
1088
|
+
|
|
1089
|
+
return notebook_data
|
|
1090
|
+
|
|
1091
|
+
except Exception as e:
|
|
1092
|
+
self.logger.error(f"β Failed to create notebook: {e}")
|
|
1093
|
+
if self.page:
|
|
1094
|
+
try:
|
|
1095
|
+
screenshot_path = "debug/create_notebook_error.png"
|
|
1096
|
+
await self.page.screenshot(path=screenshot_path, timeout=5000)
|
|
1097
|
+
self.logger.info(f"πΈ Screenshot saved to {screenshot_path}")
|
|
1098
|
+
except:
|
|
1099
|
+
self.logger.warning("β οΈ Could not save screenshot")
|
|
1100
|
+
return None
|
|
1101
|
+
|
|
1102
|
+
async def navigate_to_notebook(self, notebook_id: str = None, notebook_url: str = None) -> bool:
|
|
1103
|
+
"""
|
|
1104
|
+
Navigate to an existing notebook by ID or URL.
|
|
1105
|
+
|
|
1106
|
+
Args:
|
|
1107
|
+
notebook_id (str): The notebook ID to navigate to
|
|
1108
|
+
notebook_url (str): The full notebook URL (alternative to notebook_id)
|
|
1109
|
+
|
|
1110
|
+
Returns:
|
|
1111
|
+
bool: True if navigation successful, False otherwise
|
|
1112
|
+
"""
|
|
1113
|
+
try:
|
|
1114
|
+
if not self.page:
|
|
1115
|
+
self.logger.error("β No browser page available")
|
|
1116
|
+
return False
|
|
1117
|
+
|
|
1118
|
+
# Construct URL if only ID is provided
|
|
1119
|
+
target_url = notebook_url
|
|
1120
|
+
if not target_url and notebook_id:
|
|
1121
|
+
target_url = f"{self.base_url}/notebook/{notebook_id}"
|
|
1122
|
+
|
|
1123
|
+
if not target_url:
|
|
1124
|
+
self.logger.error("β Must provide either notebook_id or notebook_url")
|
|
1125
|
+
return False
|
|
1126
|
+
|
|
1127
|
+
self.logger.info(f"π Navigating to notebook: {target_url}")
|
|
1128
|
+
|
|
1129
|
+
# Navigate to the notebook URL
|
|
1130
|
+
await self.page.goto(target_url, timeout=30000)
|
|
1131
|
+
|
|
1132
|
+
# Wait for load state (use 'load' instead of 'networkidle' - networkidle can hang with background polling)
|
|
1133
|
+
try:
|
|
1134
|
+
await self.page.wait_for_load_state('load', timeout=10000)
|
|
1135
|
+
except:
|
|
1136
|
+
# If load state times out, continue anyway - URL check below will verify
|
|
1137
|
+
self.logger.warning("β οΈ Load state timeout, but continuing with URL verification...")
|
|
1138
|
+
|
|
1139
|
+
# Verify notebook loaded successfully
|
|
1140
|
+
# First check if URL contains /notebook/ - most reliable indicator
|
|
1141
|
+
current_url = self.page.url
|
|
1142
|
+
if '/notebook/' in current_url:
|
|
1143
|
+
self.logger.info(f"β
Notebook URL verified: {current_url}")
|
|
1144
|
+
self.logger.info(f"β
Successfully navigated to notebook")
|
|
1145
|
+
return True
|
|
1146
|
+
|
|
1147
|
+
# Multi-selector strategy for notebook verification
|
|
1148
|
+
notebook_indicators = [
|
|
1149
|
+
'mat-card.create-new-action-button', # Sources panel
|
|
1150
|
+
'h2:has-text("Add sources")', # Add sources dialog header
|
|
1151
|
+
'div[role="dialog"]', # Any dialog (add sources)
|
|
1152
|
+
'button:has-text("Audio Overview")', # Audio Overview button
|
|
1153
|
+
'[data-testid="notebook-content"]', # Notebook content area
|
|
1154
|
+
'.notebook-title', # Notebook title
|
|
1155
|
+
'div.sources-panel' # Sources panel
|
|
1156
|
+
]
|
|
1157
|
+
|
|
1158
|
+
notebook_loaded = False
|
|
1159
|
+
for selector in notebook_indicators:
|
|
1160
|
+
try:
|
|
1161
|
+
element = await self.page.wait_for_selector(selector, timeout=10000)
|
|
1162
|
+
if element:
|
|
1163
|
+
self.logger.info(f"β
Notebook verified: {selector}")
|
|
1164
|
+
notebook_loaded = True
|
|
1165
|
+
break
|
|
1166
|
+
except:
|
|
1167
|
+
continue
|
|
1168
|
+
|
|
1169
|
+
if not notebook_loaded:
|
|
1170
|
+
self.logger.warning("β οΈ Could not verify notebook UI elements")
|
|
1171
|
+
screenshot_path = "debug/notebook_verification_failed.png"
|
|
1172
|
+
await self.page.screenshot(path=screenshot_path)
|
|
1173
|
+
self.logger.info(f"πΈ Screenshot saved to {screenshot_path}")
|
|
1174
|
+
# Don't fail completely - URL navigation might still have worked
|
|
1175
|
+
return True
|
|
1176
|
+
|
|
1177
|
+
current_url = self.page.url
|
|
1178
|
+
self.logger.info(f"β
Successfully navigated to notebook")
|
|
1179
|
+
self.logger.info(f"π Current URL: {current_url}")
|
|
1180
|
+
|
|
1181
|
+
return True
|
|
1182
|
+
|
|
1183
|
+
except Exception as e:
|
|
1184
|
+
self.logger.error(f"β Failed to navigate to notebook: {e}")
|
|
1185
|
+
if self.page:
|
|
1186
|
+
screenshot_path = "debug/navigate_notebook_error.png"
|
|
1187
|
+
await self.page.screenshot(path=screenshot_path)
|
|
1188
|
+
self.logger.info(f"πΈ Screenshot saved to {screenshot_path}")
|
|
1189
|
+
return False
|
|
1190
|
+
|
|
1191
|
+
async def share_notebook(self, email: str, role: str = 'editor') -> bool:
|
|
1192
|
+
"""
|
|
1193
|
+
Share the current notebook with a collaborator via email.
|
|
1194
|
+
|
|
1195
|
+
Args:
|
|
1196
|
+
email (str): Email address of the person to share with
|
|
1197
|
+
role (str): Role to grant ('editor' or 'viewer')
|
|
1198
|
+
|
|
1199
|
+
Returns:
|
|
1200
|
+
bool: True if sharing successful, False otherwise
|
|
1201
|
+
"""
|
|
1202
|
+
try:
|
|
1203
|
+
if not self.page:
|
|
1204
|
+
self.logger.error("β No browser page available")
|
|
1205
|
+
return False
|
|
1206
|
+
|
|
1207
|
+
self.logger.info(f"π₯ Sharing notebook with {email} as {role}...")
|
|
1208
|
+
|
|
1209
|
+
# Multi-selector strategy for share button
|
|
1210
|
+
share_button_selectors = [
|
|
1211
|
+
'button[aria-label="Share"]',
|
|
1212
|
+
'button:has-text("Share")',
|
|
1213
|
+
'button[title="Share"]',
|
|
1214
|
+
'[data-testid="share-button"]',
|
|
1215
|
+
'button.share-button'
|
|
1216
|
+
]
|
|
1217
|
+
|
|
1218
|
+
# Find and click share button
|
|
1219
|
+
share_button = None
|
|
1220
|
+
for selector in share_button_selectors:
|
|
1221
|
+
try:
|
|
1222
|
+
element = await self.page.wait_for_selector(selector, timeout=5000)
|
|
1223
|
+
if element:
|
|
1224
|
+
share_button = element
|
|
1225
|
+
self.logger.info(f"β
Found share button: {selector}")
|
|
1226
|
+
break
|
|
1227
|
+
except:
|
|
1228
|
+
continue
|
|
1229
|
+
|
|
1230
|
+
if not share_button:
|
|
1231
|
+
self.logger.error("β Could not find share button")
|
|
1232
|
+
screenshot_path = "debug/share_button_not_found.png"
|
|
1233
|
+
await self.page.screenshot(path=screenshot_path, timeout=5000)
|
|
1234
|
+
self.logger.info(f"πΈ Screenshot saved to {screenshot_path}")
|
|
1235
|
+
return False
|
|
1236
|
+
|
|
1237
|
+
# Click share button
|
|
1238
|
+
await share_button.click()
|
|
1239
|
+
await asyncio.sleep(1)
|
|
1240
|
+
|
|
1241
|
+
# Wait for share dialog to appear
|
|
1242
|
+
dialog_selectors = [
|
|
1243
|
+
'div[role="dialog"]',
|
|
1244
|
+
'.share-dialog',
|
|
1245
|
+
'[data-testid="share-dialog"]',
|
|
1246
|
+
'div.modal'
|
|
1247
|
+
]
|
|
1248
|
+
|
|
1249
|
+
dialog_found = False
|
|
1250
|
+
for selector in dialog_selectors:
|
|
1251
|
+
try:
|
|
1252
|
+
await self.page.wait_for_selector(selector, timeout=5000)
|
|
1253
|
+
dialog_found = True
|
|
1254
|
+
self.logger.info(f"β
Share dialog opened: {selector}")
|
|
1255
|
+
break
|
|
1256
|
+
except:
|
|
1257
|
+
continue
|
|
1258
|
+
|
|
1259
|
+
if not dialog_found:
|
|
1260
|
+
self.logger.warning("β οΈ Could not verify share dialog opened")
|
|
1261
|
+
|
|
1262
|
+
# Find email input field
|
|
1263
|
+
email_input_selectors = [
|
|
1264
|
+
'input[type="email"]',
|
|
1265
|
+
'input[aria-label*="email"]',
|
|
1266
|
+
'input[aria-label*="Add people"]',
|
|
1267
|
+
'input[placeholder*="email"]',
|
|
1268
|
+
'input.share-email-input'
|
|
1269
|
+
]
|
|
1270
|
+
|
|
1271
|
+
email_input = None
|
|
1272
|
+
for selector in email_input_selectors:
|
|
1273
|
+
try:
|
|
1274
|
+
element = await self.page.wait_for_selector(selector, timeout=5000)
|
|
1275
|
+
if element:
|
|
1276
|
+
email_input = element
|
|
1277
|
+
self.logger.info(f"β
Found email input: {selector}")
|
|
1278
|
+
break
|
|
1279
|
+
except:
|
|
1280
|
+
continue
|
|
1281
|
+
|
|
1282
|
+
if not email_input:
|
|
1283
|
+
self.logger.error("β Could not find email input field")
|
|
1284
|
+
screenshot_path = "debug/email_input_not_found.png"
|
|
1285
|
+
await self.page.screenshot(path=screenshot_path, timeout=5000)
|
|
1286
|
+
self.logger.info(f"πΈ Screenshot saved to {screenshot_path}")
|
|
1287
|
+
return False
|
|
1288
|
+
|
|
1289
|
+
# Type email address
|
|
1290
|
+
await email_input.click()
|
|
1291
|
+
await asyncio.sleep(0.5)
|
|
1292
|
+
await self.page.keyboard.type(email, delay=50)
|
|
1293
|
+
await asyncio.sleep(1)
|
|
1294
|
+
|
|
1295
|
+
# Select role if dropdown available
|
|
1296
|
+
if role != 'editor':
|
|
1297
|
+
role_selectors = [
|
|
1298
|
+
'select[aria-label*="role"]',
|
|
1299
|
+
'button[aria-label*="Can edit"]',
|
|
1300
|
+
'.role-selector'
|
|
1301
|
+
]
|
|
1302
|
+
|
|
1303
|
+
for selector in role_selectors:
|
|
1304
|
+
try:
|
|
1305
|
+
role_element = await self.page.wait_for_selector(selector, timeout=3000)
|
|
1306
|
+
if role_element:
|
|
1307
|
+
await role_element.click()
|
|
1308
|
+
await asyncio.sleep(0.5)
|
|
1309
|
+
|
|
1310
|
+
# Click viewer option
|
|
1311
|
+
viewer_selectors = [
|
|
1312
|
+
'li:has-text("Can view")',
|
|
1313
|
+
'button:has-text("Viewer")',
|
|
1314
|
+
'[data-value="viewer"]'
|
|
1315
|
+
]
|
|
1316
|
+
|
|
1317
|
+
for viewer_sel in viewer_selectors:
|
|
1318
|
+
try:
|
|
1319
|
+
viewer_option = await self.page.wait_for_selector(viewer_sel, timeout=2000)
|
|
1320
|
+
if viewer_option:
|
|
1321
|
+
await viewer_option.click()
|
|
1322
|
+
break
|
|
1323
|
+
except:
|
|
1324
|
+
continue
|
|
1325
|
+
break
|
|
1326
|
+
except:
|
|
1327
|
+
continue
|
|
1328
|
+
|
|
1329
|
+
# Send/Submit invitation
|
|
1330
|
+
send_button_selectors = [
|
|
1331
|
+
'button:has-text("Send")',
|
|
1332
|
+
'button:has-text("Share")',
|
|
1333
|
+
'button:has-text("Invite")',
|
|
1334
|
+
'button[aria-label="Send"]',
|
|
1335
|
+
'button[type="submit"]'
|
|
1336
|
+
]
|
|
1337
|
+
|
|
1338
|
+
send_button = None
|
|
1339
|
+
for selector in send_button_selectors:
|
|
1340
|
+
try:
|
|
1341
|
+
element = await self.page.wait_for_selector(selector, timeout=5000)
|
|
1342
|
+
if element:
|
|
1343
|
+
# Check if button is enabled
|
|
1344
|
+
is_disabled = await element.get_attribute('disabled')
|
|
1345
|
+
if not is_disabled:
|
|
1346
|
+
send_button = element
|
|
1347
|
+
self.logger.info(f"β
Found send button: {selector}")
|
|
1348
|
+
break
|
|
1349
|
+
except:
|
|
1350
|
+
continue
|
|
1351
|
+
|
|
1352
|
+
if not send_button:
|
|
1353
|
+
self.logger.error("β Could not find send button")
|
|
1354
|
+
# Try pressing Enter as fallback
|
|
1355
|
+
self.logger.info("β‘ Trying Enter key as fallback...")
|
|
1356
|
+
await self.page.keyboard.press('Enter')
|
|
1357
|
+
await asyncio.sleep(2)
|
|
1358
|
+
else:
|
|
1359
|
+
await send_button.click()
|
|
1360
|
+
await asyncio.sleep(2)
|
|
1361
|
+
|
|
1362
|
+
self.logger.info(f"β
Notebook shared with {email}")
|
|
1363
|
+
return True
|
|
1364
|
+
|
|
1365
|
+
except Exception as e:
|
|
1366
|
+
self.logger.error(f"β Failed to share notebook: {e}")
|
|
1367
|
+
if self.page:
|
|
1368
|
+
try:
|
|
1369
|
+
screenshot_path = "debug/share_error.png"
|
|
1370
|
+
await self.page.screenshot(path=screenshot_path, timeout=5000)
|
|
1371
|
+
self.logger.info(f"πΈ Screenshot saved to {screenshot_path}")
|
|
1372
|
+
except:
|
|
1373
|
+
pass
|
|
1374
|
+
return False
|
|
1375
|
+
|
|
1376
|
+
async def get_page_content(self) -> Optional[str]:
|
|
1377
|
+
"""
|
|
1378
|
+
Get the HTML content of the current page.
|
|
1379
|
+
|
|
1380
|
+
Returns:
|
|
1381
|
+
Optional[str]: The HTML content of the page, or None if an error occurs.
|
|
1382
|
+
"""
|
|
1383
|
+
try:
|
|
1384
|
+
if not self.page:
|
|
1385
|
+
self.logger.error("β No browser page available")
|
|
1386
|
+
return None
|
|
1387
|
+
|
|
1388
|
+
content = await self.page.content()
|
|
1389
|
+
return content
|
|
1390
|
+
|
|
1391
|
+
except Exception as e:
|
|
1392
|
+
self.logger.error(f"β Failed to get page content: {e}")
|
|
1393
|
+
return None
|
|
1394
|
+
|
|
1395
|
+
async def close(self):
|
|
1396
|
+
"""Close browser connections and cleanup resources."""
|
|
1397
|
+
try:
|
|
1398
|
+
if self.page:
|
|
1399
|
+
await self.page.close()
|
|
1400
|
+
if self.context:
|
|
1401
|
+
await self.context.close()
|
|
1402
|
+
if self.browser:
|
|
1403
|
+
await self.browser.close()
|
|
1404
|
+
|
|
1405
|
+
self.logger.info("π Browser connections closed")
|
|
1406
|
+
except Exception as e:
|
|
1407
|
+
self.logger.error(f"β Error closing browser: {e}")
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
# Example usage and testing
|
|
1411
|
+
async def test_notebooklm_connection():
|
|
1412
|
+
"""Test function to verify NotebookLM automation setup."""
|
|
1413
|
+
automator = NotebookLMAutomator()
|
|
1414
|
+
|
|
1415
|
+
try:
|
|
1416
|
+
# Test browser connection
|
|
1417
|
+
if await automator.connect_to_browser():
|
|
1418
|
+
print("β
Browser connection successful")
|
|
1419
|
+
|
|
1420
|
+
# Test navigation
|
|
1421
|
+
if await automator.navigate_to_notebooklm():
|
|
1422
|
+
print("β
NotebookLM navigation successful")
|
|
1423
|
+
|
|
1424
|
+
# Test authentication check
|
|
1425
|
+
auth_status = await automator.check_authentication()
|
|
1426
|
+
print(f"π Authentication status: {'β
Authenticated' if auth_status else 'β Not authenticated'}")
|
|
1427
|
+
else:
|
|
1428
|
+
print("β NotebookLM navigation failed")
|
|
1429
|
+
else:
|
|
1430
|
+
print("β Browser connection failed")
|
|
1431
|
+
|
|
1432
|
+
except Exception as e:
|
|
1433
|
+
print(f"β Test failed: {e}")
|
|
1434
|
+
|
|
1435
|
+
finally:
|
|
1436
|
+
await automator.close()
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
if __name__ == "__main__":
|
|
1440
|
+
# Run test if executed directly
|
|
1441
|
+
asyncio.run(test_notebooklm_connection())
|