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.
This commit is contained in:
bobokun 2025-08-28 20:18:10 -04:00
parent 19aa766094
commit f393414599
No known key found for this signature in database
GPG key ID: B73932169607D927
4 changed files with 78 additions and 30 deletions

View file

@ -1 +1 @@
4.5.6-develop9 4.5.6-develop10

View file

@ -130,7 +130,11 @@ class Config:
logger.debug(f" --run (QBT_RUN): {self.args['run']}") logger.debug(f" --run (QBT_RUN): {self.args['run']}")
logger.debug(f" --schedule (QBT_SCHEDULE): {self.args['sch']}") logger.debug(f" --schedule (QBT_SCHEDULE): {self.args['sch']}")
logger.debug(f" --startup-delay (QBT_STARTUP_DELAY): {self.args['startupDelay']}") 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-file (QBT_LOGFILE): {self.args['log_file']}")
logger.debug(f" --log-level (QBT_LOG_LEVEL): {self.args['log_level']}") logger.debug(f" --log-level (QBT_LOG_LEVEL): {self.args['log_level']}")
logger.debug(f" --log-size (QBT_LOG_SIZE): {self.args['log_size']}") logger.debug(f" --log-size (QBT_LOG_SIZE): {self.args['log_size']}")

View file

@ -243,25 +243,31 @@ def _platform_config_base() -> Path:
return base / "qbit-manage" 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. Determine the default persistent config directory, leveraging a provided config path/pattern first.
Resolution order: Resolution order:
1) If config_hint is an absolute path or contains a directory component, use its parent directory 1) If config_dir is provided, use it directly (takes precedence over config_hint)
2) Otherwise, if config_hint is a name/pattern (e.g. 'config.yml'), search common bases for: 2) If config_hint is an absolute path or contains a directory component, use its parent directory
- A direct match to that filename/pattern 3) Otherwise, if config_hint is a name/pattern (e.g. 'config.yml'), search common bases for:
- OR a persisted scheduler file 'schedule.yml' (so we don't lose an existing schedule when config.yml is absent) - A direct match to that filename/pattern
Common bases (in order): - OR a persisted scheduler file 'schedule.yml' (so we don't lose an existing schedule when config.yml is absent)
- /config (container volume) Common bases (in order):
- repository ./config - /config (container volume)
- user OS config directory - repository ./config
Return the first base containing either. - user OS config directory
3) Fallback to legacy-ish behavior: Return the first base containing either.
- /config if it contains any *.yml.sample / *.yaml.sample 4) Fallback to legacy-ish behavior:
- otherwise user OS config directory - /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: if config_hint:
primary = str(config_hint).split(",")[0].strip() # take first if comma-separated primary = str(config_hint).split(",")[0].strip() # take first if comma-separated
if primary: if primary:
@ -1470,12 +1476,14 @@ class EnvStr(str):
return super().__repr__() 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. """Get list of config files matching a pattern.
Args: Args:
config_pattern (str): Config file pattern (e.g. "config.yml" or "config*.yml") config_pattern (str): Config file pattern (e.g. "config.yml" or "config*.yml")
default_dir (str): Default directory to look for configs 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: Returns:
list: List of matching config file names list: List of matching config file names
@ -1489,16 +1497,39 @@ def get_matching_config_files(config_pattern: str, default_dir: str) -> list:
else: else:
search_dir = default_dir search_dir = default_dir
# Handle single file vs pattern if use_config_dir_mode:
if "*" not in config_pattern: # New --config-dir approach: find all .yml and .yaml files, excluding reserved files
return [config_pattern] config_files = []
else: for pattern in ["*.yml", "*.yaml"]:
glob_configs = glob.glob(os.path.join(search_dir, config_pattern)) glob_configs = glob.glob(os.path.join(search_dir, pattern))
if glob_configs: for config_file in glob_configs:
# Return just the filenames without paths filename = os.path.basename(config_file)
return [os.path.split(x)[-1] for x in glob_configs] # 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: 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): def execute_qbit_commands(qbit_manager, commands, stats, hashes=None):

View file

@ -108,9 +108,18 @@ parser.add_argument(
action="store", action="store",
default="config.yml", default="config.yml",
type=str, type=str,
help=argparse.SUPPRESS,
)
parser.add_argument(
"-cd",
"--config-dir",
dest="config_dir",
action="store",
default=None,
type=str,
help=( help=(
"This is used if you want to use a different name for your config.yml or if you want to load multiple" "This is used to specify a custom configuration directory. "
"config files using *. Example: tv.yml or config*.yml" "Takes precedence over --config-file. If not specified, falls back to --config-file logic."
), ),
) )
parser.add_argument( parser.add_argument(
@ -265,6 +274,7 @@ run = get_arg("QBT_RUN", args.run, arg_bool=True)
sch = get_arg("QBT_SCHEDULE", args.schedule) sch = get_arg("QBT_SCHEDULE", args.schedule)
startupDelay = get_arg("QBT_STARTUP_DELAY", args.startupDelay) startupDelay = get_arg("QBT_STARTUP_DELAY", args.startupDelay)
config_files = get_arg("QBT_CONFIG", args.configfiles) 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) log_file = get_arg("QBT_LOGFILE", args.logfile)
recheck = get_arg("QBT_RECHECK", args.recheck, arg_bool=True) recheck = get_arg("QBT_RECHECK", args.recheck, arg_bool=True)
cat_update = get_arg("QBT_CAT_UPDATE", args.cat_update, arg_bool=True) cat_update = get_arg("QBT_CAT_UPDATE", args.cat_update, arg_bool=True)
@ -294,10 +304,13 @@ stats = {}
args = {} args = {}
scheduler = None # Global scheduler instance 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"] = 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 [ for v in [