"""BrowserManager: manages Playwright persistent Chrome profile contexts.""" from __future__ import annotations from pathlib import Path from typing import Optional from loguru import logger class BrowserManager: """Manages Playwright browser contexts using persistent Chrome profiles.""" def __init__( self, profile_name: str = "JobAgent", headless: bool = True, slow_mo: int = 0, ): self.profile_name = profile_name self.headless = headless self.slow_mo = slow_mo self._playwright = None self._context = None def open_context( self, profile_name: Optional[str] = None, headless: Optional[bool] = None, slow_mo: Optional[int] = None, ): """Open a persistent browser context. Returns the BrowserContext.""" from playwright.sync_api import sync_playwright from gimme_job.utils.paths import chrome_profile_dir name = profile_name or self.profile_name hl = headless if headless is not None else self.headless sm = slow_mo if slow_mo is not None else self.slow_mo profile_path = chrome_profile_dir(name) profile_path.mkdir(parents=True, exist_ok=True) logger.info(f"Opening browser context: profile={name}, headless={hl}") self._playwright = sync_playwright().start() self._context = self._playwright.chromium.launch_persistent_context( user_data_dir=str(profile_path), headless=hl, slow_mo=sm, channel="chrome", args=[ "--disable-blink-features=AutomationControlled", "--no-sandbox", "--disable-infobars", "--disable-dev-shm-usage", ], ignore_default_args=["--enable-automation"], viewport={"width": 1280, "height": 900}, locale="en-US", user_agent=( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/134.0.0.0 Safari/537.36" ), ) # Apply stealth patches to every new page try: from playwright_stealth import stealth_sync self._context.add_init_script(""" Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); """) # stealth_sync works per-page; store reference for use in open_page() self._stealth_fn = stealth_sync except ImportError: self._stealth_fn = None return self._context def new_page(self): """Open a new page with stealth applied.""" page = self._context.new_page() if getattr(self, "_stealth_fn", None): try: self._stealth_fn(page) except Exception as e: logger.debug(f"Stealth apply failed: {e}") return page def close(self) -> None: if self._context: try: self._context.close() except Exception: pass self._context = None if self._playwright: try: self._playwright.stop() except Exception: pass self._playwright = None def save_trace(self, trace_path: Path) -> None: if self._context: try: self._context.tracing.stop(path=str(trace_path)) logger.debug(f"Trace saved: {trace_path}") except Exception as e: logger.warning(f"Failed to save trace: {e}") def start_tracing(self) -> None: if self._context: try: self._context.tracing.start(screenshots=True, snapshots=True) except Exception as e: logger.warning(f"Failed to start tracing: {e}") def __enter__(self): return self def __exit__(self, *args): self.close()