from __future__ import annotations import os import signal import sys import time from pathlib import Path from typing import Any from .auth import print_password_hash from .core import BotRuntime, bool_env, env, load_dotenv, normalize_discord_token from .db import initialize_database, migrate_runtime_documents from .dashboard import maybe_start_dashboard from .discord_api import DiscordGatewayManager, discord_bot_identity from .media import maybe_run_jellyfin_sync from .status import load_services, print_preview, run_check_cycle from .watchparty import initialize_watchparty_schema def main() -> int: load_dotenv() if "--hash-password" in sys.argv: print_password_hash() return 0 if "--preview" in sys.argv: config_path = Path(os.getenv("ARCHIVE_STATUS_CONFIG", "services.json")) initialize_database() migrate_runtime_documents([config_path]) print_preview(load_services(config_path)) return 0 token = normalize_discord_token(env("DISCORD_BOT_TOKEN")) channel_id = os.getenv("DISCORD_CHANNEL_ID", "").strip() config_path = Path(env("ARCHIVE_STATUS_CONFIG", "services.json")) state_path = Path(env("ARCHIVE_STATUS_STATE", "state/status-message.json")) media_state_path = Path(env("MEDIA_CATALOG_STATE", "state/media-catalog.json")) media_library_path = Path(env("MEDIA_LIBRARY_STATE", "state/media-library.json")) settings_path = Path(env("BOT_SETTINGS_STATE", "state/bot-settings.json")) initialize_database() initialize_watchparty_schema() migrate_runtime_documents( [ config_path, state_path, media_state_path, media_library_path, settings_path, ] ) interval = int(env("CHECK_INTERVAL_SECONDS", "60")) runtime = BotRuntime( token, channel_id, config_path, state_path, media_state_path, media_library_path, settings_path, dry_run=bool_env("DISCORD_DRY_RUN", False), ) gateway = None if bool_env("DISCORD_GATEWAY_ENABLED", True) and not runtime.dry_run: gateway = DiscordGatewayManager(token) gateway.start() dashboard = maybe_start_dashboard(runtime) if runtime.dry_run: print("Discord dry run is enabled; no Discord messages will be sent or edited.", flush=True) else: try: identity = discord_bot_identity(token) username = identity.get("username", "unknown") bot_id = identity.get("id", "unknown") print(f"Discord token authenticated as {username} ({bot_id})", flush=True) except Exception as exc: print(f"Could not verify Discord bot identity: {exc}", file=sys.stderr, flush=True) stopped = False def stop(_signum: int, _frame: Any) -> None: nonlocal stopped stopped = True signal.signal(signal.SIGINT, stop) signal.signal(signal.SIGTERM, stop) while not stopped: try: message_id, results = run_check_cycle(runtime) online = sum(1 for result in results if result.ok) print(f"Updated Discord status message {message_id}: {online}/{len(results)} online", flush=True) sync_result = maybe_run_jellyfin_sync(runtime) if sync_result is not None: action = "published" if sync_result["published"] else "checked" print( f"Jellyfin sync {action}: {sync_result['movieCount']} movies, {sync_result['showCount']} shows", flush=True, ) except Exception as exc: with runtime.lock: runtime.last_error = str(exc) print(f"Status update failed: {exc}", file=sys.stderr, flush=True) for _ in range(interval): if stopped: break time.sleep(1) if dashboard is not None: dashboard.shutdown() if gateway is not None: gateway.stop() return 0