fix: calendar recurrence tz/overrides, today habits schedule, list archive mobile access
ci / docker (push) Successful in 3m23s

This commit is contained in:
2026-09-06 11:33:21 +08:00
parent 0cc486069e
commit 4e888ffe1c
10 changed files with 166 additions and 20 deletions
+33 -11
View File
@@ -280,16 +280,19 @@ def _is_forbidden_ip(value: str) -> bool:
return ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved
def _as_utc(value, default_tz=LOCAL_TZ):
def _localized_ics_datetime(value, default_tz=LOCAL_TZ):
if isinstance(value, datetime):
if value.tzinfo is None:
value = value.replace(tzinfo=default_tz)
return value.astimezone(UTC), False
return value if value.tzinfo else value.replace(tzinfo=default_tz), False
if isinstance(value, date):
return datetime.combine(value, time.min, tzinfo=default_tz).astimezone(UTC), True
return datetime.combine(value, time.min, tzinfo=default_tz), True
raise ValueError("unsupported ICS datetime")
def _as_utc(value, default_tz=LOCAL_TZ):
localized, all_day = _localized_ics_datetime(value, default_tz)
return localized.astimezone(UTC), all_day
def _event_duration(event, starts_at: datetime, all_day: bool):
if event.get("dtend"):
ends_at, _ = _as_utc(event.decoded("dtend"))
@@ -334,19 +337,28 @@ def _overlaps(starts_at: datetime, ends_at: datetime | None, window_start: datet
def parse_ics_events(content: str, source_name: str, color: str, window_start: datetime | None = None, window_end: datetime | None = None):
calendar = Calendar.from_ical(content)
components = list(calendar.walk("VEVENT"))
overrides = {}
for event in components:
recurrence_id = event.get("recurrence-id")
if recurrence_id:
overrides[(str(event.get("uid") or ""), _as_utc(event.decoded("recurrence-id"))[0])] = event
events = []
for event in calendar.walk("VEVENT"):
if not event.get("dtstart"):
for event in components:
if not event.get("dtstart") or event.get("recurrence-id") or str(event.get("status") or "").upper() == "CANCELLED":
continue
starts_at, all_day = _as_utc(event.decoded("dtstart"))
localized_start, all_day = _localized_ics_datetime(event.decoded("dtstart"))
starts_at = localized_start.astimezone(UTC)
duration = _event_duration(event, starts_at, all_day)
excluded = _excluded_starts(event)
uid = str(event.get("uid") or "")
if event.get("rrule") and window_start and window_end:
rule_text = event.get("rrule").to_ical().decode()
rule = rrulestr(rule_text, dtstart=starts_at)
rule = rrulestr(rule_text, dtstart=localized_start)
for occurrence in rule.between(window_start - duration, window_end, inc=True):
occurrence = occurrence.astimezone(UTC) if occurrence.tzinfo else occurrence.replace(tzinfo=UTC)
if occurrence in excluded:
occurrence = occurrence.astimezone(UTC) if occurrence.tzinfo else occurrence.replace(tzinfo=LOCAL_TZ).astimezone(UTC)
if occurrence in excluded or (uid, occurrence) in overrides:
continue
ends_at = occurrence + duration
if _overlaps(occurrence, ends_at, window_start, window_end):
@@ -355,6 +367,16 @@ def parse_ics_events(content: str, source_name: str, color: str, window_start: d
ends_at = starts_at + duration
if _overlaps(starts_at, ends_at, window_start, window_end):
_append_calendar_event(events, source_name, color, event, starts_at, ends_at, all_day)
for event in overrides.values():
if not event.get("dtstart") or str(event.get("status") or "").upper() == "CANCELLED":
continue
starts_at, all_day = _as_utc(event.decoded("dtstart"))
duration = _event_duration(event, starts_at, all_day)
ends_at = starts_at + duration
if _overlaps(starts_at, ends_at, window_start, window_end):
_append_calendar_event(events, source_name, color, event, starts_at, ends_at, all_day)
return events
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,3 +1,3 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#f15a29"><link rel="manifest" href="/manifest.json"><title>dodo</title> <script type="module" crossorigin src="/assets/index-BhF1FT2t.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CDrLYTXE.css">
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#f15a29"><link rel="manifest" href="/manifest.json"><title>dodo</title> <script type="module" crossorigin src="/assets/index-BqVyvsa4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BBuUKFZL.css">
</head><body><div id="app"></div></body></html>