fix: stable event hashing, HEAD fallback, caldav str/bytes compat
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user