Created
April 28, 2025 08:58
-
-
Save thomascamminady/207bed0085cfc171e80127a038e8b271 to your computer and use it in GitHub Desktop.
Export WellFit course plan to ICS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
Author
thomascamminady
commented
Apr 28, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment