import requests import caldav from caldav.elements import dav, cdav from datetime import datetime import os import time import concurrent.futures # --- CONFIGURACIÓN --- # Default to 5 minutes SYNC_FREQUENCY_MINUTES = int(os.getenv("SYNC_FREQUENCY", 5)) SYNC_FREQUENCY_SECONDS = SYNC_FREQUENCY_MINUTES * 60 # Tu URL de Outlook ICS_URL = os.getenv("ICS_URL") # Tu Baïkal BAIKAL_URL = os.getenv("BAIKAL_URL") BAIKAL_USER = os.getenv("BAIKAL_USER") BAIKAL_PASS = os.getenv("BAIKAL_PASS") CALENDAR_ID = os.getenv("CALENDAR_ID") # Headers para parecer un navegador real y evitar 'Connection Reset' HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Unraid-Sync/1.0" } def delete_event(event): """Helper function to delete a single event.""" try: event.delete() return True except Exception as e: print(f"!!! Error deleting event {event}: {e}") return False def delete_all_events(calendar): """ Deletes all events in the calendar as fast as possible using threads. """ print("-> Buscando eventos para borrar...") try: events = calendar.events() except Exception as e: print(f"!!! Error al obtener eventos: {e}") return total_events = len(events) if total_events == 0: print("-> El calendario ya está vacío.") return print(f"-> Borrando {total_events} eventos rápidamente...") # Usamos ThreadPoolExecutor para borrar en paralelo deleted_count = 0 with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: results = list(executor.map(delete_event, events)) deleted_count = results.count(True) print(f"-> Limpieza completada. Borrados {deleted_count}/{total_events} eventos.") def sync(): if not all([ICS_URL, BAIKAL_URL, BAIKAL_USER, BAIKAL_PASS]): print(f"[{datetime.now()}] !!! Error: Faltan variables de entorno. Asegúrate de configurar ICS_URL, BAIKAL_URL, BAIKAL_USER y BAIKAL_PASS.") return print(f"[{datetime.now()}] Iniciando sincronización...") # 1. Descargar ICS de Outlook print("-> Descargando calendario de Outlook...") try: response = requests.get(ICS_URL, headers=HEADERS, timeout=30) response.raise_for_status() ics_data = response.text print(f"-> Descarga exitosa ({len(ics_data)} bytes).") except Exception as e: print(f"!!! Error descargando Outlook: {e}") return # 2. Conectar a Baïkal print("-> Conectando a Baïkal...") try: client = caldav.DAVClient( url=BAIKAL_URL, username=BAIKAL_USER, password=BAIKAL_PASS, headers=HEADERS, # Clave para evitar el bloqueo ssl_verify_cert=True # Cambiar a False si tienes problemas de certificado SSL auto-firmado ) principal = client.principal() calendars = principal.calendars() # Buscar el calendario correcto por ID si se proporciona calendar = None if CALENDAR_ID: print(f"-> Buscando calendario con ID: {CALENDAR_ID}") for cal in calendars: # Comprobamos si el ID está en la URL del calendario if CALENDAR_ID in str(cal.url): calendar = cal break if not calendar: print(f"!!! Error: No se encontró ningún calendario con el ID '{CALENDAR_ID}'. Calendarios disponibles:") for c in calendars: print(f" - {c.url}") return else: # Si no hay ID, usar el primero como antes if calendars: calendar = calendars[0] else: print("!!! No se encontró ningún calendario en esa URL.") return print(f"-> Calendario seleccionado: {calendar}") # 3. Borrar eventos antiguos (NUEVO) delete_all_events(calendar) # 4. Importar eventos print("-> Procesando archivo ICS...") from icalendar import Calendar cal = Calendar.from_ical(ics_data) events = cal.walk('vevent') total_events = len(events) print(f"-> Encontrados {total_events} eventos para importar.") success_count = 0 error_count = 0 for i, component in enumerate(events, 1): try: # Intentamos pasar el string decodificado calendar.add_event(component.to_ical().decode('utf-8')) success_count += 1 except Exception as ev_err: error_count += 1 # Solo imprimimos los primeros 5 errores para no saturar if error_count <= 5: summary = component.get('summary', 'sin titulo') print(f"!!! Error ({i}/{total_events}) '{summary}': {ev_err}") # Print progress every 50 events if i % 50 == 0: print(f" Procesados {i}/{total_events} (Exitos: {success_count}, Errores: {error_count})") print(f"-> ¡Sincronización finalizada! Éxitos: {success_count}, Errores: {error_count}") except Exception as e: print(f"!!! Error en Baïkal: {e}") if __name__ == "__main__": print(f"Iniciando servicio de sincronización. Frecuencia: {SYNC_FREQUENCY_MINUTES} minutos ({SYNC_FREQUENCY_SECONDS} segundos).") while True: sync() print(f"[{datetime.now()}] Durmiendo {SYNC_FREQUENCY_MINUTES} minutos...") time.sleep(SYNC_FREQUENCY_SECONDS)