diff --git a/shotscraper b/shotscraper new file mode 100755 index 0000000..347360f --- /dev/null +++ b/shotscraper @@ -0,0 +1,98 @@ +#!/usr/bin/env -S uv run --script +# /// script +# dependencies = [] +# /// +""" +shot-scraper wrapper that uses Firefox cookies via cookiefire. + +Usage: + shotscraper https://example.com -o out.png + shotscraper -P jtm https://private.com -o private.png +""" + +import json +import os +import subprocess +import sys +import tempfile + + +def parse_netscape_cookies(cookie_text): + """Convert Netscape cookie format to Playwright storage_state 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] + # playwright requires -1 (session) or positive unix timestamp in seconds + # firefox stores milliseconds, so divide by 1000 + exp = float(expires) + if exp <= 0: + exp = -1 + elif exp > 9999999999: # looks like milliseconds + 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": cookies, "origins": []} + + +def main(): + args = sys.argv[1:] + profile = "jtm" + passthrough = [] + + # extract -P profile from args + i = 0 + while i < len(args): + if args[i] == "-P" and i + 1 < len(args): + next_arg = args[i + 1] + # don't treat URLs as profile names + if next_arg.startswith("http://") or next_arg.startswith("https://"): + passthrough.append(next_arg) + i += 2 + else: + profile = next_arg + i += 2 + else: + passthrough.append(args[i]) + i += 1 + + # get cookies from firefox + try: + cookie_text = subprocess.check_output( + ["cookiefire", profile], + text=True + ) + except subprocess.CalledProcessError as e: + print(f"cookiefire failed: {e}", file=sys.stderr) + sys.exit(1) + + storage_state = parse_netscape_cookies(cookie_text) + + # write to temp file + with tempfile.NamedTemporaryFile( + mode='w', suffix='.json', delete=False + ) as f: + json.dump(storage_state, f) + auth_file = f.name + + try: + cmd = ["shot-scraper", "--auth", auth_file] + passthrough + result = subprocess.run(cmd) + sys.exit(result.returncode) + finally: + os.unlink(auth_file) + + +if __name__ == "__main__": + main()