""" main_ln.py ========== Container entry point for the **light-novel** variant (Kavita metadata fetcher). The manga variant has its own entry point (main_manga.py); both share the modules in src/ and add their variant-specific code from src/ln/ resp. src/manga/. 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 UPDATER_ENABLED default true (run the person updater on cron) UPDATER_SCHEDULE cron expression for the person updater, default "0 10 * * 0" = Sundays 10:00 (local time — set TZ inside the container!) PERSON_PERF_PATH JSON file for person updater timing. Default /config/person_perf_stats.json """ from __future__ import annotations import os import sys from pathlib import Path try: from dotenv import load_dotenv load_dotenv() except ImportError: pass # Shared code in src/, LN-specific code in src/ln/. Modules are imported # by their plain names so src-internal imports resolve to the same module # objects (a `src.X` import would load everything twice). _BASE = Path(__file__).resolve().parent sys.path.insert(0, str(_BASE / "src")) sys.path.insert(0, str(_BASE / "src" / "ln")) from MatchesCache import MatchesCache # noqa: E402 from LightNovelOrchestrator import LightNovelOrchestrator # noqa: E402 from MatchesWebApp import MatchesWebApp # noqa: E402 from PerfStats import PerfStats # noqa: E402 from CronRunner import CronRunner # noqa: E402 def _env_bool(name: str, default: bool) -> bool: raw = os.environ.get(name) if raw is None: return default return raw.strip().lower() in ("1", "true", "yes", "y", "on") 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") updater_enabled = _env_bool("UPDATER_ENABLED", True) updater_schedule = _env_str("UPDATER_SCHEDULE", "0 10 * * 0") person_perf_path = _env_str("PERSON_PERF_PATH", "/config/person_perf_stats.json") or None 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) person_perf = PerfStats(person_perf_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, person_perf=person_perf, host=web_host, port=web_port, ) app.start() if updater_enabled: try: CronRunner( updater_schedule, lambda: orchestrator.sync_persons(trigger="cron", perf=person_perf), name="person-updater").start() except ValueError as exc: print(f"[main] UPDATER_SCHEDULE invalid ({exc}); " f"scheduled person sync DISABLED", flush=True) app.wait() return 0 if __name__ == "__main__": sys.exit(main())