feat(hamsterimg): add image uploader

This commit is contained in:
abdus committed 2026-10-08 11:27:33 +00:00
1 parent 6eb0ab7d5a
commit 0d97094092
2 files changed
+307

No files matched your search

Executable
+112
View File
@@ -0,0 +1,112 @@
#!/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())