#!/usr/bin/env python3 """Fetch real source assets for desfoto.de. Sources ------- * Own image pool (Orgatool volume, served through the photographer's own API). We pull the pre-derived `web` variant (long edge 2560 px) instead of the 20-60 MB camera originals. Only assets we are allowed to publish as the rights holder are listed in src/images.json. * Own portfolio files of the existing site (www.dennyschulz.de/legacy/). These carry the concert, band and free-work photographs that are missing from the pool; they are fetched as-is and downscaled by the build. Also used for the operator's original logo (www.dennyschulz.de/logo.png). * Google Fonts (open licensed, OFL) - downloaded once, then self-hosted. Licences are written to docs/. * YouTube poster frame for the own music video "Thjodroerir - Skogamor". Everything lands in assets-src/ which is *not* shipped. scripts/build-site.py turns assets-src/ into the committed site/ output. """ from __future__ import annotations import json import re import sys import urllib.error import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SRC = ROOT / "assets-src" IMG_OUT = SRC / "images" BRAND_OUT = SRC / "brand" FONT_OUT = SRC / "fonts" TTF_OUT = SRC / "fonts-ttf" DOCS = ROOT / "docs" LOGO_URL = "https://www.dennyschulz.de/logo.png" UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36" # Old Safari advertises truetype-only support, so Google serves real .ttf # files (the MSIE 6 UA returns EOT, which PIL/FreeType cannot read). UA_TTF = ( "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) " "AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.1" ) # family -> (google css2 query, wanted unicode-range comments) FAMILIES = { "fraunces": { "query": "family=Fraunces:opsz,wght@9..144,300..700", "keep": ("latin", "latin-ext"), "license": "https://raw.githubusercontent.com/google/fonts/main/ofl/fraunces/OFL.txt", }, "manrope": { "query": "family=Manrope:wght@200..800", "keep": ("latin", "latin-ext"), "license": "https://raw.githubusercontent.com/google/fonts/main/ofl/manrope/OFL.txt", }, } TTF_QUERIES = ( "family=Fraunces:opsz,wght@9..144,400", "family=Fraunces:opsz,wght@9..144,600", "family=Manrope:wght@400", "family=Manrope:wght@600", ) VIDEO_ID = "NWWFTf7l8g0" def fetch(url: str, *, ua: str = UA, timeout: int = 120) -> bytes: req = urllib.request.Request(url, headers={"User-Agent": ua, "Accept": "*/*"}) with urllib.request.urlopen(req, timeout=timeout) as resp: return resp.read() def try_fetch(url: str, *, ua: str = UA) -> bytes | None: try: return fetch(url, ua=ua) except urllib.error.HTTPError as exc: if exc.code in (403, 404, 410): return None raise except urllib.error.URLError: return None def fetch_images() -> None: manifest = json.loads((ROOT / "src" / "images.json").read_text(encoding="utf-8")) base = manifest["pool_base"].rstrip("/") legacy_base = manifest["legacy_base"].rstrip("/") suffixes = (".webp", ".src", ".jpg", ".jpeg", ".png") ok = skipped = 0 for entry in manifest["images"]: slug, raw = entry["slug"], entry["raw"] rel = raw.replace("\\", "/") if any((IMG_OUT / f"{slug}{suffix}").exists() for suffix in suffixes): skipped += 1 continue if entry.get("source") == "legacy": data = fetch(f"{legacy_base}/{rel}") (IMG_OUT / f"{slug}.jpg").write_bytes(data) else: data = try_fetch(f"{base}/_derived/web/{rel}.webp") if data is None: print(f" [!] web variant missing, falling back to original: {rel}") data = fetch(f"{base}/{rel}") (IMG_OUT / f"{slug}.src").write_bytes(data) else: (IMG_OUT / f"{slug}.webp").write_bytes(data) ok += 1 print(f" [ok] {slug} ({len(data) / 1024:.0f} KiB)") print(f"images: {ok} fetched, {skipped} already present") poster = IMG_OUT / "video-skogamor-poster.jpg" if not poster.exists(): data = try_fetch(f"https://i.ytimg.com/vi/{VIDEO_ID}/maxresdefault.jpg") if data is None: data = fetch(f"https://i.ytimg.com/vi/{VIDEO_ID}/hqdefault.jpg") poster.write_bytes(data) print(f" [ok] video poster ({len(data) / 1024:.0f} KiB)") def fetch_brand() -> None: """The operator's original logo (used for the brand mark and all icons).""" BRAND_OUT.mkdir(parents=True, exist_ok=True) target = BRAND_OUT / "logo.png" if target.exists(): print(f"brand: logo already present ({target.stat().st_size / 1024:.0f} KiB)") return data = fetch(LOGO_URL) target.write_bytes(data) print(f" [ok] logo ({len(data) / 1024:.0f} KiB)") def fetch_fonts() -> None: FONT_OUT.mkdir(parents=True, exist_ok=True) for family, spec in FAMILIES.items(): css = fetch(f"https://fonts.googleapis.com/css2?{spec['query']}&display=swap").decode() blocks = re.findall( r"/\* ([a-z0-9-]+) \*/\s*@font-face \{(.*?)\}", css, flags=re.S ) index = [] for subset, body in blocks: if subset not in spec["keep"]: continue url = re.search(r"url\((https://[^)]+\.woff2)\)", body) rng = re.search(r"unicode-range:\s*([^;]+);", body) if not url: continue name = f"{family}-{subset}.woff2" target = FONT_OUT / name if not target.exists(): target.write_bytes(fetch(url.group(1))) index.append({"file": name, "range": rng.group(1).strip() if rng else ""}) print(f" [ok] font {name} ({target.stat().st_size / 1024:.0f} KiB)") (SRC / f"font-index-{family}.json").write_text( json.dumps(index, indent=2) + "\n", encoding="utf-8" ) license_target = DOCS / f"font-{family}-OFL.txt" if not license_target.exists(): license_target.write_bytes(fetch(spec["license"])) print(f" [ok] licence {license_target.name}") # Build-time only TTFs (used to render the OG image and brand PNGs). TTF_OUT.mkdir(parents=True, exist_ok=True) for query in TTF_QUERIES: css = fetch(f"https://fonts.googleapis.com/css2?{query}", ua=UA_TTF).decode() blocks = re.findall(r"@font-face \{(.*?)\}", css, flags=re.S) for block in blocks: url_match = re.search(r"url\((https://[^)]+)\)", block) if not url_match: continue family = (re.search(r"font-family: '([^']+)'", block) or [None, "font"])[1] weight = (re.search(r"font-weight: (\d+)", block) or [None, "400"])[1] name = f"{family.lower()}-{weight}.ttf" target = TTF_OUT / name if target.exists(): continue target.write_bytes(fetch(url_match.group(1), ua=UA_TTF)) print(f" [ok] build font {name} ({target.stat().st_size / 1024:.0f} KiB)") def main() -> int: for path in (IMG_OUT, BRAND_OUT, FONT_OUT, DOCS): path.mkdir(parents=True, exist_ok=True) print("== brand ==") fetch_brand() print("== images ==") fetch_images() print("== fonts ==") fetch_fonts() print("done") return 0 if __name__ == "__main__": sys.exit(main())