fix calendar export
This commit is contained in:
parent
4a9e9a6c44
commit
0c25d416c6
1 changed files with 69 additions and 11 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -12,7 +11,12 @@ def safe_filename(name: str) -> str:
|
||||||
return re.sub(r"[^a-zA-Z0-9._-]+", "_", name)
|
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(
|
client = DAVClient(
|
||||||
url=url,
|
url=url,
|
||||||
username=username,
|
username=username,
|
||||||
|
|
@ -58,13 +62,45 @@ def export_calendar(url: str, username: str, password: str, calendar: str) -> Ca
|
||||||
|
|
||||||
cal = Calendar.from_ical(data)
|
cal = Calendar.from_ical(data)
|
||||||
|
|
||||||
for component in cal.walk():
|
# Only copy top-level components.
|
||||||
if component.name != "VCALENDAR":
|
#
|
||||||
result.add_component(component)
|
# 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
|
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():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("config")
|
parser.add_argument("config")
|
||||||
|
|
@ -81,12 +117,16 @@ def main():
|
||||||
output_dir = Path(args.output)
|
output_dir = Path(args.output)
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
username = config["username"]
|
||||||
|
password = config["password"]
|
||||||
|
|
||||||
combined = Calendar()
|
combined = Calendar()
|
||||||
combined.add("prodid", "-//Combined calendar//")
|
combined.add("prodid", "-//Combined calendar//")
|
||||||
combined.add("version", "2.0")
|
combined.add("version", "2.0")
|
||||||
|
|
||||||
username = config["username"]
|
# TZID -> VTIMEZONE component
|
||||||
password = config["password"]
|
timezones = {}
|
||||||
|
|
||||||
for entry in config["calendars"]:
|
for entry in config["calendars"]:
|
||||||
cal = export_calendar(
|
cal = export_calendar(
|
||||||
entry["url"],
|
entry["url"],
|
||||||
|
|
@ -97,19 +137,37 @@ def main():
|
||||||
|
|
||||||
filename = output_dir / f"{safe_filename(entry['calendar'])}.ics"
|
filename = output_dir / f"{safe_filename(entry['calendar'])}.ics"
|
||||||
|
|
||||||
|
# Individual calendar export stays as-is.
|
||||||
with open(filename, "wb") as f:
|
with open(filename, "wb") as f:
|
||||||
f.write(cal.to_ical())
|
f.write(cal.to_ical())
|
||||||
|
|
||||||
print(f" -> {filename}")
|
print(f" -> {filename}")
|
||||||
|
|
||||||
for component in cal.walk():
|
# Normalize components for the combined calendar.
|
||||||
if component.name != "VCALENDAR":
|
add_normalized_components(
|
||||||
combined.add_component(component)
|
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"
|
combined_file = output_dir / "all.ics"
|
||||||
|
|
||||||
with open(combined_file, "wb") as f:
|
with open(combined_file, "wb") as f:
|
||||||
f.write(combined.to_ical())
|
f.write(normalized.to_ical())
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(f"Combined export: {combined_file}")
|
print(f"Combined export: {combined_file}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue