| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- # -*- coding: utf-8 -*-
- """
- Sync-State fuer die EPG-Synchronisation.
- Verfolgt den Zustand der EPG-Aktualisierung und Enrichment-Warteschlange,
- um nach Neustarts nahtlos fortzufahren.
- """
- from __future__ import annotations
- import json
- from datetime import datetime, timedelta
- from pathlib import Path
- from trixy_core.utils.debug import pdebug, perror, pwarn
- class SyncState:
- """
- Persistenter Zustand der EPG-Synchronisation.
- Speichert Zeitstempel der letzten Aktualisierungen, die
- Enrichment-Warteschlange und bereits angereicherte Titel.
- """
- FILE_NAME = "sync_state.json"
- def __init__(self, cache_dir: Path) -> None:
- """
- Args:
- cache_dir: Verzeichnis fuer die State-Datei.
- """
- self._cache_dir = cache_dir
- self._file_path = cache_dir / self.FILE_NAME
- # Zustandsfelder
- self.last_epg_fetch: str = ""
- self.last_enrichment: str = ""
- self.enrichment_queue: list[str] = []
- self.enriched_titles: dict[str, str] = {}
- self.next_sync_after: str = ""
- # Verzeichnis erstellen
- self._cache_dir.mkdir(parents=True, exist_ok=True)
- def load(self) -> None:
- """
- Laedt den Sync-State aus der JSON-Datei.
- Toleriert fehlende oder beschaedigte Dateien und setzt
- dann die Standardwerte.
- """
- if not self._file_path.exists():
- pdebug("SyncState: Keine State-Datei vorhanden, starte mit Standardwerten")
- return
- try:
- raw = self._file_path.read_text(encoding="utf-8")
- data = json.loads(raw)
- self.last_epg_fetch = data.get("last_epg_fetch", "")
- self.last_enrichment = data.get("last_enrichment", "")
- self.enrichment_queue = data.get("enrichment_queue", [])
- self.enriched_titles = data.get("enriched_titles", {})
- self.next_sync_after = data.get("next_sync_after", "")
- pdebug(
- f"SyncState: Geladen - "
- f"Letzter EPG-Fetch: {self.last_epg_fetch or 'nie'}, "
- f"Queue: {len(self.enrichment_queue)} Eintraege"
- )
- except (json.JSONDecodeError, TypeError, KeyError) as e:
- pwarn(f"SyncState: Fehler beim Laden, setze Standardwerte: {e}")
- self.last_epg_fetch = ""
- self.last_enrichment = ""
- self.enrichment_queue = []
- self.enriched_titles = {}
- self.next_sync_after = ""
- def save(self) -> None:
- """
- Speichert den Sync-State atomar in die JSON-Datei.
- Schreibt zuerst in eine temporaere Datei und benennt dann um.
- """
- data = {
- "last_epg_fetch": self.last_epg_fetch,
- "last_enrichment": self.last_enrichment,
- "enrichment_queue": self.enrichment_queue,
- "enriched_titles": self.enriched_titles,
- "next_sync_after": self.next_sync_after,
- }
- tmp_path = self._file_path.with_suffix(".tmp")
- try:
- tmp_path.write_text(
- json.dumps(data, ensure_ascii=False, indent=2),
- encoding="utf-8",
- )
- tmp_path.replace(self._file_path)
- pdebug("SyncState: State gespeichert")
- except OSError as e:
- perror(f"SyncState: Fehler beim Speichern: {e}")
- def needs_epg_fetch(self) -> bool:
- """
- Prueft, ob eine neue EPG-Aktualisierung noetig ist.
- Gibt True zurueck, wenn noch nie synchronisiert wurde oder
- der naechste geplante Sync-Zeitpunkt in der Vergangenheit liegt.
- Returns:
- True, wenn ein EPG-Fetch durchgefuehrt werden sollte.
- """
- if not self.last_epg_fetch:
- return True
- if not self.next_sync_after:
- return True
- try:
- next_sync = datetime.fromisoformat(self.next_sync_after)
- return datetime.now().astimezone() >= next_sync
- except ValueError:
- # Ungueltiger Zeitstempel, sicherheitshalber neu laden
- return True
- def needs_enrichment(self) -> bool:
- """
- Prueft, ob Enrichment-Aufgaben in der Warteschlange sind.
- Returns:
- True, wenn die Warteschlange nicht leer ist.
- """
- return len(self.enrichment_queue) > 0
- def mark_epg_fetched(
- self,
- retention_days: int,
- min_interval_hours: int,
- ) -> None:
- """
- Markiert den EPG-Fetch als abgeschlossen und plant den naechsten.
- Args:
- retention_days: Wie viele Tage EPG-Daten vorgehalten werden.
- min_interval_hours: Minimales Intervall bis zum naechsten Fetch.
- """
- now = datetime.now().astimezone()
- self.last_epg_fetch = now.isoformat()
- self.next_sync_after = (now + timedelta(hours=min_interval_hours)).isoformat()
- pdebug(
- f"SyncState: EPG-Fetch abgeschlossen. "
- f"Naechster Sync nach: {self.next_sync_after}"
- )
- self.save()
- def mark_enrichment_done(self) -> None:
- """Markiert die aktuelle Enrichment-Runde als abgeschlossen."""
- self.last_enrichment = datetime.now().astimezone().isoformat()
- pdebug("SyncState: Enrichment-Runde abgeschlossen")
- self.save()
- def is_enriched(self, series_id: str) -> bool:
- """Prueft ob eine Serie bereits angereichert wurde."""
- return series_id in self.enriched_titles
- def is_in_enrichment_queue(self, series_id: str) -> bool:
- """Prueft ob eine Serie in der Enrichment-Queue ist."""
- return series_id in self.enrichment_queue
- def remove_from_enrichment_queue(self, series_id: str) -> None:
- """Entfernt eine Serie aus der Enrichment-Queue."""
- if series_id in self.enrichment_queue:
- self.enrichment_queue.remove(series_id)
- self.save()
- def add_to_enrichment_queue(self, series_ids: list[str] | str) -> None:
- """
- Fuegt Serien-IDs zur Enrichment-Warteschlange hinzu.
- IDs, die bereits angereichert wurden, werden uebersprungen.
- Args:
- series_ids: Liste von Serien-IDs oder einzelne ID.
- """
- if isinstance(series_ids, str):
- series_ids = [series_ids]
- added = 0
- existing_set = set(self.enrichment_queue)
- for series_id in series_ids:
- # Bereits angereichert oder schon in der Queue -> ueberspringen
- if series_id in self.enriched_titles:
- continue
- if series_id in existing_set:
- continue
- self.enrichment_queue.append(series_id)
- existing_set.add(series_id)
- added += 1
- if added > 0:
- pdebug(f"SyncState: {added} Serien zur Enrichment-Queue hinzugefuegt")
- self.save()
- def mark_title_enriched(self, series_id: str, source: str) -> None:
- """
- Markiert eine Serie als angereichert.
- Entfernt die ID aus der Warteschlange und fuegt sie zu den
- bereits angereicherten Titeln hinzu.
- Args:
- series_id: Die Serien-ID.
- source: Quelle der Enrichment-Daten (z.B. "tmdb", "omdb").
- """
- # Aus der Queue entfernen
- if series_id in self.enrichment_queue:
- self.enrichment_queue.remove(series_id)
- # Als angereichert markieren
- self.enriched_titles[series_id] = source
- pdebug(f"SyncState: '{series_id}' als angereichert markiert (Quelle: {source})")
- self.save()
|