feat: launch desfoto.de as a standalone photography site and retire the old redirect
This commit is contained in:
174
scripts/fetch-assets.py
Executable file
174
scripts/fetch-assets.py
Executable 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())
|
||||
Reference in New Issue
Block a user