"""Persistent browser pool — one live Camoufox browser per session, reused across page navigations.""" from typing import Any import structlog logger = structlog.get_logger("browser.pool") class PersistentBrowserPool: """Keeps one live Camoufox browser per session_id. First call starts the browser, navigates to the tag page as warmup so videos are opened organically from within a real TikTok browsing context. Subsequent calls reuse the same page (same cookies, fingerprint, IP). close() tears the browser down. """ def __init__(self) -> None: self._entries: dict[str, tuple] = {} # session_id -> (camoufox_ctx, page) async def get_page( self, session_id: str, proxy: dict | None, headless: bool = True, ) -> Any: if session_id in self._entries: _, page = self._entries[session_id] return page from camoufox.async_api import AsyncCamoufox proxy_server = (proxy or {}).get("server", "none") logger.info("pool.browser_start", session_id=session_id, proxy=proxy_server) cf = AsyncCamoufox(headless=headless, proxy=proxy) browser = await cf.__aenter__() page = await browser.new_page() self._entries[session_id] = (cf, page) return page async def close(self, session_id: str) -> None: if session_id not in self._entries: return cf, _ = self._entries.pop(session_id) try: await cf.__aexit__(None, None, None) logger.info("pool.browser_closed", session_id=session_id) except Exception as e: logger.warning( "pool.close_failed", session_id=session_id, error=str(e)[:120] ) async def close_all(self) -> None: for sid in list( self._entries.keys() ): # NOSONAR: list() snapshot required — close() mutates _entries await self.close(sid) _pool = PersistentBrowserPool()