You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
122 lines
3.9 KiB
Python
122 lines
3.9 KiB
Python
"""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()
|