#!/usr/bin/env -S uv run --script # /// script # dependencies = ["playwright"] # /// """ Headless Firefox with your cookies via cookiefire. Usage: playfox URL # load page, print title playfox --dump URL # print rendered html playfox -i URL # interactive python repl playfox -w 'selector' URL # wait for selector before proceeding playfox -q URL # quiet (no request logging) playfox -P profile URL # use specific firefox profile """ import subprocess import sys from playwright.sync_api import sync_playwright def parse_netscape_cookies(cookie_text): """Convert cookiefire output to Playwright format.""" cookies = [] for line in cookie_text.strip().split("\n"): if line.startswith("#") or not line.strip(): continue parts = line.split("\t") if len(parts) >= 7: domain, _, path, secure, expires, name, value = parts[:7] exp = float(expires) if exp <= 0: exp = -1 elif exp > 9999999999: exp = exp / 1000 cookies.append( { "name": name, "value": value, "domain": domain, "path": path, "expires": exp, "httpOnly": False, "secure": secure.upper() == "TRUE", "sameSite": "Lax", } ) return cookies def main(): args = sys.argv[1:] profile = "jtm" url = None dump = False interactive = False wait_for = None quiet = False i = 0 while i < len(args): if args[i] == "-P" and i + 1 < len(args): profile = args[i + 1] i += 2 elif args[i] == "--dump": dump = True i += 1 elif args[i] == "--interactive" or args[i] == "-i": interactive = True i += 1 elif args[i] == "-w" and i + 1 < len(args): wait_for = args[i + 1] i += 2 elif args[i] == "-q" or args[i] == "--quiet": quiet = True i += 1 elif args[i].startswith("http"): url = args[i] i += 1 else: i += 1 if not url: print("Usage: playfox [-P profile] [--dump] [-i] [-w selector] [-q] URL", file=sys.stderr) sys.exit(1) # get cookies cookie_text = subprocess.check_output(["cookiefire", profile], text=True) cookies = parse_netscape_cookies(cookie_text) with sync_playwright() as p: browser = p.firefox.launch(headless=True) context = browser.new_context() context.add_cookies(cookies) page = context.new_page() # logging (unless quiet) if not quiet: page.on("console", lambda msg: print(f"[console] {msg.text}", file=sys.stderr)) page.on("pageerror", lambda err: print(f"[error] {err}", file=sys.stderr)) page.on("request", lambda req: print(f"[req] {req.method} {req.url}", file=sys.stderr)) page.goto(url, wait_until="domcontentloaded") # wait for specific selector if requested if wait_for: try: page.wait_for_selector(wait_for, timeout=15000, state="attached") except Exception as e: print(f"[warn] wait_for '{wait_for}' timed out: {e}", file=sys.stderr) else: # default: wait a bit for SPAs/turbo to settle page.wait_for_timeout(2000) if dump: print(page.content()) if interactive: print("\n--- interactive mode ---") print("page object available as 'page'") print("ctrl-d to exit\n") import code code.interact(local={"page": page, "context": context, "browser": browser}) browser.close() if __name__ == "__main__": main()