from __future__ import annotations import os from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional import yaml @dataclass class JellyfinConfig: server_url: str = "http://127.0.0.1:8096" username: str = "" password: str = "" token: Optional[str] = None client_name: str = "Jellyfin HTPC Client" device_name: str = "HTPC" device_id: str = "htpc-default-01" @dataclass class PlayerConfig: mpv_path: str = "mpv" ipc_socket: str = "/tmp/mpv-jellyfin.sock" hwdec: str = "auto" vo: str = "gpu" fullscreen: bool = True audio_device: str = "auto" extra_args: List[str] = field(default_factory=lambda: [ "--keep-open=yes", "--force-window=immediate", "--osd-font=sans-serif", "--osd-font-size=28", ]) @dataclass class KodiRpcConfig: enabled: bool = True http_port: int = 8080 ws_port: int = 9090 event_port: int = 9777 @dataclass class WebApiConfig: enabled: bool = True host: str = "0.0.0.0" port: int = 8097 @dataclass class YoutubeConfig: max_height: int = 1080 fetch_metadata: bool = True @dataclass class AppConfig: jellyfin: JellyfinConfig = field(default_factory=JellyfinConfig) player: PlayerConfig = field(default_factory=PlayerConfig) kodi_rpc: KodiRpcConfig = field(default_factory=KodiRpcConfig) web_api: WebApiConfig = field(default_factory=WebApiConfig) youtube: YoutubeConfig = field(default_factory=YoutubeConfig) @classmethod def load(cls, config_path: Optional[str | Path] = None) -> AppConfig: paths_to_check = [] if config_path: paths_to_check.append(Path(config_path)) if env_path := os.environ.get("JELLYFIN_CLIENT_CONFIG"): paths_to_check.append(Path(env_path)) paths_to_check.extend([ Path.cwd() / "config.yaml", Path.home() / ".config" / "jellyfin-client" / "config.yaml", Path("/etc/jellyfin-client/config.yaml"), ]) data: Dict[str, Any] = {} found_path = None for p in paths_to_check: if p.is_file(): found_path = p with open(p, "r", encoding="utf-8") as f: data = yaml.safe_load(f) or {} break cfg = cls() if not data: return cfg if "jellyfin" in data: jf = data["jellyfin"] cfg.jellyfin = JellyfinConfig( server_url=jf.get("server_url", cfg.jellyfin.server_url).rstrip("/"), username=jf.get("username", cfg.jellyfin.username), password=jf.get("password", cfg.jellyfin.password), token=jf.get("token", cfg.jellyfin.token), client_name=jf.get("client_name", cfg.jellyfin.client_name), device_name=jf.get("device_name", cfg.jellyfin.device_name), device_id=jf.get("device_id", cfg.jellyfin.device_id), ) if "player" in data: pl = data["player"] cfg.player = PlayerConfig( mpv_path=pl.get("mpv_path", cfg.player.mpv_path), ipc_socket=pl.get("ipc_socket", cfg.player.ipc_socket), hwdec=pl.get("hwdec", cfg.player.hwdec), vo=pl.get("vo", cfg.player.vo), fullscreen=pl.get("fullscreen", cfg.player.fullscreen), audio_device=pl.get("audio_device", cfg.player.audio_device), extra_args=pl.get("extra_args", cfg.player.extra_args), ) if "kodi_rpc" in data: kr = data["kodi_rpc"] cfg.kodi_rpc = KodiRpcConfig( enabled=kr.get("enabled", cfg.kodi_rpc.enabled), http_port=kr.get("http_port", cfg.kodi_rpc.http_port), ws_port=kr.get("ws_port", cfg.kodi_rpc.ws_port), ) if "web_api" in data: wa = data["web_api"] cfg.web_api = WebApiConfig( enabled=wa.get("enabled", cfg.web_api.enabled), host=wa.get("host", cfg.web_api.host), port=wa.get("port", cfg.web_api.port), ) if "youtube" in data: yt = data["youtube"] cfg.youtube = YoutubeConfig( max_height=yt.get("max_height", cfg.youtube.max_height), fetch_metadata=yt.get("fetch_metadata", cfg.youtube.fetch_metadata), ) # Environment variable overrides (useful for Docker & CI) if jf_url := os.environ.get("JELLYFIN_SERVER_URL"): cfg.jellyfin.server_url = jf_url.rstrip("/") if jf_user := os.environ.get("JELLYFIN_USERNAME"): cfg.jellyfin.username = jf_user if jf_pass := os.environ.get("JELLYFIN_PASSWORD"): cfg.jellyfin.password = jf_pass if jf_dev := os.environ.get("JELLYFIN_DEVICE_NAME"): cfg.jellyfin.device_name = jf_dev if mpv_vo := os.environ.get("MPV_VO"): cfg.player.vo = mpv_vo if mpv_hwdec := os.environ.get("MPV_HWDEC"): cfg.player.hwdec = mpv_hwdec if mpv_fs := os.environ.get("MPV_FULLSCREEN"): cfg.player.fullscreen = mpv_fs.lower() in ("1", "true", "yes") if mpv_ao := os.environ.get("MPV_AO"): cfg.player.audio_device = mpv_ao return cfg