#!/usr/bin/env python3 import sys from pathlib import Path from datetime import datetime, date, timedelta, timezone from typing import Optional, NamedTuple from dataclasses import dataclass from copy import deepcopy import requests from bs4 import BeautifulSoup from icalendar import Calendar, Event import recurring_ical_events import locale #This is duplicated code from calendar-fetcher.py def split_multiday_events(events): """ Split multi-day events into separate daily events. Args: events: Iterator of iCal events Returns: List of events where multi-day events are split into daily events """ split_events = [] for event in events: # Get start and end dates start = event.get('dtstart').dt end = event.get('dtend').dt if event.get('dtend') else start # Convert datetime to date if needed if isinstance(start, datetime): start = start.date() if isinstance(end, datetime): end = end.date() # If it's a single day event or not a date/datetime, add as is if not isinstance(start, date) or not isinstance(end, date) or start == end: split_events.append(event) continue # For multi-day events, create separate events for each day current_date = start while current_date < end: # Create a copy of the original event daily_event = Event() for key in event: daily_event[key] = deepcopy(event[key]) # Update the date for this instance daily_event['dtstart'].dt = current_date if 'dtend' in daily_event: daily_event['dtend'].dt = current_date + timedelta(days=1) split_events.append(daily_event) current_date += timedelta(days=1) return split_events @dataclass class EventDetails: weekday: str date: str time: str summary: str location: str def to_html(self) -> str: return ( f'
{self.weekday} ' f'{self.date}{self.time}, {self.summary}' f'{self.location}
' ) class EventProcessor: def __init__(self, ics_url: str, content_file: str): self.ics_url = ics_url self.content_file = Path(content_file) self.fallback_html = ( '
Leider unbekannt, aber ' 'frag mal den Vorstand der müsste es wissen
' ) @property def fallback_content(self) -> str: return f"""_model: htmlpage --- title: Willkommen beim PC Stammertal --- html:

Unser nächster Anlass:


{self.fallback_html}
Terminkalender All unsere Termine
Buch Alle Infos über uns
Briefe Kontaktiere uns
--- _template: page.html """ def setup_locale(self) -> None: try: locale.setlocale(locale.LC_TIME, 'de_DE.UTF-8') except locale.Error as e: print(f"Warning: Failed to set locale: {e}", file=sys.stderr) def fetch_calendar(self) -> Optional[Calendar]: try: response = requests.get(self.ics_url, timeout=10) response.raise_for_status() return Calendar.from_ical(response.content) except (requests.RequestException, ValueError) as e: print(f"Error fetching calendar: {e}", file=sys.stderr) return None def format_event_time(self, start: datetime | date) -> str: return "" if isinstance(start, date) and not isinstance(start, datetime) else start.strftime(" %-H:%M") def get_next_event(self, calendar: Calendar) -> Optional[EventDetails]: try: now=datetime.now(timezone.utc) # + timedelta(days=1) raw_events = split_multiday_events(recurring_ical_events.of(calendar).after(now)) now_date = now.date() now_datetime = now events = [] for raw_event in raw_events: start = raw_event.get('dtstart').dt if isinstance(start, datetime): my_now = now_datetime else: my_now = now_date if start >= my_now: events.append(raw_event) event = events[0] start = event.get('dtstart').dt return EventDetails( weekday=start.strftime("%A"), date=start.strftime("%-d. %B"), time=self.format_event_time(start), summary=event.get('summary', ''), location=f", {event.get('location')}" if event.get('location') else "" ) except (StopIteration, AttributeError) as e: print(f"No upcoming events found: {e}", file=sys.stderr) return None def read_current_content(self) -> tuple[str, str]: try: content = self.content_file.read_text() soup = BeautifulSoup(content, 'html.parser') events = soup.find_all('div', {'class': 'nextevent'}) return content, str(events[0]) if len(events) == 1 else "" except (IOError, IndexError) as e: print(f"Error reading content file: {e}", file=sys.stderr) return "", "" def process(self) -> str: self.setup_locale() content, source_str = self.read_current_content() if not content: return self.fallback_content if len(source_str) > 0 and source_str in content: calendar = self.fetch_calendar() if not calendar: return content.replace(source_str, self.fallback_html).rstrip() event = self.get_next_event(calendar) if not event: return content.replace(source_str, self.fallback_html).rstrip() return content.replace(source_str, event.to_html()).rstrip() return self.fallback_content def main() -> None: if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} ", file=sys.stderr) sys.exit(1) processor = EventProcessor( ics_url=sys.argv[1], content_file="/opt/lektor/project/content/contents.lr" ) print(processor.process(), end='') print("") print("") if __name__ == "__main__": main()