diff --git a/README.md b/README.md index 554ab58..a734f5e 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Die eigentliche iCal-Datei für Abos und direkten Download ist unter `/calendar. **Konfiguration (optional, Umgebungsvariablen):** - `KANTINE_BASE_URL` – Basis-URL der Kantine (Standard: `http://kantine-bhz.de`) -- `REFRESH_INTERVAL_SECONDS` – Sekunden zwischen Aktualisierungen (Standard: 86400 = 24 h) +- `REFRESH_INTERVAL_SECONDS` – Sekunden zwischen Aktualisierungen (Standard: 43200 = 12 h) - `PUBLIC_URL` – Öffentliche Basis-URL (z. B. `https://kantine.elpatron.me`). Wenn gesetzt, wird diese URL für die Kalender-Abo-URL auf der Startseite verwendet. **Empfohlen hinter HTTPS-Proxy**, falls der Proxy keine `X-Forwarded-Proto`/`X-Forwarded-Host`-Header sendet – sonst erscheint dort weiterhin `http://`. --- @@ -99,6 +99,9 @@ docker run -p 8000:8000 -e PUBLIC_URL=https://kantine.elpatron.me kantine2ical - **Startseite** (Anleitung + Abo-URL): `http://:8000/` bzw. Ihre HTTPS-URL - **iCal-Abo:** `http://:8000/calendar.ics` bzw. `https:///calendar.ics` +- **Manueller Refresh:** `GET /refresh` (z. B. `https:///refresh`) – löst sofort einen Abruf der Speiseplan-PDFs aus. Nützlich, wenn auf [kantine-bhz.de](http://kantine-bhz.de) ein neuer Plan liegt und der 24h-Refresh noch nicht gelaufen ist. Nach dem Aufruf enthält der Kalender die aktuellen Daten. + +**Kalender wird nicht aktualisiert?** Container-Logs prüfen (`docker logs `): Dort erscheinen gefundene PDFs und Fehler beim Laden/Parsen. Bei fehlgeschlagenem PDF wird die URL und die Exception geloggt. **Mit Docker Compose:** ```bash diff --git a/app.py b/app.py index f972f29..0a5ca8a 100644 --- a/app.py +++ b/app.py @@ -18,7 +18,7 @@ from kantine2ical import BASE_URL, empty_ical_bytes, refresh_speiseplan # Konfiguration (Umgebungsvariablen mit Fallback) KANTINE_BASE_URL = os.environ.get("KANTINE_BASE_URL", BASE_URL) -REFRESH_INTERVAL_SECONDS = int(os.environ.get("REFRESH_INTERVAL_SECONDS", "86400")) # 24h +REFRESH_INTERVAL_SECONDS = int(os.environ.get("REFRESH_INTERVAL_SECONDS", "43200")) # 12h # Öffentliche Basis-URL (z. B. https://kantine.elpatron.me), wenn Proxy keine X-Forwarded-* sendet PUBLIC_URL = os.environ.get("PUBLIC_URL", "").strip().rstrip("/") @@ -60,7 +60,11 @@ def _refresh_loop() -> None: # Beim Import: einmal Refresh, Hintergrund-Thread starten (gilt auch für Gunicorn) -logging.basicConfig(level=logging.INFO) +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) _log.info("Starte Speiseplan-Refresh beim Start ...") _do_refresh() _refresh_thread = threading.Thread(target=_refresh_loop, daemon=True) @@ -68,6 +72,18 @@ _refresh_thread.start() _log.info("Hintergrund-Refresh alle %s Sekunden.", REFRESH_INTERVAL_SECONDS) +@app.route("/refresh") +def refresh() -> Response: + """Manuellen Refresh auslösen (z. B. nach neuem Speiseplan auf kantine-bhz.de).""" + _log.info("Manueller Refresh ausgelöst") + _do_refresh() + last = _format_last_refresh() + return Response( + f"Refresh ausgeführt. Zuletzt aktualisiert: {last or '–'}.\n", + mimetype="text/plain; charset=utf-8", + ) + + @app.route("/calendar.ics") def calendar_ics() -> Response: """iCal-Kalender ausliefern (für Abo-URL z. B. in Google Kalender).""" diff --git a/kantine2ical.py b/kantine2ical.py index f0e3ddc..1ba013e 100644 --- a/kantine2ical.py +++ b/kantine2ical.py @@ -6,6 +6,7 @@ Termine täglich 12:00 mit allen Tagesgerichten (I. bis V.). import argparse import io +import logging import re from datetime import date, datetime, time from zoneinfo import ZoneInfo @@ -33,6 +34,7 @@ DATE_LINE_RE = re.compile( DISH_LINE_RE = re.compile(r"^\s*(IV\.?|V\.?|I{1,3}\.?)\s*(.*)$", re.IGNORECASE) ROMAN_ORDER = {"I": 1, "II": 2, "III": 3, "IV": 4, "V": 5} +_log = logging.getLogger(__name__) def fetch_speiseplan_pdf_urls(base_url: str = BASE_URL) -> list[str]: @@ -177,7 +179,9 @@ def refresh_speiseplan(base_url: str = BASE_URL) -> tuple[dict[date, list[str]], try: urls = fetch_speiseplan_pdf_urls(base_url) if not urls: + _log.warning("refresh_speiseplan: Keine PDF-URLs gefunden auf %s", base_url) return None + _log.info("refresh_speiseplan: %d PDF(s) gefunden", len(urls)) all_parsed: list[list[tuple[date, list[str]]]] = [] for url in urls: try: @@ -185,14 +189,19 @@ def refresh_speiseplan(base_url: str = BASE_URL) -> tuple[dict[date, list[str]], text = extract_text_from_pdf(pdf_bytes) days = parse_speiseplan_text(text) all_parsed.append(days) - except Exception: + _log.info("refresh_speiseplan: %s -> %d Tage", url.split("/")[-1], len(days)) + except Exception as e: + _log.exception("refresh_speiseplan: PDF fehlgeschlagen %s: %s", url, e) continue if not all_parsed: + _log.warning("refresh_speiseplan: Kein PDF erfolgreich gelesen") return None by_date = merge_day_events(all_parsed) ical_bytes = build_ical_bytes(by_date) + _log.info("refresh_speiseplan: %d Termine im Kalender", len(by_date)) return (by_date, ical_bytes) - except Exception: + except Exception as e: + _log.exception("refresh_speiseplan: Ablauf fehlgeschlagen: %s", e) return None