#!/usr/bin/env -S uv run --script # /// script # dependencies = ["httpx"] # /// import argparse import json import logging import os import sys from concurrent.futures import ThreadPoolExecutor from dataclasses import asdict, dataclass from pathlib import Path import httpx @dataclass class UploadedImage: url: str thumb_url: str delete_url: str def upload_image_to_hamsterimg(api_key: str, image_paths: list[Path]) -> list[UploadedImage]: if not image_paths: return [] if not api_key: raise ValueError("HamsterImg API key is required") for image_path in image_paths: if not image_path.is_file(): raise FileNotFoundError(f"Image file not found: {image_path}") with httpx.Client(headers={"X-API-Key": api_key}, timeout=60) as client: def upload(image_path: Path) -> UploadedImage: with image_path.open(mode="rb") as image_file: response = client.post( url="https://hamsterimg.net/api/1/upload", data={"format": "json"}, files={"source": (image_path.name, image_file)}, ) try: result = response.json() except ValueError: result = {} if response.is_error or not isinstance(result, dict) or result.get("status_code") != 200: error = result.get("error", {}) if isinstance(result, dict) else {} message = error.get("message") if isinstance(error, dict) else str(error) status = result.get("status_txt") if isinstance(result, dict) else None reason = message or status or response.reason_phrase or "Unknown error" raise RuntimeError(f"HamsterImg upload failed for {image_path}: {reason}") try: image = result["image"] url = image["url"] thumb_url = image["thumb"]["url"] delete_url = image["delete_url"] except (KeyError, TypeError) as error: raise ValueError(f"HamsterImg returned incomplete image details for {image_path}") from error if not all(isinstance(value, str) and value for value in (url, thumb_url, delete_url)): raise ValueError(f"HamsterImg returned missing image URLs for {image_path}") return UploadedImage(url=url, thumb_url=thumb_url, delete_url=delete_url) with ThreadPoolExecutor(max_workers=min(8, len(image_paths))) as executor: futures = [executor.submit(upload, image_path=image_path) for image_path in image_paths] return [future.result() for future in futures] def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Upload images to HamsterImg and print their URLs.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument("image_paths", nargs="+", type=Path, help="Image files to upload") parser.add_argument("--api-key", help="HamsterImg API key; also reads HAMSTERIMG_API_KEY") parser.add_argument( "--format", dest="output_format", choices=["text", "json", "bbcode"], default="text", help="Output format; BBCode shows thumbnails linked to full images", ) arguments = parser.parse_args() arguments.api_key = arguments.api_key or os.getenv("HAMSTERIMG_API_KEY") if not arguments.api_key: parser.error("Provide --api-key or set HAMSTERIMG_API_KEY") for image_path in arguments.image_paths: if not image_path.is_file(): parser.error(f"Image file not found: {image_path}") return arguments def main() -> int: logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s") arguments = parse_args() try: images = upload_image_to_hamsterimg(api_key=arguments.api_key, image_paths=arguments.image_paths) except (OSError, ValueError, RuntimeError, httpx.HTTPError) as error: logging.error(f"{error}") return 1 if arguments.output_format == "json": print(json.dumps([asdict(obj=image) for image in images], indent=2, ensure_ascii=False)) return 0 for image in images: if arguments.output_format == "bbcode": print(f"[url={image.url}][img]{image.thumb_url}[/img][/url]") else: print(image.url) return 0 if __name__ == "__main__": sys.exit(main())