338 lines
12 KiB
Python
338 lines
12 KiB
Python
"""CLI tool to run xml commands."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import click
|
|
|
|
from config import Settings
|
|
from core import log_process, setup_logging
|
|
from handlers import (
|
|
check_icons,
|
|
check_prefs,
|
|
check_prefs_reverse,
|
|
check_strings,
|
|
missing_strings,
|
|
remove_unused_resources,
|
|
remove_unused_strings,
|
|
replace_strings,
|
|
sort_strings,
|
|
update_from_diff,
|
|
update_strings,
|
|
)
|
|
from utils import GitClient
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
from logging import Logger
|
|
|
|
settings = Settings()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Operation:
|
|
"""Represents a single CLI operation and its execution metadata."""
|
|
|
|
key: str
|
|
name: str
|
|
handler: Callable[..., Any]
|
|
args: tuple[Any, ...]
|
|
|
|
|
|
@dataclass
|
|
class CLIConfig:
|
|
"""Configuration for CLI commands."""
|
|
|
|
log_file: str | None
|
|
rvx_base_dir: Path | None
|
|
app: str
|
|
logger: Logger
|
|
|
|
|
|
@click.group(invoke_without_command=True)
|
|
@click.option("--log-file", type=str, help="Path to log file")
|
|
@click.option("--rvx-base-dir", type=str, envvar="RVX_BASE_DIR", help="Path to RVX 'patches' directory")
|
|
@click.option("-a", "--all", "run_all", is_flag=True, help="Run all commands in order")
|
|
@click.option("-m", "--missing", is_flag=True, help="Run missing strings check")
|
|
@click.option("-r", "--replace", is_flag=True, help="Run replace strings operation")
|
|
@click.option("--remove", is_flag=True, help="Remove unused strings")
|
|
@click.option("--remove-resources", is_flag=True, help="Remove unused resource files")
|
|
@click.option("-s", "--sort", is_flag=True, help="Sort strings in XML files")
|
|
@click.option("-c", "--check", is_flag=True, help="Run missing strings check")
|
|
@click.option("-p", "--prefs", is_flag=True, help="Run missing preferences check")
|
|
@click.option("-pr", "--reverse", is_flag=True, help="Run missing preferences check (reverse)")
|
|
@click.option("--icons", is_flag=True, help="Check icon preference keys against XML.")
|
|
@click.option(
|
|
"--update-file",
|
|
type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
|
|
help="Create updated_strings.xml from keys listed in the specified file.",
|
|
)
|
|
@click.option(
|
|
"--update-from-diff",
|
|
is_flag=True,
|
|
help="Check git diff and create updated_strings.xml for forced strings.",
|
|
)
|
|
@click.option("--youtube", is_flag=True, help="Process YouTube")
|
|
@click.option("--music", is_flag=True, help="Process Music")
|
|
@click.option("--reddit", is_flag=True, help="Process Reddit")
|
|
@click.option("--debug", is_flag=True, help="Enable debug logging")
|
|
@click.pass_context
|
|
def cli(ctx: click.Context, **kwargs: dict[str, Any]) -> None:
|
|
"""CLI tool for processing XML commands."""
|
|
log_file = kwargs.get("log_file")
|
|
log_file = log_file if isinstance(log_file, str) else None
|
|
|
|
flags = [bool(kwargs.get("youtube")), bool(kwargs.get("music")), bool(kwargs.get("reddit"))]
|
|
if sum(flags) > 1:
|
|
exc: str = "You can only use one of --youtube, --music, or --reddit at a time."
|
|
raise click.UsageError(exc)
|
|
|
|
app: str = (
|
|
"youtube"
|
|
if kwargs.get("youtube")
|
|
else "music"
|
|
if kwargs.get("music")
|
|
else "reddit"
|
|
if kwargs.get("reddit")
|
|
else "youtube" # The default fallback if nothing is clicked
|
|
)
|
|
debug: bool = bool(kwargs.get("debug", False))
|
|
|
|
logger = setup_logging(Path(log_file) if log_file else None, debug=debug)
|
|
|
|
rvx_base_dir_str = kwargs.get("rvx_base_dir")
|
|
if not rvx_base_dir_str:
|
|
rvx_base_dir_str = os.getenv("RVX_BASE_DIR")
|
|
|
|
rvx_base_dir = Path(rvx_base_dir_str) if isinstance(rvx_base_dir_str, str) else None
|
|
|
|
ctx.obj = CLIConfig(
|
|
log_file=log_file,
|
|
rvx_base_dir=rvx_base_dir,
|
|
app=app,
|
|
logger=logger,
|
|
)
|
|
|
|
command_flags = [
|
|
"run_all",
|
|
"missing",
|
|
"replace",
|
|
"remove",
|
|
"remove_resources",
|
|
"sort",
|
|
"check",
|
|
"prefs",
|
|
"reverse",
|
|
"update_file",
|
|
"update_from_diff",
|
|
"icons",
|
|
]
|
|
if kwargs.get("run_all"):
|
|
process_all(ctx.obj)
|
|
elif any(kwargs.get(flag) for flag in command_flags):
|
|
handle_individual_operations(ctx.obj, kwargs)
|
|
else:
|
|
# If no command flag/option is provided, show help
|
|
click.echo(ctx.get_help())
|
|
sys.exit(0)
|
|
|
|
|
|
def process_all(config: CLIConfig) -> None:
|
|
"""Run all operations in sequence."""
|
|
logger = config.logger
|
|
base_dir = config.rvx_base_dir
|
|
|
|
git_ready = False
|
|
|
|
# Initialize Git state if base_dir is valid
|
|
if base_dir is not None and base_dir.exists():
|
|
try:
|
|
git = GitClient(base_dir)
|
|
if git.sync_repository():
|
|
git_ready = True
|
|
else:
|
|
logger.warning("Git sync failed. Operations requiring Git (Replace) will be skipped.")
|
|
except (RuntimeError, OSError, ValueError) as e:
|
|
logger.warning("Failed to initialize Git client: %s. Operations requiring Git will be skipped.", e)
|
|
|
|
handlers: list[tuple[str, Callable[..., Any], list[Any]]] = [
|
|
("Replace Strings (YouTube)", replace_strings.process, ["youtube", base_dir]),
|
|
("Replace Strings (YouTube Music)", replace_strings.process, ["music", base_dir]),
|
|
("Remove Unused Strings (YouTube)", remove_unused_strings.process, ["youtube"]),
|
|
("Remove Unused Strings (YouTube Music)", remove_unused_strings.process, ["music"]),
|
|
("Sort Strings (YouTube)", sort_strings.process, ["youtube"]),
|
|
("Sort Strings (YouTube Music)", sort_strings.process, ["music"]),
|
|
("Sort Strings (Reddit)", sort_strings.process, ["reddit"]),
|
|
("Missing Strings Creation (YouTube)", missing_strings.process, ["youtube"]),
|
|
("Missing Strings Creation (YouTube Music)", missing_strings.process, ["music"]),
|
|
("Missing Strings Creation (Reddit)", missing_strings.process, ["reddit"]),
|
|
("Remove Unused Resources (YouTube)", remove_unused_resources.process, ["youtube"]),
|
|
("Remove Unused Resources (YouTube Music)", remove_unused_resources.process, ["music"]),
|
|
("Missing Prefs Check", check_prefs.process, ["youtube", base_dir]),
|
|
("Missing Prefs Check (Reverse)", check_prefs_reverse.process, ["youtube", base_dir]),
|
|
("Missing Strings Check (YouTube)", check_strings.process, ["youtube", base_dir]),
|
|
("Missing Strings Check (YouTube Music)", check_strings.process, ["music", base_dir]),
|
|
("Check Icon Preferences", check_icons.process, ["youtube"]),
|
|
("Update Strings from Git Diff (YouTube)", update_from_diff.process, ["youtube"]),
|
|
("Update Strings from Git Diff (YouTube Music)", update_from_diff.process, ["music"]),
|
|
]
|
|
|
|
for name, handler, args in handlers:
|
|
# Check if this operation requires base_dir
|
|
requires_base_dir = base_dir in args
|
|
|
|
if requires_base_dir:
|
|
if base_dir is None:
|
|
logger.warning("Skipping '%s': RVX_BASE_DIR is not defined.", name)
|
|
continue
|
|
|
|
if not base_dir.exists():
|
|
logger.warning("Skipping '%s': RVX_BASE_DIR '%s' does not exist.", name, base_dir)
|
|
continue
|
|
|
|
# Additional check for operations requiring valid Git state
|
|
if "Replace Strings" in name and not git_ready:
|
|
logger.warning("Skipping '%s': Git sync failed or invalid repo.", name)
|
|
continue
|
|
|
|
log_process(logger, name)
|
|
typed_args = [arg if isinstance(arg, Path) else str(arg) for arg in args]
|
|
try:
|
|
handler(*typed_args)
|
|
except Exception:
|
|
logger.exception("Error during process step '%s': ", name)
|
|
sys.exit(1)
|
|
|
|
|
|
def handle_operation(
|
|
config: CLIConfig,
|
|
operation_name: str,
|
|
handler: Callable[..., Any],
|
|
*args: tuple[Any, ...],
|
|
) -> None:
|
|
"""Handle a single operation, including logging and error handling."""
|
|
log_process(config.logger, operation_name)
|
|
try:
|
|
# Ensure Path arguments are passed correctly if needed
|
|
handler(*args)
|
|
except Exception:
|
|
config.logger.exception("Error during %s: ", operation_name)
|
|
sys.exit(1)
|
|
|
|
|
|
def _validate_base_dir(
|
|
config: CLIConfig,
|
|
operation_name: str,
|
|
base_dir: Path | None,
|
|
) -> bool:
|
|
if base_dir is None:
|
|
config.logger.warning(
|
|
"Skipping '%s': RVX_BASE_DIR is not defined.",
|
|
operation_name,
|
|
)
|
|
return False
|
|
|
|
if not base_dir.exists():
|
|
config.logger.warning(
|
|
"Skipping '%s': RVX_BASE_DIR '%s' does not exist.",
|
|
operation_name,
|
|
base_dir,
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def _run_operation(
|
|
config: CLIConfig,
|
|
operation: Operation,
|
|
base_dir: Path | None,
|
|
) -> None:
|
|
if operation.key == "replace":
|
|
try:
|
|
git = GitClient(base_dir or Path())
|
|
if git.sync_repository():
|
|
handle_operation(
|
|
config,
|
|
operation.name,
|
|
operation.handler,
|
|
*operation.args,
|
|
)
|
|
else:
|
|
config.logger.warning(
|
|
"Skipping '%s': Git sync failed.",
|
|
operation.name,
|
|
)
|
|
except (RuntimeError, OSError) as e:
|
|
config.logger.warning(
|
|
"Skipping '%s': Failed to initialize Git client: %s",
|
|
operation.name,
|
|
e,
|
|
)
|
|
return
|
|
|
|
if operation.key == "update_file" and operation.args[1] is None:
|
|
config.logger.warning(
|
|
"Skipping '%s': Input file path is missing.",
|
|
operation.name,
|
|
)
|
|
return
|
|
|
|
handle_operation(
|
|
config,
|
|
operation.name,
|
|
operation.handler,
|
|
*operation.args,
|
|
)
|
|
|
|
|
|
def handle_individual_operations(
|
|
config: CLIConfig,
|
|
options: dict[str, Any],
|
|
) -> None:
|
|
"""Handle individual operations based on user flags."""
|
|
base_dir = config.rvx_base_dir
|
|
app = config.app
|
|
|
|
operations: list[tuple[str, str, Callable[..., Any], tuple[Any, ...]]] = [
|
|
("missing", "Missing Strings Check", missing_strings.process, (app,)),
|
|
("remove", "Remove Unused Strings", remove_unused_strings.process, (app,)),
|
|
("remove_resources", "Remove Unused Resources", remove_unused_resources.process, (app,)),
|
|
("sort", "Sort Strings", sort_strings.process, (app,)),
|
|
("replace", "Replace Strings", replace_strings.process, (app, base_dir)),
|
|
("check", "Check Strings", check_strings.process, (app, base_dir)),
|
|
("prefs", "Check Preferences", check_prefs.process, (app, base_dir)),
|
|
("reverse", "Check Preferences (Reverse)", check_prefs_reverse.process, (app, base_dir)),
|
|
("update_file", "Update Strings from File", update_strings.process, (app, options.get("update_file"))),
|
|
("update_from_diff", "Update Forced Strings from Git Diff", update_from_diff.process, (app,)),
|
|
("icons", "Check Icon Preferences", check_icons.process, (app,)),
|
|
]
|
|
|
|
something_processed = False
|
|
base_dir_required = {"replace", "check", "prefs", "reverse"}
|
|
|
|
for option_key, operation_name, handler, args in operations:
|
|
if not options.get(option_key):
|
|
continue
|
|
|
|
something_processed = True
|
|
|
|
if option_key in base_dir_required and not _validate_base_dir(config, operation_name, base_dir):
|
|
continue
|
|
|
|
op = Operation(option_key, operation_name, handler, args)
|
|
_run_operation(config, op, base_dir)
|
|
|
|
if not something_processed and not options.get("run_all"):
|
|
config.logger.info(
|
|
"No specific operation requested. Use --help for options.",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|