feat: OG link previews — track image + meta tags for Telegram/WhatsApp
- bincio/render/ogimage.py: generate 400x400 elevation-coloured PNG with Pillow
- bincio/serve/routers/ogimage.py: /activity/{id}/ OG HTML stub for bot UAs;
/og-image/{user}/{id}.png serves pre-generated images with on-demand fallback
- scripts/generate_og_images.py: batch pre-generation, incremental (mtime skip)
- scripts/strava_elevation_audit.py: add source/threshold/MA columns and pct stats
- pyproject.toml: add Pillow>=10 to serve extras
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""Pre-generate OG track images for all activities.
|
||||
|
||||
Writes 400×400 PNGs to {www_root}/og-image/{user}/{activity_id}.png.
|
||||
Skips activities that already have an up-to-date image (mtime check).
|
||||
Safe to run repeatedly — only processes new/changed activities.
|
||||
|
||||
Usage:
|
||||
uv run scripts/generate_og_images.py [--data-dir /var/bincio/data] [--www-root /var/www/activity]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def generate_all(data_dir: Path, www_root: Path) -> None:
|
||||
out_root = www_root / "og-image"
|
||||
out_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
from bincio.render.ogimage import generate
|
||||
|
||||
total = generated = skipped = errors = 0
|
||||
|
||||
users = sorted(
|
||||
d.name for d in data_dir.iterdir()
|
||||
if d.is_dir() and not d.name.startswith("_") and d.name != "segments"
|
||||
)
|
||||
|
||||
for handle in users:
|
||||
user_dir = data_dir / handle
|
||||
acts_dir = user_dir / "activities"
|
||||
img_dir = out_root / handle
|
||||
if not acts_dir.exists():
|
||||
continue
|
||||
img_dir.mkdir(exist_ok=True)
|
||||
u_gen = u_skip = u_err = 0
|
||||
|
||||
for ts_path in sorted(acts_dir.glob("*.timeseries.json")):
|
||||
activity_id = ts_path.name.replace(".timeseries.json", "")
|
||||
out_path = img_dir / f"{activity_id}.png"
|
||||
total += 1
|
||||
|
||||
# Skip if image is newer than timeseries
|
||||
if out_path.exists() and out_path.stat().st_mtime >= ts_path.stat().st_mtime:
|
||||
skipped += 1
|
||||
u_skip += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
ts = json.loads(ts_path.read_text(encoding="utf-8"))
|
||||
lat_arr = ts.get("lat") or []
|
||||
lon_arr = ts.get("lon") or []
|
||||
ele_arr = ts.get("elevation_m") or []
|
||||
png = generate(lat_arr, lon_arr, ele_arr)
|
||||
out_path.write_bytes(png)
|
||||
generated += 1
|
||||
u_gen += 1
|
||||
except Exception as exc:
|
||||
errors += 1
|
||||
u_err += 1
|
||||
print(f" ERROR {handle}/{activity_id}: {exc}", file=sys.stderr)
|
||||
|
||||
if u_gen or u_err:
|
||||
print(f"{handle:<25} generated={u_gen:4d} skipped={u_skip:4d} errors={u_err}")
|
||||
else:
|
||||
print(f"{handle:<25} skipped={u_skip:4d} (all up to date)")
|
||||
|
||||
print(f"\nDone — {generated} generated, {skipped} skipped, {errors} errors (total {total})")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="Pre-generate OG track images")
|
||||
ap.add_argument("--data-dir", default="/var/bincio/data", type=Path)
|
||||
ap.add_argument("--www-root", default="/var/www/activity", type=Path)
|
||||
args = ap.parse_args()
|
||||
|
||||
if not args.data_dir.exists():
|
||||
print(f"ERROR: data dir not found: {args.data_dir}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"data-dir : {args.data_dir}")
|
||||
print(f"www-root : {args.www_root}")
|
||||
print(f"output : {args.www_root}/og-image/\n")
|
||||
generate_all(args.data_dir, args.www_root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user