"""Backend configuration using pydantic-settings. Reads from environment variables and .env file automatically. Uses a flat settings model so that standard env vars like SSH_HOST, JELLYFIN_URL etc. are picked up directly without nested-model complications. """ from __future__ import annotations import logging from functools import lru_cache from pathlib import Path from pydantic_settings import BaseSettings from media_library_viewer_api.logging_utils import describe_settings logger = logging.getLogger(__name__) class Settings(BaseSettings): """Flat application settings read from env vars / .env file.""" # Logging log_level: str = "INFO" log_format: str = "text" # "text" or "json" # Auth / OIDC (Authentik-compatible JWT validation) auth_enabled: bool = False oidc_issuer_url: str = "" oidc_audience: str = "" oidc_jwks_url: str = "" oidc_clock_skew_seconds: int = 30 # SMTP (optional, used for Users -> message popup) smtp_host: str = "" smtp_port: int = 587 smtp_username: str = "" smtp_password: str = "" smtp_from_address: str = "" smtp_from_name: str = "Manage" smtp_use_tls: bool = True smtp_use_ssl: bool = False smtp_timeout: int = 30 # Legacy SSH fallback (new preferred path is machine-specific settings) ssh_host: str = "" ssh_username: str = "" ssh_port: int = 22 ssh_key_directory: str = "" ssh_key_name: str = "" ssh_password: str = "" ssh_known_hosts_path: str = "" # Observability prometheus_enabled: bool = True # Remote paths remote_media_root: str = "" remote_path_prefix: str = "" # Derived convenience @property def media_root(self) -> str: return self.remote_media_root or "" @property def path_prefix(self) -> str: return self.remote_path_prefix or "" @property def ssh_key_path(self) -> str: if not self.ssh_key_directory or not self.ssh_key_name: return "" return str(Path(self.ssh_key_directory) / self.ssh_key_name) @property def ssh_known_hosts_file(self) -> Path: return Path(self.ssh_known_hosts_path or ".cache/media_library_viewer/known_hosts") model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"} # type: ignore[assignment] def _find_env_file() -> str | None: """Look for .env in CWD, then parent directories up to the repo root.""" current = Path.cwd() for directory in [current, *current.parents]: candidate = directory / ".env" if candidate.is_file(): return str(candidate) if (directory / ".git").exists(): break return None @lru_cache def get_settings() -> Settings: """Return a cached Settings instance.""" env_file = _find_env_file() settings = Settings(_env_file=env_file) if env_file else Settings() logger.info( "Loaded backend settings from %s: %s", env_file or "environment/defaults", describe_settings(settings), ) return settings