Die layouts/_default/*.calendar.html-Vorlagen werden in Hugo
≥0.158 fälschlich für die HTML-Ausgabe ausgewählt, sodass alle
Sektions- und Einzelseiten VCALENDAR- statt HTML-Inhalt
enthielten. Die Vorlagen waren ohnehin nie funktionsfähig
(Warnung „found no layout file for calendar"); die ICS-Feeds
liefern die abschnittsspezifischen Vorlagen unter
layouts/{veranstaltungen,datengarten,page}/.
list.xml.html bekommt aus demselben Grund die korrekte Endung
.xml.
tools/gen_upcoming.py vergleicht Datumsangaben jetzt
zeitzonenneutral, damit Events mit Z-Suffix keinen TypeError
auslösen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
105 lines
3 KiB
Python
Executable file
105 lines
3 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import sys
|
|
import logging
|
|
import locale
|
|
from dateutil.parser import parse as _parse
|
|
from datetime import datetime, timedelta
|
|
from dateutil.rrule import rruleset, rrulestr
|
|
|
|
import icalendar
|
|
|
|
|
|
def parse(value):
|
|
# Some templates emit DTSTART with a trailing "Z" (e.g. 20260507T190000Z)
|
|
# which produces tz-aware datetimes, while others emit naive local times.
|
|
# Normalize to naive so comparisons and sorting don't mix the two.
|
|
dt = _parse(value)
|
|
if dt.tzinfo is not None:
|
|
dt = dt.replace(tzinfo=None)
|
|
return dt
|
|
|
|
|
|
def vevent_to_event(event, rrstart=None):
|
|
if rrstart == None:
|
|
begin = parse(event["DTSTART"].to_ical())
|
|
else:
|
|
begin = rrstart
|
|
|
|
return {
|
|
"name": event["SUMMARY"].to_ical(),
|
|
"url": event["URL"].to_ical(),
|
|
"begin": begin
|
|
}
|
|
|
|
|
|
def parse_single_event(event, start, end):
|
|
logging.info(f"Processing single event {event['SUMMARY'].to_ical().decode('utf-8')}")
|
|
dtstart = parse(event["DTSTART"].to_ical())
|
|
if dtstart >= start and dtstart < end:
|
|
return vevent_to_event(event)
|
|
|
|
|
|
def parse_recurring_event(event, start, end):
|
|
logging.info(f"Processing recurring event {event['SUMMARY'].to_ical().decode('utf-8')}")
|
|
dtstart = parse(event["DTSTART"].to_ical())
|
|
rs = rruleset()
|
|
rs.rrule(rrulestr(event["RRULE"].to_ical().decode("utf-8"), dtstart=dtstart))
|
|
if "EXDATE" in event.keys():
|
|
for exdate in event["EXDATE"]:
|
|
rs.exdate(parse(exdate.to_ical()))
|
|
|
|
events = []
|
|
for date in list(rs):
|
|
if date >= start and date < end:
|
|
events.append(vevent_to_event(event, date))
|
|
|
|
return events
|
|
|
|
|
|
def find_events(icsfilestr, start, end, num):
|
|
with open(icsfilestr, "r") as icsfile:
|
|
cal = icalendar.Calendar.from_ical(icsfile.read())
|
|
|
|
events = []
|
|
for event in cal.subcomponents:
|
|
if event.name == "VEVENT":
|
|
if "RRULE" in event.keys():
|
|
events.extend(parse_recurring_event(event, start, end))
|
|
else:
|
|
ev = parse_single_event(event, start, end)
|
|
if ev is not None:
|
|
events.append(ev)
|
|
|
|
events = sorted(events, key=lambda k: k["begin"])
|
|
events = events[0:num]
|
|
|
|
return events
|
|
|
|
|
|
def format_events(events):
|
|
print("<table class=\"table table-condensed\">")
|
|
for event in events:
|
|
print(
|
|
"<tr>"
|
|
f"<td>{event['begin'].strftime('%A, %d.%m um %H:%M Uhr')}</td>"
|
|
f"<td><a href=\"{event['url'].decode('utf-8')}\">{event['name'].decode('utf-8')}</a></td>"
|
|
"</tr>"
|
|
)
|
|
print("</table><!--/.table .table-condensed-->")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 3:
|
|
print(f"Usage: {sys.argv[0]} calendar max_days max_items")
|
|
sys.exit(-1)
|
|
|
|
locale.setlocale(locale.LC_TIME, "de_DE.UTF-8")
|
|
calendar = sys.argv[1]
|
|
max_days = int(sys.argv[2])
|
|
max_items = int(sys.argv[3])
|
|
|
|
now = datetime.now()
|
|
events = find_events(calendar, now, now + timedelta(days=max_days), max_items)
|
|
format_events(events)
|
|
|