From f393414599f55deff22b49ea58a99eb8e6d8ef1e Mon Sep 17 00:00:00 2001 From: bobokun Date: Thu, 28 Aug 2025 20:18:10 -0400 Subject: [PATCH] feat(config): add --config-dir argument for directory-based config discovery Add support for specifying a configuration directory using the new --config-dir argument. When provided, this option takes precedence over --config-file and automatically discovers all .yml and .yaml files in the specified directory (excluding schedule.yml). Falls back to legacy --config-file behavior when not specified, maintaining backward compatibility. --- VERSION | 2 +- modules/config.py | 6 +++- modules/util.py | 79 +++++++++++++++++++++++++++++++++-------------- qbit_manage.py | 21 ++++++++++--- 4 files changed, 78 insertions(+), 30 deletions(-) diff --git a/VERSION b/VERSION index a46006b..5aa6e51 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.5.6-develop9 +4.5.6-develop10 diff --git a/modules/config.py b/modules/config.py index c66a2a1..7c0f6f2 100755 --- a/modules/config.py +++ b/modules/config.py @@ -130,7 +130,11 @@ class Config: logger.debug(f" --run (QBT_RUN): {self.args['run']}") logger.debug(f" --schedule (QBT_SCHEDULE): {self.args['sch']}") logger.debug(f" --startup-delay (QBT_STARTUP_DELAY): {self.args['startupDelay']}") - logger.debug(f" --config-file (QBT_CONFIG): {self.args['config_files']}") + logger.debug(f" --config-dir (QBT_CONFIG_DIR): {self.args['config_dir_args']}") + if self.args["config_dir_args"] is None: + logger.debug(f" --config-file (QBT_CONFIG): {self.args['config_files']} (legacy)") + else: + logger.debug(f" Configs found from QBT_CONFIG_DIR: {self.args['config_files']}") logger.debug(f" --log-file (QBT_LOGFILE): {self.args['log_file']}") logger.debug(f" --log-level (QBT_LOG_LEVEL): {self.args['log_level']}") logger.debug(f" --log-size (QBT_LOG_SIZE): {self.args['log_size']}") diff --git a/modules/util.py b/modules/util.py index 4ac66f0..0957c47 100755 --- a/modules/util.py +++ b/modules/util.py @@ -243,25 +243,31 @@ def _platform_config_base() -> Path: return base / "qbit-manage" -def get_default_config_dir(config_hint: str = None) -> str: +def get_default_config_dir(config_hint: str = None, config_dir: str = None) -> str: """ Determine the default persistent config directory, leveraging a provided config path/pattern first. Resolution order: - 1) If config_hint is an absolute path or contains a directory component, use its parent directory - 2) Otherwise, if config_hint is a name/pattern (e.g. 'config.yml'), search common bases for: - - A direct match to that filename/pattern - - OR a persisted scheduler file 'schedule.yml' (so we don't lose an existing schedule when config.yml is absent) - Common bases (in order): - - /config (container volume) - - repository ./config - - user OS config directory - Return the first base containing either. - 3) Fallback to legacy-ish behavior: - - /config if it contains any *.yml.sample / *.yaml.sample - - otherwise user OS config directory + 1) If config_dir is provided, use it directly (takes precedence over config_hint) + 2) If config_hint is an absolute path or contains a directory component, use its parent directory + 3) Otherwise, if config_hint is a name/pattern (e.g. 'config.yml'), search common bases for: + - A direct match to that filename/pattern + - OR a persisted scheduler file 'schedule.yml' (so we don't lose an existing schedule when config.yml is absent) + Common bases (in order): + - /config (container volume) + - repository ./config + - user OS config directory + Return the first base containing either. + 4) Fallback to legacy-ish behavior: + - /config if it contains any *.yml.sample / *.yaml.sample + - otherwise user OS config directory """ - # 1) If a direct path is provided, prefer its parent directory + # 1) If config_dir is provided, use it directly (takes precedence) + if config_dir: + p = Path(config_dir).expanduser() + return str(p.resolve()) + + # 2) If a direct path is provided, prefer its parent directory if config_hint: primary = str(config_hint).split(",")[0].strip() # take first if comma-separated if primary: @@ -1470,12 +1476,14 @@ class EnvStr(str): return super().__repr__() -def get_matching_config_files(config_pattern: str, default_dir: str) -> list: +def get_matching_config_files(config_pattern: str, default_dir: str, use_config_dir_mode: bool = False) -> list: """Get list of config files matching a pattern. Args: config_pattern (str): Config file pattern (e.g. "config.yml" or "config*.yml") default_dir (str): Default directory to look for configs + use_config_dir_mode (bool): If True, use new config-dir approach (find all .yml/.yaml files) + If False, use legacy config-file approach (pattern matching) Returns: list: List of matching config file names @@ -1489,16 +1497,39 @@ def get_matching_config_files(config_pattern: str, default_dir: str) -> list: else: search_dir = default_dir - # Handle single file vs pattern - if "*" not in config_pattern: - return [config_pattern] - else: - glob_configs = glob.glob(os.path.join(search_dir, config_pattern)) - if glob_configs: - # Return just the filenames without paths - return [os.path.split(x)[-1] for x in glob_configs] + if use_config_dir_mode: + # New --config-dir approach: find all .yml and .yaml files, excluding reserved files + config_files = [] + for pattern in ["*.yml", "*.yaml"]: + glob_configs = glob.glob(os.path.join(search_dir, pattern)) + for config_file in glob_configs: + filename = os.path.basename(config_file) + # Exclude reserved files + if filename != "schedule.yml": + config_files.append(filename) + + if config_files: + # Return just the filenames without paths, sorted for consistency + return sorted(config_files) else: - raise Failed(f"Config Error: Unable to find any config files in the pattern '{config_pattern}'") + raise Failed(f"Config Error: Unable to find any config files in '{search_dir}'") + else: + # Legacy --config-file approach: pattern matching + # Handle single file vs pattern + if "*" not in config_pattern: + # For single file, check if it exists + if os.path.exists(os.path.join(search_dir, config_pattern)): + return [config_pattern] + else: + raise Failed(f"Config Error: Unable to find config file '{config_pattern}' in '{search_dir}'") + else: + # For patterns, use glob matching + glob_configs = glob.glob(os.path.join(search_dir, config_pattern)) + if glob_configs: + # Return just the filenames without paths + return [os.path.basename(x) for x in glob_configs] + else: + raise Failed(f"Config Error: Unable to find any config files in the pattern '{config_pattern}' in '{search_dir}'") def execute_qbit_commands(qbit_manager, commands, stats, hashes=None): diff --git a/qbit_manage.py b/qbit_manage.py index bbc77a8..0950698 100755 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -108,9 +108,18 @@ parser.add_argument( action="store", default="config.yml", type=str, + help=argparse.SUPPRESS, +) +parser.add_argument( + "-cd", + "--config-dir", + dest="config_dir", + action="store", + default=None, + type=str, help=( - "This is used if you want to use a different name for your config.yml or if you want to load multiple" - "config files using *. Example: tv.yml or config*.yml" + "This is used to specify a custom configuration directory. " + "Takes precedence over --config-file. If not specified, falls back to --config-file logic." ), ) parser.add_argument( @@ -265,6 +274,7 @@ run = get_arg("QBT_RUN", args.run, arg_bool=True) sch = get_arg("QBT_SCHEDULE", args.schedule) startupDelay = get_arg("QBT_STARTUP_DELAY", args.startupDelay) config_files = get_arg("QBT_CONFIG", args.configfiles) +config_dir = get_arg("QBT_CONFIG_DIR", args.config_dir) log_file = get_arg("QBT_LOGFILE", args.logfile) recheck = get_arg("QBT_RECHECK", args.recheck, arg_bool=True) cat_update = get_arg("QBT_CAT_UPDATE", args.cat_update, arg_bool=True) @@ -294,10 +304,13 @@ stats = {} args = {} scheduler = None # Global scheduler instance -default_dir = ensure_config_dir_initialized(get_default_config_dir(config_files)) +default_dir = ensure_config_dir_initialized(get_default_config_dir(config_files, config_dir)) args["config_dir"] = default_dir +args["config_dir_args"] = config_dir -config_files = get_matching_config_files(config_files, default_dir) +# Use config_dir_mode if --config-dir was provided, otherwise use legacy mode +use_config_dir_mode = config_dir is not None +config_files = get_matching_config_files(config_files, default_dir, use_config_dir_mode) for v in [