143 lines
4.5 KiB
Python
Executable File
143 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Build the image manifest and commit it.
|
|
|
|
Scans ./img recursively, writes ./images.json (the file the single-page app
|
|
fetches at runtime), then stages and commits the result with git.
|
|
|
|
Usage:
|
|
./generate-manifest.py # generate + commit
|
|
./generate-manifest.py --no-commit # generate only
|
|
./generate-manifest.py -m "Add logos" # custom commit message
|
|
|
|
Run this after adding, renaming, or deleting images.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
IMG_DIR = ROOT / "img"
|
|
OUT_FILE = ROOT / "images.json"
|
|
|
|
IMAGE_EXTS = {
|
|
".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif",
|
|
".svg", ".bmp", ".ico", ".tif", ".tiff",
|
|
}
|
|
|
|
|
|
def natural_key(text: str) -> list:
|
|
"""Sort key so 'img2' comes before 'img10'."""
|
|
return [int(part) if part.isdigit() else part.lower() for part in re.split(r"(\d+)", text)]
|
|
|
|
|
|
def find_images() -> list[dict]:
|
|
if not IMG_DIR.is_dir():
|
|
print(f"✗ Image folder not found: {IMG_DIR}", file=sys.stderr)
|
|
print(" Create it and add some images, then re-run.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
images = []
|
|
for path in IMG_DIR.rglob("*"):
|
|
if not path.is_file():
|
|
continue
|
|
# Skip dotfiles and anything inside a dot-directory.
|
|
if any(part.startswith(".") for part in path.relative_to(IMG_DIR).parts):
|
|
continue
|
|
if path.suffix.lower() not in IMAGE_EXTS:
|
|
continue
|
|
|
|
stat = path.stat()
|
|
images.append({
|
|
"name": path.name,
|
|
"path": path.relative_to(ROOT).as_posix(), # POSIX-style for URLs
|
|
"size": stat.st_size,
|
|
"mtime": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
|
|
.isoformat()
|
|
.replace("+00:00", "Z"),
|
|
})
|
|
|
|
images.sort(key=lambda img: natural_key(img["path"]))
|
|
return images
|
|
|
|
|
|
def write_manifest(images: list[dict]) -> bool:
|
|
"""Write images.json. Returns True if the file changed.
|
|
|
|
If the image list is identical to what's already on disk, the file is left
|
|
untouched (keeping its timestamp) so a no-op run produces no commit.
|
|
"""
|
|
plural = "" if len(images) == 1 else "s"
|
|
|
|
if OUT_FILE.exists():
|
|
try:
|
|
existing = json.loads(OUT_FILE.read_text(encoding="utf-8"))
|
|
except (json.JSONDecodeError, OSError):
|
|
existing = None
|
|
if isinstance(existing, dict) and existing.get("images") == images:
|
|
print(f"• Manifest unchanged ({len(images)} image{plural}).")
|
|
return False
|
|
|
|
manifest = {
|
|
"generatedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
"count": len(images),
|
|
"images": images,
|
|
}
|
|
OUT_FILE.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
print(f"✓ Wrote {len(images)} image{plural} to {OUT_FILE.name}")
|
|
return True
|
|
|
|
|
|
def git(*args: str, check: bool = True) -> subprocess.CompletedProcess:
|
|
return subprocess.run(["git", *args], cwd=ROOT, check=check, text=True,
|
|
capture_output=True)
|
|
|
|
|
|
def commit(message: str) -> None:
|
|
"""Stage everything and commit, if this is a git repo with changes."""
|
|
if git("rev-parse", "--is-inside-work-tree", check=False).returncode != 0:
|
|
print("• Not a git repository — skipping commit.")
|
|
return
|
|
|
|
git("add", "-A")
|
|
|
|
# Nothing staged? Then there's nothing to commit.
|
|
if git("diff", "--cached", "--quiet", check=False).returncode == 0:
|
|
print("• No changes to commit.")
|
|
return
|
|
|
|
result = git("commit", "-m", message, check=False)
|
|
if result.returncode != 0:
|
|
print(f"✗ git commit failed:\n{result.stderr.strip()}", file=sys.stderr)
|
|
sys.exit(1)
|
|
print(f"✓ Committed: {message}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Build images.json and commit it.")
|
|
parser.add_argument("--no-commit", action="store_true",
|
|
help="generate the manifest without committing")
|
|
parser.add_argument("-m", "--message", default=None,
|
|
help="commit message (default: auto-generated)")
|
|
args = parser.parse_args()
|
|
|
|
images = find_images()
|
|
write_manifest(images)
|
|
|
|
if args.no_commit:
|
|
return
|
|
|
|
plural = "" if len(images) == 1 else "s"
|
|
message = args.message or f"Update image manifest ({len(images)} image{plural})"
|
|
commit(message)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|