andea_revanced-patches/xml_tools/main.py
2026-01-30 12:54:40 +03:00

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()