""" Mark videos as watched using a headless browser. Visiting the page with authenticated cookies triggers the watch tracking JavaScript. """ import os import re import logging import asyncio from urllib.parse import urlparse, parse_qs from typing import Optional, Dict log = logging.getLogger("headless_watcher") # Try to import playwright try: from playwright.async_api import async_playwright HAS_PLAYWRIGHT = True except ImportError: HAS_PLAYWRIGHT = False log.warning("Playwright not installed, headless watching will not work") class HeadlessWatcher: """Uses headless browser to visit pages and trigger watch tracking.""" def __init__(self, cookie_file: str): self.cookie_file = cookie_file self.domain_cookies = self._parse_cookie_file() def _parse_cookie_file(self) -> Dict[str, list]: """Parse Netscape cookie file and group by domain.""" cookies_by_domain = {} if not os.path.exists(self.cookie_file): log.warning(f"Cookie file not found: {self.cookie_file}") return cookies_by_domain try: with open(self.cookie_file, "r") as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue parts = line.split("\t") if len(parts) >= 7: domain = parts[0].lstrip(".") path = parts[2] secure = parts[3] == "TRUE" name = parts[5] value = parts[6] if domain not in cookies_by_domain: cookies_by_domain[domain] = [] cookies_by_domain[domain].append({ "name": name, "value": value, "domain": parts[0], "path": path, "secure": secure, "httpOnly": False, }) log.debug(f"Parsed cookies for {len(cookies_by_domain)} domains") except Exception as e: log.error(f"Error parsing cookie file: {e}") return cookies_by_domain async def visit_page(self, url: str, wait_seconds: int = 5) -> bool: """Visit a page with cookies to trigger watch tracking.""" if not HAS_PLAYWRIGHT: log.error("Playwright not installed") return False parsed_url = urlparse(url) domain = parsed_url.netloc.lower() cookies = [] for cookie_domain, domain_cookies in self.domain_cookies.items(): if domain in cookie_domain or cookie_domain in domain: cookies.extend(domain_cookies) if not cookies: log.warning(f"No cookies found for domain: {domain}") return False log.info(f"Visiting {url} with {len(cookies)} cookies") try: async with async_playwright() as p: # Use system Chromium if available (for Alpine/Docker) chromium_path = os.environ.get("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH") if chromium_path and os.path.exists(chromium_path): log.debug(f"Using system Chromium: {chromium_path}") browser = await p.chromium.launch(headless=True, executable_path=chromium_path) else: browser = await p.chromium.launch(headless=True) try: context = await browser.new_context( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" ) await context.add_cookies(cookies) page = await context.new_page() response = await page.goto(url, wait_until="networkidle", timeout=30000) if response and response.status == 200: log.debug(f"Page loaded, waiting {wait_seconds}s") await asyncio.sleep(wait_seconds) log.info("Successfully triggered watch tracking") return True else: status = response.status if response else "no response" log.warning(f"Failed to load page, status: {status}") return False finally: await browser.close() except Exception as e: log.error(f"Error in headless browser: {e}") return False class PHEadlessWatcher(HeadlessWatcher): """PornHub specific headless watcher.""" DOMAINS = ["pornhub.com", "www.pornhub.com", "de.pornhub.com", "fr.pornhub.com", "es.pornhub.com", "it.pornhub.com", "rt.pornhub.com"] def can_handle(self, url: str) -> bool: parsed = urlparse(url) domain = parsed.netloc.lower() return any(d in domain for d in self.DOMAINS) async def mark_watched(self, url: str, wait_seconds: int = 5) -> bool: return await self.visit_page(url, wait_seconds=wait_seconds) async def headless_mark_watched(url: str, cookie_file: str, wait_seconds: int = 5) -> bool: """Mark a video as watched using headless browser.""" if not HAS_PLAYWRIGHT: log.error("Playwright is not installed") return False # Try PH handler first ph_handler = PHHeadlessWatcher(cookie_file) if ph_handler.can_handle(url): return await ph_handler.mark_watched(url, wait_seconds) # Fallback to generic handler generic_handler = HeadlessWatcher(cookie_file) return await generic_handler.visit_page(url, wait_seconds)