fix calendar export

This commit is contained in:
XenGi 2026-08-25 09:35:12 +02:00
commit 0c25d416c6
Signed by: xengi
SSH key fingerprint: SHA256:jxWM2RTHvxxcncXycwwWkP7HCWb4VREN05UGJTbIPZg

View file

@ -1,5 +1,4 @@
import argparse
import os
import re
from pathlib import Path
@ -12,7 +11,12 @@ def safe_filename(name: str) -> str:
return re.sub(r"[^a-zA-Z0-9._-]+", "_", name)
def export_calendar(url: str, username: str, password: str, calendar: str) -> Calendar:
def export_calendar(
url: str,
username: str,
password: str,
calendar: str,
) -> Calendar:
client = DAVClient(
url=url,
username=username,
@ -58,13 +62,45 @@ def export_calendar(url: str, username: str, password: str, calendar: str) -> Ca
cal = Calendar.from_ical(data)
for component in cal.walk():
if component.name != "VCALENDAR":
result.add_component(component)
# Only copy top-level components.
#
# Do NOT use cal.walk() here. walk() also returns the
# STANDARD/DAYLIGHT components inside VTIMEZONE.
for component in cal.subcomponents:
result.add_component(component)
return result
def add_normalized_components(
target: Calendar,
source: Calendar,
timezones: dict[str, object],
) -> None:
"""
Add components from source to target while collecting VTIMEZONE
components separately.
Exactly one VTIMEZONE is retained for each TZID.
"""
for component in source.subcomponents:
if component.name == "VTIMEZONE":
tzid = component.get("TZID")
if tzid is None:
print(" warning: ignoring VTIMEZONE without TZID")
continue
tzid = str(tzid)
if tzid not in timezones:
timezones[tzid] = component
else:
target.add_component(component)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("config")
@ -81,12 +117,16 @@ def main():
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
username = config["username"]
password = config["password"]
combined = Calendar()
combined.add("prodid", "-//Combined calendar//")
combined.add("version", "2.0")
username = config["username"]
password = config["password"]
# TZID -> VTIMEZONE component
timezones = {}
for entry in config["calendars"]:
cal = export_calendar(
entry["url"],
@ -97,19 +137,37 @@ def main():
filename = output_dir / f"{safe_filename(entry['calendar'])}.ics"
# Individual calendar export stays as-is.
with open(filename, "wb") as f:
f.write(cal.to_ical())
print(f" -> {filename}")
for component in cal.walk():
if component.name != "VCALENDAR":
combined.add_component(component)
# Normalize components for the combined calendar.
add_normalized_components(
combined,
cal,
timezones,
)
# Add each VTIMEZONE exactly once.
#
# Put them before events/components in the resulting VCALENDAR.
normalized = Calendar()
normalized.add("prodid", "-//Combined calendar//")
normalized.add("version", "2.0")
for tzid, timezone in timezones.items():
print(f" timezone: {tzid}")
normalized.add_component(timezone)
for component in combined.subcomponents:
normalized.add_component(component)
combined_file = output_dir / "all.ics"
with open(combined_file, "wb") as f:
f.write(combined.to_ical())
f.write(normalized.to_ical())
print()
print(f"Combined export: {combined_file}")