import os import re import tempfile from datetime import datetime, timedelta from zoneinfo import ZoneInfo import requests from bs4 import BeautifulSoup AJAX_URL = "https://www.igmg.org/wp-content/themes/igmg/include/gebetskalender_ajax_api.php" TZ_NAME = os.environ.get("TZ", "Europe/Berlin") TZ = ZoneInfo(TZ_NAME) CITY_ID = os.environ.get("CITY_ID", "20020") CITY_SLUG = os.environ.get("CITY_SLUG", "Blumberg_(DE)") LANG = os.environ.get("LANG", "de") ICS_FILENAME = os.environ.get("ICS_FILENAME", f"igmg-{CITY_ID}.ics") OUT_ICS = f"/data/{ICS_FILENAME}" OUT_INDEX = "/data/index.html" SOURCE_PAGE = f"https://www.igmg.org/gebetskalender?stadt={CITY_SLUG}&id={CITY_ID}" DATE_RE = re.compile(r"^\s*(\d{2})\.(\d{2})\.(\d{4})\s*$") TIME_RE = re.compile(r"^([01]?\d|2[0-3]):[0-5]\d$") PRAYERS = [ ("imsak_time", "Imsak"), ("gunes_time", "Sonnenaufgang"), ("ogle_time", "Dhuhr / Mittag"), ("ikindi_time", "Asr / Nachmittag"), ("aksam_time", "Maghrib / Abend"), ("yatsi_time", "Isha / Nacht"), ] def fetch_month(year: int, month: int) -> str: data = { "show_ajax_variable": CITY_ID, "show_month": str(month), "show_year": str(year), "lang": LANG, } headers = { "User-Agent": "igmg-ics/1.0", "X-Requested-With": "XMLHttpRequest", "Referer": "https://www.igmg.org/gebetskalender/", } r = requests.post(AJAX_URL, data=data, headers=headers, timeout=30) r.raise_for_status() return r.text def ics_escape(s: str) -> str: return ( s.replace("\\", "\\\\") .replace(";", "\\;") .replace(",", "\\,") .replace("\n", "\\n") ) def fmt_dt(dt: datetime) -> str: return dt.strftime("%Y%m%dT%H%M%S") def build_ics(calname: str, events: list[dict]) -> str: now_utc = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") lines = [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//igmg-ics//Prayer Times//DE", "CALSCALE:GREGORIAN", "METHOD:PUBLISH", f"X-WR-CALNAME:{ics_escape(calname)}", f"X-WR-TIMEZONE:{TZ_NAME}", "REFRESH-INTERVAL;VALUE=DURATION:P1D", "X-PUBLISHED-TTL:P1D", ] for e in events: lines += [ "BEGIN:VEVENT", f"UID:{e['uid']}", f"DTSTAMP:{now_utc}", f"SUMMARY:{ics_escape(e['summary'])}", f"DTSTART;TZID={TZ_NAME}:{fmt_dt(e['start'])}", f"DTEND;TZID={TZ_NAME}:{fmt_dt(e['end'])}", f"DESCRIPTION:{ics_escape(e['description'])}", "END:VEVENT", ] lines.append("END:VCALENDAR") return "\r\n".join(lines) + "\r\n" def parse_events(html: str) -> tuple[str, list[dict]]: soup = BeautifulSoup(html, "lxml") calname = "IGMG Gebetszeiten" h3 = soup.select_one("h3.green") if h3 and h3.get_text(strip=True): calname = f"IGMG {h3.get_text(strip=True)} Gebetszeiten" rows = soup.select("div.zeiten_box > div.zeiten") if not rows: raise RuntimeError("Keine 'zeiten' Zeilen gefunden (AJAX-Format geändert?).") events: list[dict] = [] for row in rows: date_span = row.select_one("span.tarih") if not date_span: continue date_text = date_span.get_text(strip=True) m = DATE_RE.match(date_text) if not m: continue # Header-Zeile ("Datum") dd, mm, yyyy = map(int, m.groups()) for cls, label in PRAYERS: tspan = row.select_one(f"span.{cls}") if not tspan: continue ttxt = tspan.get_text(strip=True) if not TIME_RE.match(ttxt): continue hh, minute = map(int, ttxt.split(":")) start = datetime(yyyy, mm, dd, hh, minute, tzinfo=TZ) end = start + timedelta(minutes=10) # stabile UID => keine Duplikate in Kalender-Abos uid = f"igmg-{CITY_ID}-{yyyy:04d}{mm:02d}{dd:02d}-{cls}@igmg-ics" events.append( { "uid": uid, "summary": label, "start": start, "end": end, "description": f"Quelle: {SOURCE_PAGE}", } ) if not events: raise RuntimeError("Zeilen gefunden, aber keine Events extrahiert.") return calname, events def month_add(year: int, month: int, delta: int) -> tuple[int, int]: m = month - 1 + delta y = year + (m // 12) m = (m % 12) + 1 return y, m def atomic_write(path: str, content: str): os.makedirs(os.path.dirname(path), exist_ok=True) fd, tmp_path = tempfile.mkstemp(prefix=os.path.basename(path) + ".", dir=os.path.dirname(path)) try: with os.fdopen(fd, "w", encoding="utf-8", newline="") as f: f.write(content) os.replace(tmp_path, path) finally: try: if os.path.exists(tmp_path): os.remove(tmp_path) except Exception: pass def main(): now = datetime.now(TZ) y, m = now.year, now.month # aktueller + nächster Monat (Abo hat “Vorlauf”) months = [(y, m), month_add(y, m, 1)] all_events: list[dict] = [] calname_final = None for yy, mm in months: html = fetch_month(yy, mm) calname, events = parse_events(html) calname_final = calname_final or calname all_events.extend(events) # dedupe nach UID unique = {e["uid"]: e for e in all_events} all_events = list(unique.values()) all_events.sort(key=lambda e: e["start"]) ics = build_ics(calname_final or "IGMG Gebetszeiten", all_events) atomic_write(OUT_ICS, ics) # simple Landing-Page updated = datetime.now(TZ).strftime("%Y-%m-%d %H:%M %Z") index = f"""
Letztes Update: {updated}
Quelle: {SOURCE_PAGE}
""" atomic_write(OUT_INDEX, index) print(f"[ok] wrote {OUT_ICS} ({len(all_events)} events) months={months}") if __name__ == "__main__": main()