import os from dataclasses import dataclass from pathlib import Path from typing import List, Optional, Union from urllib.parse import quote_plus from omegaconf import DictConfig, OmegaConf from komconfig import app SETTINGS_PATH = os.path.join(app.user_config_dir, "config.yaml") @dataclass class RemoteSettings: url: str port: int user: str password: str database_name: str = "komcache" @dataclass class Cache: mode: str remote: dict[str, Union[str, int]] | RemoteSettings local_path: str | Path def __post__init__(self): if self.mode == "remote": self.remote = RemoteSettings(**self.remote) else: self.remote = None self.local_path = Path(self.local_path).expanduser() @property def path(self): if self.mode == "local": return Path(self.local_path).expanduser() else: return f"{self.remote.url}:{self.remote.port}" @property def url(self): password = quote_plus(self.remote.password) if self.remote else "" return f"mysql+pymysql://{self.remote.user}:{password}@{self.remote.url}:{self.remote.port}/{self.remote.database_name}" @dataclass class Library: """Komga library settings.""" id: Optional[str] = None name: Optional[str] = None type: Optional[str] = None media_path: Optional[str] = None valid_extensions: Optional[List[str]] = None def from_ddict(self, d: dict): keys = d.keys() for key in keys: self.name = key data = d[self.name] for key, value in data.items(): setattr(self, key, value) return self @dataclass class Komga: """Komga server settings.""" url: str user: str password: str media_path: Path api_key: str = None libraries: List[Library] = None def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) def __post_init__(self): if "~" in self.media_path: self.media_path = Path(os.path.expanduser(self.media_path)) if self.libraries: self.libraries = [Library().from_ddict(lib) for lib in self.libraries] def getLibraryByName(self, name: str) -> Optional[Library]: """Get a library by its name. Args: name (str): The name of the library. Returns: Optional[Library]: The library object if found, None otherwise. """ if self.libraries: for lib in self.libraries: if lib.name == name: return lib return None @dataclass class General: """General application settings.""" log_level: str log_file: str def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) @dataclass class Aria2: """Aria2 settings.""" host: str port: int secret: str timeout: int kill_after_completion: bool def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) @dataclass class QbitTorrent: """QbitTorrent settings.""" host: str port: int username: str password: str category: str def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) @dataclass class EbookSettings: min_filesize: int max_filesize: int data_directory: str skip_parameters: List[str] valid_file_extensions: List[str] def __post_init__(self): self.skip_parameters = [ param.lower() for param in self.skip_parameters if param is not None ] def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) @dataclass class ComicSettings: min_filesize: int max_filesize: int data_directory: str valid_file_extensions: List[str] skip_parameters: List[str] def __post_init__(self): self.skip_parameters = [ param.lower() for param in self.skip_parameters if param is not None ] def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) @dataclass class KomGrabber: """ Configs used by the KomGrabber. Attributes ---------- download_location : str | Path The location to download the files to. get_chapters : bool Whether to get chapters or not. skip_parameters : List[str] A list of parameters to skip. aria2 : Aria2 The aria2 settings. see Aria2 class. tag_interactive : bool Whether to use interactive tagging or not. ebook : EbookSettings The ebook settings. see EbookSettings class. manga : ComicSettings The manga settings. see ComicSettings class. check_interval : int The interval to check for new files. use_cache : bool Whether to use cache or not. cache_check_interval : int The days an entry will not be checked based on the last change in the cache. """ download_location: Path tag_location: Path copy_location: Optional[Path] copy_files: Optional[List[str]] copy: bool get_chapters: bool skip_parameters: List[str] tag_interactive: bool ebook: EbookSettings manga: ComicSettings check_interval: int use_cache: bool cache_check_interval: int downloader: str downloader_settings: Union[Aria2, QbitTorrent] def __post_init__(self): self.skip_parameters = [param.lower() for param in self.skip_parameters] # if "~" in self.download_location: # self.download_location = os.path.expanduser(self.download_location) if isinstance(self.download_location, str): self.download_location = Path(self.download_location).expanduser() if isinstance(self.tag_location, str): self.tag_location = Path(self.tag_location).expanduser() if isinstance(self.downloader_settings, dict): if self.downloader == "aria2": self.downloader_settings = Aria2(**self.downloader_settings) elif self.downloader == "qbit": self.downloader_settings = QbitTorrent(**self.downloader_settings) def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) @dataclass class KomTagger: """KomTagger settings.""" sanitize_description: bool check_cache: bool check_interval: int def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) def __post_init__(self): pass @dataclass class ComicVine: """ComicVine settings.""" url: str api_key: str def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) @dataclass class MangaDex: """MangaDex settings.""" url: str username: str password: str def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) @dataclass class ComicsOrg: """Comics.org settings.""" path: str def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) def __post_init__(self): if "~" in self.path: self.path = os.path.expanduser(self.path) @dataclass class API: """API settings.""" comicvine: ComicVine mangadex: MangaDex comicsorg: ComicsOrg def __post_init__(self): # Convert dictionaries to their respective dataclass objects self.comicvine = ComicVine(**self.comicvine) self.mangadex = MangaDex(**self.mangadex) self.comicsorg = ComicsOrg(**self.comicsorg) def getattr(self, name): return getattr(self, name) def _setattr(self, name, value): setattr(self, name, value) class Settings: """A class to handle the configuration of the application. After initializing, it will try to load the config file and store it for future access. Any changes made can be saved to the file using the .save() method. Changes are used in real time in the app, if a restart is required, the Application will show a window. Raises: RuntimeError: Configuration not loaded KeyError: Invalid option """ _config: Optional[DictConfig] = None def __init__(self, config_path: str = SETTINGS_PATH): """ Loads the configuration file and stores it for future access. Args: config_path (str): Path to the YAML configuration file. Raises: FileNotFoundError: Configuration file not found """ if "~" in config_path: config_path = os.path.expanduser(config_path) if not os.path.exists(config_path): raise FileNotFoundError(f"Configuration file not found: {config_path}") self._config = OmegaConf.load(os.path.expanduser(config_path)) self.config_path = config_path def save(self): """ Saves the current configuration to the file. Args: config_path (str): Path to the YAML configuration file. """ OmegaConf.save(self._config, self.config_path) @property def general(self): return General(**self._config.general) @property def komga(self): return Komga(**self._config.komga) @property def komga_attr(self, name): return getattr(self.komga, name) @komga_attr.setter def komga_attr(self, name, value): self.komga._setattr(name, value) @property def general_attr(self, name): return getattr(self.general, name) @general_attr.setter def general_attr(self, name, value): self.general._setattr(name, value) @property def komgrabber(self): return KomGrabber(**self._config.komgrabber) @property def komgrabber_attr(self, name): return getattr(self.komgrabber, name) @komgrabber_attr.setter def komgrabber_attr(self, name, value): self.komgrabber._setattr(name, value) @property def komtagger(self): return KomTagger(**self._config.komtagger) @property def cache(self): return Cache(**self._config.cache) def komtagger_attr(self, name): return getattr(self.komtagger, name) def set_komtagger_attr(self, name, value): OmegaConf.update(self._config, f"komtagger.{name}", value) def set_komgrabber_attr(self, name, value): OmegaConf.update(self._config, f"komgrabber.{name}", value) def set_general_attr(self, name, value): OmegaConf.update(self._config, f"general.{name}", value) def set_api_attr(self, name, value): OmegaConf.update(self._config, f"api.{name}", value) def set_komga_attr(self, name, value): OmegaConf.update(self._config, f"komga.{name}", value) @property def save_path(self): return self._config.save_path @save_path.setter def save_path(self, value: str): self._config.save_path = value def load_config(self, path, filename): return OmegaConf.load(os.path.join(path, filename)) @property def api(self): return API(**self._config.api) def dict(self): return OmegaConf.to_container(self._config) @property def komga_auth(self): return (self.komga.user, self.komga.password)