"""Configuration loading and defaults.""" from __future__ import annotations import os import tomllib from pathlib import Path from typing import Any, Optional DEFAULT_CONFIG_NAME = "yt_downloader.toml" QUALITY_PRESETS = ("best", "1080", "720", "480", "audio") def default_config_path() -> Path: xdg = os.environ.get("XDG_CONFIG_HOME") base = Path(xdg) if xdg else Path.home() / ".config" return base / "yt-downloader" / DEFAULT_CONFIG_NAME def _as_bool(value: Any, default: bool) -> bool: if value is None: return default if isinstance(value, bool): return value return str(value).strip().lower() in ("1", "true", "yes", "on") class Config: """Thin wrapper around the parsed TOML config.""" def __init__(self, data: dict, path: Path): self._data = data self.path = path # -- download -------------------------------------------------------- @property def out_dir(self) -> Path: return Path(self._data.get("download", {}).get("out", "./videos")) @property def quality(self) -> str: q = str(self._data.get("download", {}).get("quality", "best")).lower() return q if q in QUALITY_PRESETS else "best" @property def format_override(self) -> Optional[str]: return self._data.get("download", {}).get("format") @property def cookies_file(self) -> Optional[str]: return self._data.get("download", {}).get("cookies_file") @property def cookies_from_browser(self) -> Optional[str]: return self._data.get("download", {}).get("cookies_from_browser") # -- vpn ------------------------------------------------------------ @property def vpn_wireguard(self) -> Optional[str]: return self._data.get("vpn", {}).get("wireguard") @property def vpn_namespace(self) -> Optional[str]: return self._data.get("vpn", {}).get("namespace") @property def vpn_interface(self) -> Optional[str]: return self._data.get("vpn", {}).get("interface") # -- misc ----------------------------------------------------------- @property def dry_run(self) -> bool: return _as_bool(self._data.get("global", {}).get("dry_run", False), False) def load_config(path: Optional[Path | str] = None) -> Config: if path is None: path = default_config_path() path = Path(path) if not path.exists(): raise FileNotFoundError( f"Config not found at {path}. Run 'yt-downloader init' first." ) with open(path, "rb") as fh: data = tomllib.load(fh) return Config(data, path) def write_default_config(path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) template = """\ # yt-downloader configuration [global] dry_run = false # when true: plan only, touch nothing [download] out = "~/Videos/yt-downloader" # where videos land quality = "best" # best | 1080 | 720 | 480 | audio # format = "" # raw yt-dlp -f string, overrides quality # cookies_file = "/path/to/cookies.txt" # pass your browser cookies (best # cookies_from_browser = "firefox" # protection against bot checks; # # e.g. chromium, firefox, safari) [vpn] # wireguard = "~/.config/yt-downloader/mullvad.conf" # # optional: path to a WireGuard (Mullvad) .conf. # # When set, `download` runs inside a dedicated # # network namespace so ONLY yt-dlp traffic goes # # through the VPN. Requires sudo + wireguard-tools. # namespace = "mullvad" # optional: network namespace name # interface = "mv0" # optional: WireGuard interface name """ path.write_text(template)