200 lines
8.0 KiB
Python
200 lines
8.0 KiB
Python
"""
|
|
main_manga.py
|
|
=============
|
|
|
|
Container entry point for the **manga** variant (Suwayomi -> Kavita mover
|
|
plus metadata enrichment). The light-novel variant has its own entry
|
|
point (main_ln.py); both share the modules in src/ and add their
|
|
variant-specific code from src/manga/ resp. src/ln/.
|
|
|
|
Mount points (Docker)
|
|
---------------------
|
|
/mnt/suwayomi -> Suwayomi downloads (read/write, sources deleted)
|
|
/mnt/kavita -> Kavita library (read/write, CBZs written here)
|
|
|
|
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:
|
|
SUWAYOMI_PATH default /mnt/suwayomi
|
|
KAVITA_PATH default /mnt/kavita
|
|
LANGUAGE default en
|
|
SETTLE_SECONDS default 600 (10-minute quiet window)
|
|
REQUEST_TIMEOUT default 30
|
|
DELETE_SOURCE default true (delete source folders after pack)
|
|
MATCH_PATH default /config/matches.json
|
|
WEB_PORT default 8080 (Flask web UI for matches.json)
|
|
WEB_HOST default 0.0.0.0
|
|
UPDATER_ENABLED default true (run volume/cover + person updaters on cron)
|
|
UPDATER_SCHEDULE cron expression for the periodic updaters,
|
|
default "0 10 * * 0" = Sundays 10:00
|
|
(local time — set TZ inside the container!)
|
|
UPDATER_LOG default /config/volume_updater.log
|
|
COVER_CACHE_PATH directory for the persistent cover cache;
|
|
empty (default) = temporary cache, deleted on exit
|
|
PERF_PATH JSON file for per-step move timing stats.
|
|
Default /config/perf_stats.json (empty disables it)
|
|
VOLUME_PERF_PATH JSON file for volume/cover updater timing.
|
|
Default /config/volume_perf_stats.json
|
|
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/, manga-specific code in src/manga/. 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" / "manga"))
|
|
|
|
from SuwayomiMover import SuwayomiMover # noqa: E402
|
|
from SuwayomiFolderWatcher import SuwayomiFolderWatcher # noqa: E402,F401
|
|
from MatchesCache import MatchesCache # noqa: E402
|
|
from MatchesWebApp import MatchesWebApp # noqa: E402
|
|
from KavitaVolumeCoverUpdater import KavitaVolumeCoverUpdater # noqa: E402
|
|
from KavitaClient import KavitaClient # noqa: E402
|
|
from KavitaPersonUpdater import KavitaPersonUpdater # noqa: E402
|
|
from PerfStats import PerfStats # noqa: E402
|
|
from CronRunner import CronRunner # 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_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 main() -> int:
|
|
suwayomi_path = _env_str("SUWAYOMI_PATH", "/mnt/suwayomi")
|
|
kavita_path = _env_str("KAVITA_PATH", "/mnt/kavita")
|
|
kavita_url = _env_str("KAVITA_URL", "http://kavita:5000")
|
|
kavita_api_key = _env_str("KAVITA_API_KEY", "")
|
|
language = _env_str("LANGUAGE", "en") or "en"
|
|
settle_seconds = _env_int("SETTLE_SECONDS", 600)
|
|
request_timeout = _env_int("REQUEST_TIMEOUT", 30)
|
|
delete_source = _env_bool("DELETE_SOURCE", True)
|
|
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)
|
|
updater_enabled = _env_bool("UPDATER_ENABLED", True)
|
|
updater_schedule = _env_str("UPDATER_SCHEDULE", "0 10 * * 0")
|
|
updater_log = _env_str("UPDATER_LOG", "/config/volume_updater.log")
|
|
cover_cache_path = _env_str("COVER_CACHE_PATH", "") or None
|
|
perf_path = _env_str("PERF_PATH", "/config/perf_stats.json") or None
|
|
volume_perf_path = _env_str("VOLUME_PERF_PATH",
|
|
"/config/volume_perf_stats.json") or None
|
|
person_perf_path = _env_str("PERSON_PERF_PATH",
|
|
"/config/person_perf_stats.json") or None
|
|
|
|
print(f"[main] suwayomi = {suwayomi_path}", flush=True)
|
|
print(f"[main] kavita = {kavita_path}", flush=True)
|
|
print(f"[main] kavita url= {kavita_url}", flush=True)
|
|
print(f"[main] settle = {settle_seconds}s", flush=True)
|
|
print(f"[main] language = {language}", flush=True)
|
|
print(f"[main] delete src= {delete_source}", flush=True)
|
|
print(f"[main] match path= {match_path}", flush=True)
|
|
print(f"[main] web = {web_host}:{web_port}", flush=True)
|
|
|
|
matches_cache = MatchesCache(match_path)
|
|
perf_move = PerfStats(perf_path)
|
|
perf_volume = PerfStats(volume_perf_path)
|
|
perf_person = PerfStats(person_perf_path)
|
|
|
|
mover = SuwayomiMover(
|
|
suwayomi_path, kavita_path,
|
|
language=language,
|
|
request_timeout=request_timeout,
|
|
delete_source=delete_source,
|
|
matches_cache=matches_cache,
|
|
cover_cache_dir=cover_cache_path,
|
|
perf_stats=perf_move,
|
|
)
|
|
|
|
# Standalone, global, id-based person updater (manga + LN libraries).
|
|
person_updater = None
|
|
if kavita_api_key:
|
|
kavita_client = KavitaClient(kavita_url, kavita_api_key,
|
|
request_timeout=request_timeout)
|
|
person_updater = KavitaPersonUpdater(kavita_client)
|
|
|
|
# watcher = SuwayomiFolderWatcher(suwayomi_path, mover, settle_seconds=settle_seconds)
|
|
|
|
web_app = MatchesWebApp(
|
|
matches_cache, mover=mover,
|
|
person_updater=person_updater, person_trigger="web",
|
|
perf_stats={"move": perf_move, "volume": perf_volume,
|
|
"person": perf_person},
|
|
host=web_host, port=web_port)
|
|
web_app.start()
|
|
|
|
if updater_enabled:
|
|
updater = KavitaVolumeCoverUpdater(
|
|
kavita_path,
|
|
matches_cache=matches_cache,
|
|
language=language,
|
|
request_timeout=request_timeout,
|
|
log_path=updater_log,
|
|
cover_cache_dir=cover_cache_path,
|
|
perf_stats=perf_volume,
|
|
)
|
|
|
|
def _scheduled_job():
|
|
updater.update_all()
|
|
if person_updater is not None:
|
|
person_updater.update_all_persons(trigger="cron",
|
|
perf=perf_person)
|
|
|
|
try:
|
|
CronRunner(updater_schedule, _scheduled_job,
|
|
name="updaters").start()
|
|
except ValueError as exc:
|
|
# Invalid cron expression — keep the service up, just without
|
|
# the scheduled updaters, and surface the config error.
|
|
print(f"[main] UPDATER_SCHEDULE invalid ({exc}); "
|
|
f"scheduled updaters DISABLED", flush=True)
|
|
|
|
# watcher.start()
|
|
# watcher.wait() # blocks until stop() is called via a signal
|
|
web_app.wait() # keep process alive while the watcher is disabled
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|