diff --git a/Tools/README.md b/Tools/README.md index 9c75da5..9a3fb5a 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -17,7 +17,7 @@ Repeatable import/export and processing helpers. Anything that was done twice by - `street_mesh.py` — road, kerbs, and sidewalks on the DEM, in the project frame; kerbs flush at the three crossings. - `terrain_from_dem.py` — terrain grid from the committed DEM, with building footprints cut out. - `blosm_blockout.py` — headless Blosm import of the committed `.osm` extract into a `.blend`, with a per-building scale report. Exact invocation in `Art/blender/README.md`. -- `commons_fetch.py` — Wikimedia Commons reference fetcher. Search or exact `File:` titles, thumbnails, provenance `sidecar.json` per folder. Wikimedia throttles `upload.wikimedia.org` (HTTP 429), so keep batches to six or fewer files and pass `--delay 3`; a throttled file is skipped and recorded rather than aborting the run. Writes only into the gitignored `Art/reference/`; reference images are never committed. +- `commons_fetch.py` — Wikimedia Commons reference fetcher. Search, exact `File:` titles, or a whole Commons category (`--category`), thumbnails, provenance `sidecar.json` per folder. Wikimedia throttles `upload.wikimedia.org` (HTTP 429), so keep batches to six or fewer files and pass `--delay 3`; a throttled file is skipped and recorded rather than aborting the run. Writes only into the gitignored `Art/reference/`; reference images are never committed. ## Planned helpers - Blender export preset runner (named presets, metric scale assertion) diff --git a/Tools/commons_fetch.py b/Tools/commons_fetch.py index 0b6bb9e..38845b4 100755 --- a/Tools/commons_fetch.py +++ b/Tools/commons_fetch.py @@ -86,6 +86,20 @@ def search_titles(query: str, limit: int) -> list[str]: return titles, order +def category_titles(category: str, limit: int) -> list[str]: + data = api_get( + { + "action": "query", + "list": "categorymembers", + "cmtitle": category, + "cmtype": "file", + "cmlimit": str(max(limit, 50)), + } + ) + members = data.get("query", {}).get("categorymembers", []) + return [m["title"] for m in members][:limit] + + def image_info(titles: list[str], thumb_width: int) -> list[dict]: data = api_get( { @@ -160,6 +174,7 @@ def download(url: str, path: str, retries: int = 3) -> str: def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--query", help="Commons search string") + ap.add_argument("--category", help='Commons category, e.g. "Category:Hollywood Masonic Temple"') ap.add_argument("--file", action="append", default=[], help="exact File: title, repeatable") ap.add_argument("--slug", required=True, help="output folder name under the out dir") ap.add_argument("--out", default="Art/reference", help="base output directory") @@ -169,10 +184,14 @@ def main() -> int: ap.add_argument("--delay", type=float, default=2.0, help="seconds between downloads") args = ap.parse_args() - if not args.query and not args.file: - ap.error("need --query or --file") + if not args.query and not args.file and not args.category: + ap.error("need --query, --category, or --file") titles = list(args.file) + if args.category: + found = category_titles(args.category, args.limit * 4) + print(f"{len(found)} file(s) in {args.category}") + titles += [t for t in found if t not in titles] if args.query: found, _ = search_titles(args.query, args.limit * 3) titles += [t for t in found if t not in titles] @@ -222,6 +241,7 @@ def main() -> int: "source": "Wikimedia Commons API", "api": API, "query": args.query, + "category": args.category, "requested_files": args.file, "slug": args.slug, "thumb_width": args.width,