feat: launch desfoto.de as a standalone photography site and retire the old redirect

This commit is contained in:
opencode
2026-09-19 16:02:16 +02:00
commit 160ec017eb
218 changed files with 7518 additions and 0 deletions

174
scripts/fetch-assets.py Executable file
View File

@@ -0,0 +1,174 @@
#!/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.
* 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"
FONT_OUT = SRC / "fonts"
TTF_OUT = SRC / "fonts-ttf"
DOCS = ROOT / "docs"
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("/")
ok = skipped = 0
for entry in manifest["images"]:
slug, raw = entry["slug"], entry["raw"]
rel = raw.replace("\\", "/")
target = IMG_OUT / f"{slug}.webp"
source = IMG_OUT / f"{slug}.src"
if target.exists() or source.exists():
skipped += 1
continue
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}")
source.write_bytes(data)
else:
target.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_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, FONT_OUT, DOCS):
path.mkdir(parents=True, exist_ok=True)
print("== images ==")
fetch_images()
print("== fonts ==")
fetch_fonts()
print("done")
return 0
if __name__ == "__main__":
sys.exit(main())