from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path from typing import Optional @dataclass(slots=True) class QBittorrentConfig: host: str port: int username: str password: str use_https: bool @dataclass(slots=True) class SchedulerConfig: poll_seconds: int safe_margin_gb: int anchors_per_batch: int filler_limit: int cold_start_small_limit: int stall_percent: float stall_minutes: int stall_resume_minutes: int @dataclass(slots=True) class UploaderConfig: remote: str root_template: str threads: int rclone_path: str rclone_config: Optional[Path] transfers: int checkers: int bwlimit_up: Optional[str] drive_chunk_size: Optional[str] retries: int low_level_retries: int ionice_class: Optional[str] ionice_level: Optional[str] nice_level: Optional[str] @dataclass(slots=True) class AppPaths: download_dir: Path database_path: Path log_dir: Path @dataclass(slots=True) class AppConfig: qbittorrent: QBittorrentConfig scheduler: SchedulerConfig uploader: UploaderConfig paths: AppPaths def _env(key: str, default: Optional[str] = None) -> Optional[str]: return os.environ.get(key, default) def _env_int(key: str, default: int) -> int: try: return int(_env(key, str(default))) except (TypeError, ValueError): return default def _env_float(key: str, default: float) -> float: try: return float(_env(key, str(default))) except (TypeError, ValueError): return default def _env_path(key: str, default: Path) -> Path: raw = _env(key) return Path(raw).expanduser() if raw else default def load_config() -> AppConfig: state_dir = _env_path("QFLOW_STATE_DIR", Path("./state")) log_dir = _env_path("QFLOW_LOG_DIR", state_dir / "logs") db_path = _env_path("QFLOW_DB_PATH", state_dir / "qflow.db") download_dir = _env_path("QFLOW_DOWNLOAD_DIR", Path("/downloads")) qb = QBittorrentConfig( host=_env("QFLOW_QBIT_HOST", "http://qbittorrent:8080"), port=_env_int("QFLOW_QBIT_PORT", 8080), username=_env("QFLOW_QBIT_USER", "admin"), password=_env("QFLOW_QBIT_PASS", "adminadmin"), use_https=_env("QFLOW_QBIT_HTTPS", "false").lower() == "true", ) scheduler = SchedulerConfig( poll_seconds=_env_int("QFLOW_SCHED_POLL", 15), safe_margin_gb=_env_int("QFLOW_SAFE_MARGIN_GB", 20), anchors_per_batch=_env_int("QFLOW_ANCHORS", 1), filler_limit=_env_int("QFLOW_FILLERS", 4), cold_start_small_limit=_env_int("QFLOW_COLD_SMALL", 3), stall_percent=_env_float("QFLOW_STALL_PCT", 85.0), stall_minutes=_env_int("QFLOW_STALL_MIN", 5), stall_resume_minutes=_env_int("QFLOW_STALL_RESUME_MIN", 2), ) uploader = UploaderConfig( remote=_env("QFLOW_REMOTE", "gcrypt:"), root_template=_env("QFLOW_REMOTE_TEMPLATE", "{year}/{month:02d}"), threads=_env_int("QFLOW_UPLOAD_THREADS", 2), rclone_path=_env("QFLOW_RCLONE_BIN", "rclone"), rclone_config=_env_path("QFLOW_RCLONE_CONFIG", Path("/config/rclone/rclone.conf")), transfers=_env_int("QFLOW_RCLONE_TRANSFERS", 8), checkers=_env_int("QFLOW_RCLONE_CHECKERS", 16), bwlimit_up=_env("QFLOW_RCLONE_BWLIMIT", None), drive_chunk_size=_env("QFLOW_DRIVE_CHUNK", "128M"), retries=_env_int("QFLOW_RCLONE_RETRIES", 3), low_level_retries=_env_int("QFLOW_RCLONE_LL_RETRIES", 10), ionice_class=_env("QFLOW_IONICE_CLASS", "2"), ionice_level=_env("QFLOW_IONICE_LEVEL", "7"), nice_level=_env("QFLOW_NICE_LEVEL", "10"), ) return AppConfig( qbittorrent=qb, scheduler=scheduler, uploader=uploader, paths=AppPaths( download_dir=download_dir, database_path=db_path, log_dir=log_dir, ), )