import re import time from collections.abc import Awaitable, Callable from typing import Any from marketing_intelligence.browser.base import BrowserAdapter, PageSnapshot from marketing_intelligence.core.config import settings _VALID_ENGINES = ("camoufox", "chromium", "webkit") class PlaywrightAdapter(BrowserAdapter): """ Playwright-based adapter. engine controls which browser is launched: camoufox — AsyncCamoufox (Firefox + fingerprint spoofing, default) chromium — Playwright Chromium (stealth via CDP) webkit — Playwright WebKit (Safari-like) """ def __init__(self, engine: str = "camoufox", **kwargs: Any) -> None: super().__init__(**kwargs) if engine not in _VALID_ENGINES: raise ValueError(f"engine must be one of {_VALID_ENGINES}, got {engine!r}") self.engine = engine # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ def _camoufox_ctx(self): from camoufox.async_api import AsyncCamoufox return AsyncCamoufox(headless=settings.browser_headless, proxy=self.proxy) async def _get_page_camoufox(self, ctx): page = await ctx.new_page() page.on("pageerror", lambda _: None) return page # ------------------------------------------------------------------ # Public interface # ------------------------------------------------------------------ async def fetch_with_scroll( self, url: str, scroll_distance: int = 3000 ) -> PageSnapshot: t0 = time.monotonic() if self.engine == "camoufox": async with self._camoufox_ctx() as browser: page = await self._get_page_camoufox(browser) await page.goto(url, wait_until="domcontentloaded", timeout=30000) await self.behavior.human_delay(base=5.0, variance=2.0) await self.behavior.scroll(page, scroll_distance) await self.behavior.human_delay(base=2.0, variance=1.0) html = await page.content() final_url = page.url else: from playwright.async_api import async_playwright async with async_playwright() as p: launcher = getattr(p, self.engine) browser = await launcher.launch( headless=settings.browser_headless, proxy=self.proxy, ) page = await browser.new_page() page.on("pageerror", lambda _: None) await page.goto(url, wait_until="domcontentloaded", timeout=30000) await self.behavior.human_delay(base=5.0, variance=2.0) await self.behavior.scroll(page, scroll_distance) await self.behavior.human_delay(base=2.0, variance=1.0) html = await page.content() final_url = page.url await browser.close() response_time_ms = int((time.monotonic() - t0) * 1000) title_m = re.search(r"([^<]+)", html) title = title_m.group(1) if title_m else None return PageSnapshot( url=url, html=html, title=title, final_url=final_url, response_time_ms=response_time_ms, ) async def fetch_interactive( self, url: str, callback: Callable[[Any, str, str, int], Awaitable[Any]], ) -> Any: if self.engine == "camoufox": async with self._camoufox_ctx() as browser: page = await self._get_page_camoufox(browser) t0 = time.monotonic() await page.goto(url, wait_until="domcontentloaded", timeout=30000) await self.behavior.human_delay(base=2.0, variance=1.0) html = await page.content() final_url = page.url response_time_ms = int((time.monotonic() - t0) * 1000) return await callback(page, html, final_url, response_time_ms) else: from playwright.async_api import async_playwright async with async_playwright() as p: launcher = getattr(p, self.engine) browser = await launcher.launch( headless=settings.browser_headless, proxy=self.proxy, ) page = await browser.new_page() page.on("pageerror", lambda _: None) t0 = time.monotonic() await page.goto(url, wait_until="domcontentloaded", timeout=30000) await self.behavior.human_delay(base=2.0, variance=1.0) html = await page.content() final_url = page.url response_time_ms = int((time.monotonic() - t0) * 1000) result = await callback(page, html, final_url, response_time_ms) await browser.close() return result