TheOrb/archive_bot/main.py

116 lines
3.9 KiB
Python

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