#!/usr/bin/env python3
import sys
from pathlib import Path
from datetime import datetime, date, timedelta
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}
---
_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:
events = split_multiday_events(recurring_ical_events.of(calendar).after(datetime.now()))
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()