""" Download and patch ChromeDriver once. Run via: make patch-driver Uses the official Chrome for Testing API to fetch the exact ChromeDriver version matching the installed Chrome, then patches it with undetected_chromedriver. """ from __future__ import annotations import io import shutil import stat import subprocess import sys import urllib.request import zipfile from pathlib import Path import undetected_chromedriver as uc from config import config from logger import get_logger log = get_logger(__name__) def _get_chrome_full_version() -> tuple[int, str]: candidates = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"] if config.chrome_binary: candidates.insert(0, config.chrome_binary) for binary in candidates: if not shutil.which(binary) and not Path(binary).exists(): continue try: out = subprocess.check_output( [binary, "--version"], stderr=subprocess.DEVNULL, text=True, timeout=5 ).strip() full = out.split()[-1] major = int(full.split(".")[0]) log.info("Detected Chrome %s via '%s'", full, binary) return major, full except Exception: continue log.critical("Could not detect Chrome version. Set CHROME_BINARY in .env") sys.exit(1) def _latest_chromedriver_version(major: int) -> str: url = f"https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_{major}" try: with urllib.request.urlopen(url, timeout=15) as resp: version = resp.read().decode().strip() log.info("Latest ChromeDriver for Chrome %d: %s", major, version) return version except Exception as exc: log.critical("Could not fetch ChromeDriver version for Chrome %d: %s", major, exc) sys.exit(1) def _download_chromedriver(version: str, dest: Path) -> None: zip_url = ( f"https://storage.googleapis.com/chrome-for-testing-public" f"/{version}/linux64/chromedriver-linux64.zip" ) log.info("Downloading ChromeDriver %s …", version) try: with urllib.request.urlopen(zip_url, timeout=60) as resp: data = resp.read() except Exception as exc: log.critical("Download failed: %s", exc) sys.exit(1) with zipfile.ZipFile(io.BytesIO(data)) as zf: binary_name = next( n for n in zf.namelist() if n.endswith("/chromedriver") or n == "chromedriver" ) dest.write_bytes(zf.read(binary_name)) dest.chmod(dest.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) log.info("Extracted to %s", dest) def _patch(dest: Path) -> None: log.info("Patching binary …") patcher = uc.Patcher(executable_path=str(dest)) patcher.patch_exe() log.info("Patch applied successfully") def main() -> None: dest = Path(config.chromedriver_path) dest.parent.mkdir(parents=True, exist_ok=True) major, _ = _get_chrome_full_version() cd_version = _latest_chromedriver_version(major) _download_chromedriver(cd_version, dest) _patch(dest) log.info("ChromeDriver %s ready at %s", cd_version, dest) log.info("Re-run 'make patch-driver' only when Chrome updates to a new major version.") if __name__ == "__main__": main()