Skip to content

Instantly share code, notes, and snippets.

@thomascamminady
Created April 28, 2025 08:58
Show Gist options
  • Save thomascamminady/207bed0085cfc171e80127a038e8b271 to your computer and use it in GitHub Desktop.
Save thomascamminady/207bed0085cfc171e80127a038e8b271 to your computer and use it in GitHub Desktop.
Export WellFit course plan to ICS
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "beautifulsoup4",
# "requests",
# "icalendar"
# ]
# ///
# pyright: reportMissingModuleSource=false
# pyright: reportMissingImports=false
from datetime import datetime, timedelta
import requests
from bs4 import BeautifulSoup
from icalendar import Calendar, Event, vRecur
def next_date_by_weekday(start_date: datetime, target_weekday: int) -> datetime:
"""Return the next date after start_date that falls on target_weekday (0=Monday, …, 6=Sunday).
If start_date is already that weekday, returns one week later.
"""
days_ahead = (target_weekday - start_date.weekday() + 7) % 7
days_ahead = days_ahead or 7
return start_date + timedelta(days=days_ahead)
def main(
url: str = "https://www.robinson-wellfit-bonn.de/bonn/kurse/#kursplan",
out_filename: str = "courses.ics",
) -> None:
# 1) Fetch and parse
resp = requests.get(url, timeout=20)
resp.raise_for_status()
soup = BeautifulSoup(resp.content, "html.parser")
# 2) Prepare the calendar
cal = Calendar()
cal.add("prodid", "-//Robinson Wellfit Bonn Course Schedule//")
cal.add("version", "2.0")
# German → (python weekday, ICS BYDAY code)
weekday_map = {
"Montag": (0, "MO"),
"Dienstag": (1, "TU"),
"Mittwoch": (2, "WE"),
"Donnerstag": (3, "TH"),
"Freitag": (4, "FR"),
"Samstag": (5, "SA"),
"Sonntag": (6, "SU"),
}
today = datetime.now()
# 3) Locate each weekday container
containers = soup.find_all("div", class_="fs-load-more-item")
for cont in containers:
h3 = cont.find("h3", class_="el-title")
if not h3:
continue
day_name = h3.get_text(strip=True)
if day_name not in weekday_map:
continue
wd_num, ics_byday = weekday_map[day_name]
event_date = next_date_by_weekday(today, wd_num)
# 4) Extract each class card
cards = cont.select("div.el-item.uk-card")
for card in cards:
# Time range
time_txt = card.find("div", class_="el-meta").get_text(strip=True)
start_s, end_s = time_txt.replace("Uhr", "").split("–")
t0 = datetime.strptime(start_s.strip(), "%H:%M").time()
t1 = datetime.strptime(end_s.strip(), "%H:%M").time()
dtstart = datetime.combine(event_date.date(), t0)
dtend = datetime.combine(event_date.date(), t1)
summary = card.find("div", class_="el-title").get_text(strip=True)
location = card.find("div", class_="el-content").get_text(
strip=True
)
# Build the event
ev = Event()
ev.add("summary", summary)
ev.add("dtstart", dtstart)
ev.add("dtend", dtend)
ev.add("location", location)
# Repeat weekly forever
ev.add(
"rrule",
vRecur(
{
"FREQ": "WEEKLY",
"BYDAY": [ics_byday],
}
),
)
cal.add_component(ev)
# 5) Ensure no VALARM components remain
for vevent in cal.walk("VEVENT"):
vevent.subcomponents[:] = [
sub for sub in vevent.subcomponents if sub.name != "VALARM"
]
# 6) Write out .ics file
with open(out_filename, "wb") as f:
f.write(cal.to_ical())
print(
f"✅ Written full weekly schedule (no notifications) to {out_filename}"
)
if __name__ == "__main__":
main()
@thomascamminady
Copy link
Author

uv run export_courseplan.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment