fix: stable event hashing, HEAD fallback, caldav str/bytes compat

This commit is contained in:
2026-06-11 23:06:47 +02:00
parent 0f390ff1e1
commit d628f03a82
3 changed files with 52 additions and 9 deletions
+10 -3
View File
@@ -5,8 +5,11 @@ from typing import Any
logger = logging.getLogger(__name__)
def _extract_uid(ical_data: bytes) -> str:
for line in ical_data.decode("utf-8", errors="replace").split("\r\n"):
def _extract_uid(ical_data) -> str:
raw = ical_data
if isinstance(raw, bytes):
raw = raw.decode("utf-8", errors="replace")
for line in str(raw).split("\r\n"):
if line.upper().startswith("UID:"):
return line[4:].strip()
return ""
@@ -15,7 +18,11 @@ def _extract_uid(ical_data: bytes) -> str:
def _find_event_by_uid(calendar: Any, uid: str) -> Any | None:
try:
for event in calendar.events():
content = event.data.decode("utf-8", errors="replace")
raw = event.data
if isinstance(raw, bytes):
content = raw.decode("utf-8", errors="replace")
else:
content = str(raw)
for line in content.split("\r\n"):
if line.upper().startswith("UID:"):
if line[4:].strip() == uid:
+14 -3
View File
@@ -5,6 +5,19 @@ from icalendar import Calendar
logger = logging.getLogger(__name__)
def _stable_event_key(component) -> str:
event_bytes = component.to_ical()
text = event_bytes.decode("utf-8", errors="replace")
stable_lines = []
for line in text.split("\r\n"):
key = line.split(":")[0].upper() if ":" in line else ""
if key in ("DTSTAMP", "LAST-MODIFIED"):
continue
stable_lines.append(line)
stable_text = "\r\n".join(stable_lines)
return hashlib.sha256(stable_text.encode("utf-8")).hexdigest()
def parse_ics_events(ics_text: str) -> dict[str, str]:
cal = Calendar.from_ical(ics_text.encode() if isinstance(ics_text, str) else ics_text)
result = {}
@@ -17,9 +30,7 @@ def parse_ics_events(ics_text: str) -> dict[str, str]:
continue
uid_str = str(uid)
try:
event_bytes = component.to_ical()
file_hash = hashlib.sha256(event_bytes).hexdigest()
result[uid_str] = file_hash
result[uid_str] = _stable_event_key(component)
except Exception as e:
logger.warning("Failed to process event %s: %s", uid_str, e)
return result
+28 -3
View File
@@ -56,9 +56,14 @@ def sync_once(state: SyncState, health: HealthServer, config: Config) -> bool:
logger.info("Starting sync cycle...")
try:
r = requests.head(config.ics_url, headers=HEADERS, timeout=30, allow_redirects=True)
r.raise_for_status()
remote_etag = r.headers.get("ETag")
remote_etag = None
try:
r = requests.head(config.ics_url, headers=HEADERS, timeout=15, allow_redirects=True)
if r.status_code < 400:
remote_etag = r.headers.get("ETag")
except Exception:
pass
cached_hash, cached_etag, _ = state.get_ics_cache()
if remote_etag and cached_etag == remote_etag:
@@ -91,6 +96,8 @@ def sync_once(state: SyncState, health: HealthServer, config: Config) -> bool:
to_update = deltas["to_update"]
to_delete = deltas["to_delete"]
is_first_run = not state.get_event_uids() and not cached_hash
if not to_add and not to_update and not to_delete:
logger.info("Calendar is already in sync.")
duration = time.time() - start_time
@@ -100,6 +107,24 @@ def sync_once(state: SyncState, health: HealthServer, config: Config) -> bool:
True,
len(ics_uids),
)
for uid, h in ics_uids.items():
state.upsert_event(uid, h)
return True
if is_first_run and not to_delete:
logger.info(
"First run detected: %d events in ICS. Registering state without re-adding to calendar.",
len(to_add),
)
for uid, h in ics_uids.items():
state.upsert_event(uid, h)
duration = time.time() - start_time
health.update_status(
datetime.now(timezone.utc),
duration,
True,
len(ics_uids),
)
return True
logger.info(