Files
antigravity-skills-reference/skills/notebooklm/scripts/auth_manager.py

358 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Authentication Manager for NotebookLM
Handles Google login and browser state persistence
Based on the MCP server implementation
Implements hybrid auth approach:
- Persistent browser profile (user_data_dir) for fingerprint consistency
- Manual cookie injection from state.json for session cookies (Playwright bug workaround)
See: https://github.com/microsoft/playwright/issues/36139
"""
import json
import time
import argparse
import shutil
import re
import sys
from pathlib import Path
from typing import Optional, Dict, Any
from patchright.sync_api import sync_playwright, BrowserContext
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent))
from config import BROWSER_STATE_DIR, STATE_FILE, AUTH_INFO_FILE, DATA_DIR
from browser_utils import BrowserFactory
class AuthManager:
"""
Manages authentication and browser state for NotebookLM
Features:
- Interactive Google login
- Browser state persistence
- Session restoration
- Account switching
"""
def __init__(self):
"""Initialize the authentication manager"""
# Ensure directories exist
DATA_DIR.mkdir(parents=True, exist_ok=True)
BROWSER_STATE_DIR.mkdir(parents=True, exist_ok=True)
self.state_file = STATE_FILE
self.auth_info_file = AUTH_INFO_FILE
self.browser_state_dir = BROWSER_STATE_DIR
def is_authenticated(self) -> bool:
"""Check if valid authentication exists"""
if not self.state_file.exists():
return False
# Check if state file is not too old (7 days)
age_days = (time.time() - self.state_file.stat().st_mtime) / 86400
if age_days > 7:
print(f"⚠️ Browser state is {age_days:.1f} days old, may need re-authentication")
return True
def get_auth_info(self) -> Dict[str, Any]:
"""Get authentication information"""
info = {
'authenticated': self.is_authenticated(),
'state_file': str(self.state_file),
'state_exists': self.state_file.exists()
}
if self.auth_info_file.exists():
try:
with open(self.auth_info_file, 'r') as f:
saved_info = json.load(f)
info.update(saved_info)
except Exception:
pass
if info['state_exists']:
age_hours = (time.time() - self.state_file.stat().st_mtime) / 3600
info['state_age_hours'] = age_hours
return info
def setup_auth(self, headless: bool = False, timeout_minutes: int = 10) -> bool:
"""
Perform interactive authentication setup
Args:
headless: Run browser in headless mode (False for login)
timeout_minutes: Maximum time to wait for login
Returns:
True if authentication successful
"""
print("🔐 Starting authentication setup...")
print(f" Timeout: {timeout_minutes} minutes")
playwright = None
context = None
try:
playwright = sync_playwright().start()
# Launch using factory
context = BrowserFactory.launch_persistent_context(
playwright,
headless=headless
)
# Navigate to NotebookLM
page = context.new_page()
page.goto("https://notebooklm.google.com", wait_until="domcontentloaded")
# Check if already authenticated
if "notebooklm.google.com" in page.url and "accounts.google.com" not in page.url:
print(" ✅ Already authenticated!")
self._save_browser_state(context)
return True
# Wait for manual login
print("\n ⏳ Please log in to your Google account...")
print(f" ⏱️ Waiting up to {timeout_minutes} minutes for login...")
try:
# Wait for URL to change to NotebookLM (regex ensures it's the actual domain, not a parameter)
timeout_ms = int(timeout_minutes * 60 * 1000)
page.wait_for_url(re.compile(r"^https://notebooklm\.google\.com/"), timeout=timeout_ms)
print(f" ✅ Login successful!")
# Save authentication state
self._save_browser_state(context)
self._save_auth_info()
return True
except Exception as e:
print(f" ❌ Authentication timeout: {e}")
return False
except Exception as e:
print(f" ❌ Error: {e}")
return False
finally:
# Clean up browser resources
if context:
try:
context.close()
except Exception:
pass
if playwright:
try:
playwright.stop()
except Exception:
pass
def _save_browser_state(self, context: BrowserContext):
"""Save browser state to disk"""
try:
# Save storage state (cookies, localStorage)
context.storage_state(path=str(self.state_file))
print(f" 💾 Saved browser state to: {self.state_file}")
except Exception as e:
print(f" ❌ Failed to save browser state: {e}")
raise
def _save_auth_info(self):
"""Save authentication metadata"""
try:
info = {
'authenticated_at': time.time(),
'authenticated_at_iso': time.strftime('%Y-%m-%d %H:%M:%S')
}
with open(self.auth_info_file, 'w') as f:
json.dump(info, f, indent=2)
except Exception:
pass # Non-critical
def clear_auth(self) -> bool:
"""
Clear all authentication data
Returns:
True if cleared successfully
"""
print("🗑️ Clearing authentication data...")
try:
# Remove browser state
if self.state_file.exists():
self.state_file.unlink()
print(" ✅ Removed browser state")
# Remove auth info
if self.auth_info_file.exists():
self.auth_info_file.unlink()
print(" ✅ Removed auth info")
# Clear entire browser state directory
if self.browser_state_dir.exists():
shutil.rmtree(self.browser_state_dir)
self.browser_state_dir.mkdir(parents=True, exist_ok=True)
print(" ✅ Cleared browser data")
return True
except Exception as e:
print(f" ❌ Error clearing auth: {e}")
return False
def re_auth(self, headless: bool = False, timeout_minutes: int = 10) -> bool:
"""
Perform re-authentication (clear and setup)
Args:
headless: Run browser in headless mode
timeout_minutes: Login timeout in minutes
Returns:
True if successful
"""
print("🔄 Starting re-authentication...")
# Clear existing auth
self.clear_auth()
# Setup new auth
return self.setup_auth(headless, timeout_minutes)
def validate_auth(self) -> bool:
"""
Validate that stored authentication works
Uses persistent context to match actual usage pattern
Returns:
True if authentication is valid
"""
if not self.is_authenticated():
return False
print("🔍 Validating authentication...")
playwright = None
context = None
try:
playwright = sync_playwright().start()
# Launch using factory
context = BrowserFactory.launch_persistent_context(
playwright,
headless=True
)
# Try to access NotebookLM
page = context.new_page()
page.goto("https://notebooklm.google.com", wait_until="domcontentloaded", timeout=30000)
# Check if we can access NotebookLM
if "notebooklm.google.com" in page.url and "accounts.google.com" not in page.url:
print(" ✅ Authentication is valid")
return True
else:
print(" ❌ Authentication is invalid (redirected to login)")
return False
except Exception as e:
print(f" ❌ Validation failed: {e}")
return False
finally:
if context:
try:
context.close()
except Exception:
pass
if playwright:
try:
playwright.stop()
except Exception:
pass
def main():
"""Command-line interface for authentication management"""
parser = argparse.ArgumentParser(description='Manage NotebookLM authentication')
subparsers = parser.add_subparsers(dest='command', help='Commands')
# Setup command
setup_parser = subparsers.add_parser('setup', help='Setup authentication')
setup_parser.add_argument('--headless', action='store_true', help='Run in headless mode')
setup_parser.add_argument('--timeout', type=float, default=10, help='Login timeout in minutes (default: 10)')
# Status command
subparsers.add_parser('status', help='Check authentication status')
# Validate command
subparsers.add_parser('validate', help='Validate authentication')
# Clear command
subparsers.add_parser('clear', help='Clear authentication')
# Re-auth command
reauth_parser = subparsers.add_parser('reauth', help='Re-authenticate (clear + setup)')
reauth_parser.add_argument('--timeout', type=float, default=10, help='Login timeout in minutes (default: 10)')
args = parser.parse_args()
# Initialize manager
auth = AuthManager()
# Execute command
if args.command == 'setup':
if auth.setup_auth(headless=args.headless, timeout_minutes=args.timeout):
print("\n✅ Authentication setup complete!")
print("You can now use ask_question.py to query NotebookLM")
else:
print("\n❌ Authentication setup failed")
exit(1)
elif args.command == 'status':
info = auth.get_auth_info()
print("\n🔐 Authentication Status:")
print(f" Authenticated: {'Yes' if info['authenticated'] else 'No'}")
if info.get('state_age_hours'):
print(f" State age: {info['state_age_hours']:.1f} hours")
if info.get('authenticated_at_iso'):
print(f" Last auth: {info['authenticated_at_iso']}")
print(f" State file: {info['state_file']}")
elif args.command == 'validate':
if auth.validate_auth():
print("Authentication is valid and working")
else:
print("Authentication is invalid or expired")
print("Run: auth_manager.py setup")
elif args.command == 'clear':
if auth.clear_auth():
print("Authentication cleared")
elif args.command == 'reauth':
if auth.re_auth(timeout_minutes=args.timeout):
print("\n✅ Re-authentication complete!")
else:
print("\n❌ Re-authentication failed")
exit(1)
else:
parser.print_help()
if __name__ == "__main__":
main()