from __future__ import annotations import json import os import urllib.parse from datetime import datetime, timezone from pathlib import Path from typing import Any from .core import BotRuntime from .db import load_document, save_document def load_state(path: Path) -> dict[str, Any]: data = load_document(path) if data is None: return {} return data if isinstance(data, dict) else {} def save_state(path: Path, state: dict[str, Any]) -> None: save_document(path, state) def validate_channel_id(value: str, label: str) -> str: channel_id = value.strip() if not channel_id: raise ValueError(f"{label} channel ID is required") if not channel_id.isdigit(): raise ValueError(f"{label} channel ID must be a Discord numeric channel ID") return channel_id def channel_settings(runtime: BotRuntime) -> dict[str, str]: data = load_state(runtime.settings_path) status_channel = str(data.get("status_channel_id", "")).strip() or runtime.default_channel_id media_channel = str(data.get("media_channel_id", "")).strip() or status_channel catalog_url = str(data.get("catalog_url", "")).strip() or os.getenv("PUBLIC_CATALOG_URL", "").strip() return { "statusChannelId": status_channel, "mediaChannelId": media_channel, "catalogUrl": catalog_url, } def validate_catalog_url(value: str) -> str: catalog_url = value.strip() if not catalog_url: return "" parsed = urllib.parse.urlparse(catalog_url) if parsed.scheme not in {"http", "https"} or not parsed.netloc: raise ValueError("Catalog URL must be a valid http(s) URL") return catalog_url def save_channel_settings( runtime: BotRuntime, status_channel_id: str, media_channel_id: str, catalog_url: str = "", ) -> dict[str, str]: status_channel = validate_channel_id(status_channel_id, "Status") media_channel = validate_channel_id(media_channel_id or status_channel, "Media") state = load_state(runtime.settings_path) state.update( { "status_channel_id": status_channel, "media_channel_id": media_channel, "catalog_url": validate_catalog_url(catalog_url), "updated_at": datetime.now(timezone.utc).isoformat(), } ) save_state(runtime.settings_path, state) return channel_settings(runtime) def save_media_channel_setting(runtime: BotRuntime, media_channel_id: str) -> dict[str, str]: media_channel = validate_channel_id(media_channel_id, "Media") state = load_state(runtime.settings_path) state["media_channel_id"] = media_channel state["updated_at"] = datetime.now(timezone.utc).isoformat() save_state(runtime.settings_path, state) return channel_settings(runtime) def jellyfin_settings(runtime: BotRuntime) -> dict[str, Any]: data = load_state(runtime.settings_path) api_key = str(data.get("jellyfin_api_key", "")).strip() library_names = data.get("jellyfin_library_names", []) if not isinstance(library_names, list): library_names = [] return { "url": str(data.get("jellyfin_url", "")).strip(), "configured": bool(api_key), "libraryNames": [str(name) for name in library_names if str(name).strip()], "autoSync": bool(data.get("jellyfin_auto_sync", False)), "lastSyncAt": data.get("jellyfin_last_sync_at"), "lastSyncError": data.get("jellyfin_last_sync_error"), "lastPublishedAt": data.get("jellyfin_last_published_at"), "lastFingerprint": data.get("jellyfin_last_fingerprint"), "lastPublishedFingerprint": data.get("jellyfin_last_published_fingerprint"), "lastChanges": data.get("jellyfin_last_changes"), } def save_jellyfin_settings(runtime: BotRuntime, data: dict[str, Any]) -> dict[str, Any]: state = load_state(runtime.settings_path) url = str(data.get("url", "")).strip().rstrip("/") if url: parsed = urllib.parse.urlparse(url) if parsed.scheme not in {"http", "https"} or not parsed.netloc: raise ValueError("Jellyfin URL must be a valid http(s) URL") api_key = str(data.get("apiKey", "")).strip() state["jellyfin_url"] = url if api_key: state["jellyfin_api_key"] = api_key elif data.get("clearApiKey"): state["jellyfin_api_key"] = "" raw_library_names = data.get("libraryNames", []) if isinstance(raw_library_names, str): library_names = [name.strip() for name in raw_library_names.split(",") if name.strip()] elif isinstance(raw_library_names, list): library_names = [str(name).strip() for name in raw_library_names if str(name).strip()] else: raise ValueError("Jellyfin libraries must be a list or comma-separated string") state["jellyfin_library_names"] = library_names state["jellyfin_auto_sync"] = bool(data.get("autoSync", False)) state["updated_at"] = datetime.now(timezone.utc).isoformat() save_state(runtime.settings_path, state) return jellyfin_settings(runtime) def save_catalog_url_setting(runtime: BotRuntime, catalog_url: str) -> dict[str, str]: state = load_state(runtime.settings_path) state["catalog_url"] = validate_catalog_url(catalog_url) state["updated_at"] = datetime.now(timezone.utc).isoformat() save_state(runtime.settings_path, state) return channel_settings(runtime)