#!/usr/bin/env python3 """Generate missing archive editions for a date range.""" from __future__ import annotations import argparse import json import os import random import sys import time from datetime import date, datetime, timedelta, timezone from pathlib import Path import topics from generate import ( THEMES, build_archiv_entries, build_system_prompt, build_user_prompt, call_openrouter, date_human, generate_hero_image, load_archive, log, render_template, save_json_atomic, write_text_atomic, ) def load_env_file(path: Path) -> None: if not path.exists(): return for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) os.environ.setdefault(key.strip(), value.strip()) def parse_date(value: str) -> date: return date.fromisoformat(value) def generate_day( day: date, api_key: str, model: str, pixazo_key: str | None, site_dir: Path, data_dir: Path, archive: list[dict], *, update_index: bool, pixazo_pause: float, ) -> list[dict]: date_iso = day.isoformat() if any(entry.get("date") == date_iso for entry in archive): log(f"skipping {date_iso}, already in archive") return archive selected_topics = topics.pick_topics(day) theme = random.Random(f"theme-{date_iso}").choice(THEMES) log( f"generating edition for {date_iso} theme={theme} " f"topics={[t['name'] for t in selected_topics]}" ) llm_json = call_openrouter(api_key, model, selected_topics) hero_image = None if pixazo_key: hero_image = generate_hero_image( pixazo_key, site_dir, date_iso, llm_json["image_prompt"], llm_json["image_negative_prompt"], ) if pixazo_pause > 0: log(f"pausing {pixazo_pause:.0f}s after Pixazo call") time.sleep(pixazo_pause) else: log("PIXAZO_API_KEY not set, skipping hero image") human_date = date_human(day) site_url = os.environ.get("SITE_URL", "https://mobea.de") startup_base = { "date": date_iso, "date_human": human_date, "theme": theme, "hero_image": hero_image, "s": llm_json, "site_url": site_url, } archiv_day_html = render_template( "startup.html.j2", {**startup_base, "canonical_path": f"/archiv/{date_iso}.html"}, ) archiv_day_path = site_dir / "archiv" / f"{date_iso}.html" write_text_atomic(archiv_day_path, archiv_day_html) log(f"wrote {archiv_day_path}") if update_index: startup_html = render_template( "startup.html.j2", {**startup_base, "canonical_path": "/"}, ) write_text_atomic(site_dir / "index.html", startup_html) log(f"wrote {site_dir / 'index.html'}") archive_entry = { "date": date_iso, "date_human": human_date, "backronym": " ".join(llm_json["backronym_words"]), "tagline": llm_json["tagline"], "theme": theme, "topics": [topic["name"] for topic in selected_topics], "has_image": hero_image is not None, "prompts": { "content_system": build_system_prompt(), "content_user": build_user_prompt(selected_topics), "image_prompt": llm_json["image_prompt"], "image_negative_prompt": llm_json["image_negative_prompt"], }, } archive = [entry for entry in archive if entry.get("date") != date_iso] archive.append(archive_entry) save_json_atomic(data_dir / "archive.json", archive) log(f"updated {data_dir / 'archive.json'}") return archive def main() -> int: parser = argparse.ArgumentParser(description="Backfill mobea archive editions") parser.add_argument("--from", dest="from_date", required=True, help="Start date (YYYY-MM-DD)") parser.add_argument("--to", dest="to_date", default=None, help="End date (YYYY-MM-DD), default today UTC") parser.add_argument("--site-dir", default=None) parser.add_argument("--data-dir", default=None) parser.add_argument("--env-file", default=None) parser.add_argument( "--pixazo-pause", type=float, default=30.0, help="Seconds to wait after each Pixazo image generation (default: 30)", ) args = parser.parse_args() repo_root = Path(__file__).resolve().parent.parent env_file = Path(args.env_file) if args.env_file else repo_root / ".env" load_env_file(env_file) api_key = os.environ.get("OPENROUTER_API_KEY") model = os.environ.get("OPENROUTER_MODEL") pixazo_key = os.environ.get("PIXAZO_API_KEY") site_dir = Path(args.site_dir or os.environ.get("SITE_DIR", repo_root / "public")) data_dir = Path(args.data_dir or os.environ.get("DATA_DIR", repo_root / "data")) if not api_key: log("error: OPENROUTER_API_KEY is required") return 1 if not model: log("error: OPENROUTER_MODEL is required") return 1 start = parse_date(args.from_date) end = parse_date(args.to_date) if args.to_date else datetime.now(timezone.utc).date() if start > end: log("error: --from must not be after --to") return 1 site_dir.mkdir(parents=True, exist_ok=True) data_dir.mkdir(parents=True, exist_ok=True) archive = load_archive(data_dir) current = start days: list[date] = [] while current <= end: days.append(current) current += timedelta(days=1) log(f"backfill {start.isoformat()} .. {end.isoformat()} ({len(days)} day(s))") for idx, day in enumerate(days): try: archive = generate_day( day, api_key, model, pixazo_key, site_dir, data_dir, archive, update_index=(idx == len(days) - 1), pixazo_pause=args.pixazo_pause, ) except Exception as exc: log(f"error: failed for {day.isoformat()}: {exc}") return 1 latest_image = next( ( f"/img/{entry['date']}.jpg" for entry in sorted(archive, key=lambda e: e["date"], reverse=True) if entry.get("has_image") ), "/og-default.jpg", ) archiv_html = render_template( "archiv.html.j2", { "entries": build_archiv_entries(archive, site_dir), "site_url": os.environ.get("SITE_URL", "https://mobea.de"), "og_image": latest_image, }, ) write_text_atomic(site_dir / "archiv" / "index.html", archiv_html) log(f"wrote {site_dir / 'archiv' / 'index.html'}") log("backfill complete") return 0 if __name__ == "__main__": sys.exit(main())