import asyncio import logging from typing import Optional from playwright.async_api import async_playwright, Browser, BrowserContext, Page logger = logging.getLogger(__name__) class PlaywrightManager: """ Singleton-style manager for a single Playwright browser instance. Ensures only ONE browser/process runs per container, with proper cleanup. """ _instance: Optional["PlaywrightManager"] = None _pw_cm = None _pw = None _browser: Optional[Browser] = None _context: Optional[BrowserContext] = None _initialized = False _cleanup_lock = asyncio.Lock() def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance @classmethod async def initialize(cls, user_agent: Optional[str] = None, headless: bool = True): if cls._initialized: logger.debug("Playwright already initialized, reusing instance") return cls._instance cls._pw_cm = async_playwright() cls._pw = await cls._pw_cm.__aenter__() browser_args = { "headless": headless, "args": [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--disable-blink-features=AutomationControlled", ], } cls._browser = await cls._pw.chromium.launch(**browser_args) ua = user_agent or ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/131.0.0.0 Safari/131.0.0.0" ) cls._context = await cls._browser.new_context( user_agent=ua, viewport={"width": 1920, "height": 1080}, locale="en-US", timezone_id="America/New_York", extra_http_headers={"Accept-Language": "en-US,en;q=0.9"}, ) cls._initialized = True logger.info("Playwright browser initialized (single instance)") return cls._instance @classmethod async def get_page(cls, stealth: bool = False) -> Page: if not cls._initialized: raise RuntimeError( "PlaywrightManager not initialized. Call initialize() first." ) page = await cls._context.new_page() if stealth: try: from playwright_stealth import Stealth stealth = Stealth() await stealth.apply_stealth_async(page) logger.debug("Stealth mode applied to page") except (ImportError, Exception) as e: logger.warning(f"Could not apply stealth: {e}") return page @classmethod async def close_page(cls, page: Page): try: await page.close() except Exception as e: logger.debug(f"Error closing page: {e}") @classmethod async def shutdown(cls): async with cls._cleanup_lock: if not cls._initialized: return logger.info("Shutting down Playwright browser...") try: if cls._context: await cls._context.close() cls._context = None if cls._browser: await cls._browser.close() cls._browser = None if cls._pw_cm: await cls._pw_cm.__aexit__(None, None, None) cls._pw = None cls._pw_cm = None except Exception as e: logger.error(f"Error during Playwright shutdown: {e}") finally: cls._initialized = False cls._instance = None @classmethod def is_initialized(cls) -> bool: return cls._initialized