""" main.py ======= Container entry point for the Kavita light-novel metadata fetcher. Reads configuration from environment variables, starts the orchestrator and exposes the Flask WebApp on WEB_HOST:WEB_PORT. Everything happens through HTTP — there is no folder watcher and no file mover (Kavita is the source of truth for the library content; this service only writes metadata back to it). Environment variables --------------------- Required: KAVITA_URL base URL of the Kavita server, e.g. http://kavita:5000 KAVITA_API_KEY Kavita API key (Settings -> User -> API key) Optional: LIBRARY_IDS comma-separated default library ids (e.g. "3,5"). Empty = user picks in the WebUI each time. LANGUAGE default "en" REQUEST_TIMEOUT default 30 MATCH_PATH default /config/matches.json WEB_PORT default 8080 WEB_HOST default 0.0.0.0 """ from __future__ import annotations import os import sys from pathlib import Path try: from dotenv import load_dotenv load_dotenv() except ImportError: pass # Make src/ importable when running as `python main.py`. sys.path.insert(0, str(Path(__file__).resolve().parent / "src")) from src.MatchesCache import MatchesCache # noqa: E402 from src.LightNovelOrchestrator import LightNovelOrchestrator # noqa: E402 from src.MatchesWebApp import MatchesWebApp # noqa: E402 def _env_str(name: str, default: "str | None" = None, required: bool = False) -> "str | None": value = os.environ.get(name, default) if required and not value: print(f"[main] missing required env var: {name}", flush=True) sys.exit(2) return value def _env_int(name: str, default: int) -> int: raw = os.environ.get(name) if raw is None or raw == "": return default try: return int(raw) except ValueError: print(f"[main] {name}={raw!r} is not a valid integer; " f"falling back to {default}", flush=True) return default def _env_int_list(name: str) -> list[int]: raw = os.environ.get(name) or "" out: list[int] = [] for part in raw.split(","): part = part.strip() if not part: continue try: out.append(int(part)) except ValueError: print(f"[main] {name}: ignoring non-integer value {part!r}", flush=True) return out def main() -> int: kavita_url = _env_str("KAVITA_URL", required=True) kavita_api_key = _env_str("KAVITA_API_KEY", required=True) language = _env_str("LANGUAGE", "en") or "en" request_timeout = _env_int("REQUEST_TIMEOUT", 30) match_path = _env_str("MATCH_PATH", "/config/matches.json") web_host = _env_str("WEB_HOST", "0.0.0.0") or "0.0.0.0" web_port = _env_int("WEB_PORT", 8080) library_ids = _env_int_list("LIBRARY_IDS") print(f"[main] kavita url = {kavita_url}", flush=True) print(f"[main] language = {language}", flush=True) print(f"[main] match path = {match_path}", flush=True) print(f"[main] libraries = {library_ids or '(picked in WebUI)'}", flush=True) print(f"[main] web = {web_host}:{web_port}", flush=True) cache = MatchesCache(match_path) orchestrator = LightNovelOrchestrator( kavita_url=kavita_url, kavita_api_key=kavita_api_key, matches_cache=cache, language=language, request_timeout=request_timeout, ) app = MatchesWebApp( cache, orchestrator=orchestrator, default_library_ids=library_ids, host=web_host, port=web_port, ) app.start() app.wait() return 0 if __name__ == "__main__": sys.exit(main())