#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa # Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> # See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html) """config.py The industry-standard hierarchy for application configuration is: 1. Command-Line Arguments (argparse) (Highest priority – explicit runtime overrides). 2. Environment Variables (os.environ) (Medium priority – deployment or session-specific configuration). 3. Configuration Files (e.g., .env, .ini, .yaml files) 4. Hardcoded Defaults (Lowest priority – fallback safety net) """ # stdlib from argparse import ArgumentParser from collections import UserDict from pathlib import Path from typing import Optional import logging import os import re import shutil import sys # Pip packages from dotenv import load_dotenv # Local imports from . import tts_aedocw, tts_generic, tts_kokoro from . import logger, PathLike, loglevel_map from . import DataDict class App(): WHICH = ["ffmpeg"] # Needed in $PATH BACKENDS = { # keys are the backend names, values are the corresponding TTS classes "default": tts_generic.GenericTTSBackend, "edge": tts_aedocw.TTSEdge, # aedocw-backed implementations — these expect the corresponding # package "console scripts"/entrypoints to be available in the # environment (or an explicit backend_cmd to be provided). "epub2tts": tts_aedocw.AedocwEpub2TTS, "epub2tts-edge": tts_aedocw.AedocwEpub2TTSEdge, "epub2tts-chatterbox": tts_aedocw.AedocwChatterbox, "epub2tts-kokoro": tts_aedocw.AedocwKokoro, "generic-epub2tts": tts_aedocw.Epub2TTS, "kokoro": tts_kokoro.KokoroBackend, } def __init__(self, input_str: Optional[str|Path|list[str|Path]], output_str: Optional[str|Path], backend: str, ): # Validate and sanitize output self.output_path = App.path(self.output) if len(self.input) > 1 and not self.output_path.is_dir(): self.logger.error("Output must be a directory when multiple input files are provided. Invalid: '%s'", self.output_path) sys.exit(1) del self._data["output"] # Validate and sanitize input self.input_files = {} self.input_dirs = [] self.logger.info("Paths provided as input: %d", len(self.input)) for i, input_str in enumerate(self.input): self.logger.info("[Input %d] %s", i+1, input_str) input_path = App.path(input_str) if not input_path.exists(): self.logger.warning("Skipping path (do not exist): '%s'.", input_path) continue if input_path.is_dir(): self.input_dir.append(input_path) self.logger.info("Added directory to queue: '%s'", input_path) else: self.input_files[input_str] = {"path":input_path} self.input_files[input_str]["filename"] = self.input_files[input_str]["path"].name self.input_files[input_str]["stem"] = self.input_files[input_str]["path"].stem self.input_files[input_str]["suffix"] = self.input_files[input_str]["path"].suffix #self.input_files[input_str]["filetype"] = ebook_meta self.input_files[input_str]["path"] self.logger.info("Added file to queue: %s", self.input_files[input_str]) del self._data["input"] # Sanitize replace_map self.logger.debug(self.replace) if self.replace and len(self.replace) % 2 == 0: self.replace_map = { old: new for old, new in self.replace} del self.replace # print args self.logger.debug(self) # Execute actions if self._parsed_args.check_env: self.check_env() self.engine = App.BACKENDS[self.backend](self) # Utility methods def check_env(self, which=None): """Check anvironment for necessary binaries""" if not which: which = self.WHICH if isinstance(which, str): if " " in which: which = which.split() which = [which] for bin in which: if sys.platform == "win32": p = shutil.which(bin + ".exe") else: p = shutil.which(bin) if not p: self.logger.error(f"%s is either NOT installed or " "NOT in your system PATH.", bin) return False else: self.logger.info(f"%s found at: %s", bin, p) return True