Улучшение кода main.py и конфигурации

This commit is contained in:
2025-09-14 20:12:28 +03:00
parent 9453b3b50b
commit 9da9e98547
7 changed files with 270 additions and 262 deletions
+67 -55
View File
@@ -1,63 +1,75 @@
{ {
"tools": { "base": {
"apktool_jar_url": "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.12.0.jar", "tools": {
"apktool_wrapper_url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool" "apktool_jar_url": "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.12.0.jar",
}, "apktool_wrapper_url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool"
"new_package_name": "com.wowlikon.anixart2",
"server": "https://anixarty.wowlikon.tech/modding",
"theme": {
"colors": {
"primary": "#ccff00",
"secondary": "#ffffd700",
"background": "#ffffff",
"text": "#000000"
}, },
"gradient": { "xml_ns": {
"angle": "135.0", "android": "http://schemas.android.com/apk/res/android",
"from": "#ffff6060", "app": "http://schemas.android.com/apk/res-auto"
"to": "#ffccff00"
} }
}, },
"cleanup": { "patches": {
"keep_dirs": ["META-INF", "kotlin"] "package_name": {
}, "new_package_name": "com.wowlikon.anixart"
"settings_urls": { },
"Мы в социальных сетях": [ "cleanup": {
{ "keep_dirs": ["META-INF", "kotlin"]
"title": "wowlikon", },
"description": "Разработчик", "change_server": {
"url": "https://t.me/wowlikon", "server": "https://anixarty.wowlikon.tech/modding"
"icon": "@drawable/ic_custom_telegram", },
"icon_space_reserved": "false" "color_theme": {
"colors": {
"primary": "#ccff00",
"secondary": "#ffffd700",
"background": "#ffffff",
"text": "#000000"
}, },
{ "gradient": {
"title": "Kentai Radiquum", "angle": "135.0",
"description": "Разработчик", "from": "#ffff6060",
"url": "https://t.me/radiquum", "to": "#ffccff00"
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
} }
], },
"Прочее": [ "custom_speed": {
{ "speeds": [9.0]
"title": "Поддержать проект", },
"description": "После пожертвования вы сможете выбрать в своём профиле любую роль, например «Кошка-девочка», которая будет видна всем пользователям мода.", "settings_urls": {
"url": "https://t.me/wowlikon", "menu": {
"icon": "@drawable/ic_custom_crown", "Мы в социальных сетях": [
"icon_space_reserved": "false" {
"title": "wowlikon",
"description": "Разработчик",
"url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Kentai Radiquum",
"description": "Разработчик",
"url": "https://t.me/radiquum",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
}
],
"Прочее": [
{
"title": "Поддержать проект",
"description": "После пожертвования вы сможете выбрать в своём профиле любую роль, например «Кошка-девочка», которая будет видна всем пользователям мода.",
"url": "https://t.me/wowlikon",
"icon": "@drawable/ic_custom_crown",
"icon_space_reserved": "false"
}
]
} }
] }
}, }
"xml_ns": {
"android": "http://schemas.android.com/apk/res/android",
"app": "http://schemas.android.com/apk/res-auto"
},
"speeds": [9.0]
} }
+188 -198
View File
@@ -1,230 +1,220 @@
import os from pathlib import Path
import sys from typing import List
import json
import yaml import httpx
import requests import typer
import argparse
import colorama
import importlib import importlib
import traceback import traceback
import subprocess import yaml
from tqdm import tqdm
def init() -> dict: from pydantic import BaseModel, ValidationError
for directory in ["original", "modified", "patches", "tools", "decompiled"]: from plumbum import local, ProcessExecutionError
if not os.path.exists(directory): from rich.console import Console
os.makedirs(directory) from rich.progress import Progress
from rich.prompt import Prompt
from rich.table import Table
with open("./config.json", "r") as config_file: # --- Paths ---
conf = json.load(config_file) TOOLS = Path("tools")
ORIGINAL = Path("original")
MODIFIED = Path("modified")
DECOMPILED = Path("decompiled")
PATCHES = Path("patches")
if not os.path.exists("./tools/apktool.jar"): console = Console()
try: app = typer.Typer()
print("Скачивание Apktool...")
jar_response = requests.get(conf["tools"]["apktool_jar_url"], stream=True)
jar_path = "tools/apktool.jar"
with open(jar_path, "wb") as f:
for chunk in jar_response.iter_content(chunk_size=8192):
f.write(chunk)
wrapper_response = requests.get(conf["tools"]["apktool_wrapper_url"])
wrapper_path = "tools/apktool"
with open(wrapper_path, "w") as f:
f.write(wrapper_response.text)
os.chmod(wrapper_path, 0o755)
except Exception as e: # ======================= CONFIG =========================
print(f"Ошибка при скачивании Apktool: {e}") class ToolsConfig(BaseModel):
exit(1) apktool_jar_url: str
apktool_wrapper_url: str
class XmlNamespaces(BaseModel):
android: str
app: str
class BaseSection(BaseModel):
tools: ToolsConfig
xml_ns: XmlNamespaces
class Config(BaseModel):
base: BaseSection
patches: dict
def load_config() -> Config:
try:
return Config.model_validate_json(Path("config.json").read_text())
except FileNotFoundError:
console.print("[red]Файл config.json не найден")
raise typer.Exit(1)
except ValidationError as e:
console.print("[red]Ошибка валидации config.json:", e)
raise typer.Exit(1)
# ======================= UTILS =========================
def ensure_dirs():
for d in [TOOLS, ORIGINAL, MODIFIED, DECOMPILED, PATCHES]:
d.mkdir(exist_ok=True)
def run(cmd: List[str], hide_output=True):
prog = local[cmd[0]][cmd[1:]]
try:
prog() if hide_output else prog & FG
except ProcessExecutionError as e:
console.print(f"[red]Ошибка при выполнении команды: {' '.join(cmd)}")
console.print(e.stderr)
raise typer.Exit(1)
def download(url: str, dest: Path):
console.print(f"[cyan]Скачивание {url}{dest.name}")
with httpx.stream("GET", url, timeout=30) as r:
r.raise_for_status()
with open(dest, "wb") as f:
for chunk in r.iter_bytes():
f.write(chunk)
# ======================= INIT =========================
@app.command()
def init():
"""Создание директорий и скачивание инструментов"""
ensure_dirs()
conf = load_config()
if not (TOOLS / "apktool.jar").exists():
download(conf.base.tools.apktool_jar_url, TOOLS / "apktool.jar")
wrapper = httpx.get(conf.base.tools.apktool_wrapper_url, timeout=30).text
(TOOLS / "apktool").write_text(wrapper, encoding="utf-8")
(TOOLS / "apktool").chmod(0o755)
try: try:
result = subprocess.run( local["java"]["-version"]()
["java", "-version"], capture_output=True, text=True, check=True console.print("[green]Java найдена")
) except ProcessExecutionError:
console.print("[red]Java не установлена")
version_line = result.stderr.splitlines()[0] raise typer.Exit(1)
if "1.8" in version_line or any(f"{i}." in version_line for i in range(9, 100)):
print("Java 8 или более поздняя версия установлена.")
else:
print("Java 8 или более поздняя версия не установлена.")
sys.exit(1)
except subprocess.CalledProcessError:
print("Java не установлена. Установите Java 8 или более позднюю версию.")
exit(1)
return conf
def select_apk() -> str:
apks = []
for file in os.listdir("original"):
if file.endswith(".apk") and os.path.isfile(os.path.join("original", file)):
apks.append(file)
if not apks:
print("Нет файлов .apk в текущей директории")
sys.exit(1)
if len(apks) == 1:
apk = apks[0]
print(f"Выбран файл {apk}")
return apk
while True:
print("Выберете файл для модификации")
for index, apk in enumerate(apks):
print(f"{index + 1}. {apk}")
print("0. Exit")
try:
selected_index = int(input("\nВведите номер файла: "))
if selected_index == 0:
sys.exit(0)
elif selected_index > len(apks):
print("Неверный номер файла")
else:
apk = apks[selected_index - 1]
print(f"Выбран файл {apk}")
return apk
except ValueError:
print("Неверный формат ввода")
except KeyboardInterrupt:
print("Прервано пользователем")
sys.exit(0)
def decompile_apk(apk: str):
print("Декомпилируем apk...")
try:
result = subprocess.run(
"tools/apktool d -f -o decompiled " + os.path.join("original", apk),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
except subprocess.CalledProcessError as e:
print("Ошибка при выполнении команды:")
print(e.stderr)
sys.exit(1)
def compile_apk(apk: str):
print("Компилируем apk...")
try:
subprocess.run(
"tools/apktool b decompiled -o " + os.path.join("modified", apk),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
subprocess.run(
"zipalign -v 4 " + os.path.join("modified", apk) + " " + os.path.join("modified", apk.replace(".apk", "-aligned.apk")),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
subprocess.run(
"apksigner sign " +
"--v1-signing-enabled false " +
"--v2-signing-enabled true " +
"--v3-signing-enabled true " +
"--ks keystore.jks " +
"--ks-pass file:keystore.pass " +
"--out " + os.path.join("modified", apk.replace(".apk", "-mod.apk")) +
" " + os.path.join("modified", apk.replace(".apk", "-aligned.apk")),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
title = "anixart mod "
with open('./decompiled/apktool.yml') as f:
package = yaml.safe_load(f)
title += ' '.join([f'{k}: {v}' for k, v in package['versionInfo'].items()])
with open("./modified/report.log", "w") as log_file:
log_file.write(title+'\n')
log_file.write("\n".join([f"{patch.name}: {'applied' if patch.applied else 'failed'}" for patch in patches]))
except subprocess.CalledProcessError as e:
print("Ошибка при выполнении команды:")
print(e.stderr)
sys.exit(1)
# ======================= PATCHING =========================
class Patch: class Patch:
def __init__(self, name, pkg): def __init__(self, name: str, module):
self.name = name self.name = name
self.package = pkg self.module = module
self.applied = False self.applied = False
try: self.priority = getattr(module, "priority", 0)
self.priority = pkg.priority
except AttributeError:
self.priority = 0
def apply(self, conf: dict) -> bool: def apply(self, conf: dict) -> bool:
try: try:
self.applied = self.package.apply(conf) self.applied = bool(self.module.apply(conf))
return True return self.applied
except Exception as e: except Exception as e:
print(f"Ошибка при применении патча {self.name}: {e}") console.print(f"[red]Ошибка в патче {self.name}: {e}")
print(type(e), e.args)
traceback.print_exc() traceback.print_exc()
return False return False
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Автоматический патчер anixart"
)
parser.add_argument("-v", "--verbose", def select_apk() -> Path:
action="store_true", apks = [f for f in ORIGINAL.glob("*.apk")]
help="Выводить подробные сообщения") if not apks:
console.print("[red]Нет apk-файлов в папке original")
raise typer.Exit(1)
parser.add_argument("-f", "--force", if len(apks) == 1:
action="store_true", console.print(f"[green]Выбран {apks[0].name}")
help="Принудительно собрать APK") return apks[0]
args = parser.parse_args() options = {str(i): apk for i, apk in enumerate(apks, 1)}
for k, v in options.items():
console.print(f"{k}. {v.name}")
conf = init() choice = Prompt.ask("Выберите номер", choices=list(options.keys()))
return options[choice]
def decompile(apk: Path):
console.print("[yellow]Декомпиляция apk...")
run(["java", "-jar", str(TOOLS / "apktool.jar"), "d", "-f", "-o", str(DECOMPILED), str(apk)])
def compile(apk: Path, patches: List[Patch]):
console.print("[yellow]Сборка apk...")
out_apk = MODIFIED / apk.name
aligned = out_apk.with_stem(out_apk.stem + "-aligned")
signed = out_apk.with_stem(out_apk.stem + "-mod")
run(["java", "-jar", str(TOOLS / "apktool.jar"), "b", str(DECOMPILED), "-o", str(out_apk)])
run(["zipalign", "-v", "4", str(out_apk), str(aligned)])
run([
"apksigner", "sign",
"--v1-signing-enabled", "false",
"--v2-signing-enabled", "true",
"--v3-signing-enabled", "true",
"--ks", "keystore.jks",
"--ks-pass", "file:keystore.pass",
"--out", str(signed),
str(aligned)
])
with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f:
meta = yaml.safe_load(f)
version_str = " ".join(f"{k}:{v}" for k, v in meta.get("versionInfo", {}).items())
with open(MODIFIED / "report.log", "w", encoding="utf-8") as f:
f.write(f"anixart mod {version_str}\n")
for p in patches:
f.write(f"{p.name}: {'applied' if p.applied else 'failed'}\n")
@app.command()
def build(
force: bool = typer.Option(False, "--force", "-f", help="Принудительная сборка"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
):
"""Декомпиляция, патчи и сборка apk"""
conf = load_config().model_dump()
apk = select_apk() apk = select_apk()
patch = decompile_apk(apk) decompile(apk)
if args.verbose: conf["verbose"] = True patch_settings = conf.get("patches", {})
patch_objs: List[Patch] = []
patches = [] for f in PATCHES.glob("*.py"):
for filename in os.listdir("patches/"): if f.name.startswith("todo_") or f.name == "__init__.py":
if filename.endswith(".py") and filename != "__init__.py" and not filename.startswith("todo_"): continue
module_name = filename[:-3] name = f.stem
module = importlib.import_module(f"patches.{module_name}") settings = patch_settings.get(name, {})
patches.append(Patch(module_name, module)) if not settings.get("enable", True):
console.print(f"[yellow]≫ Пропускаем {name}")
continue
module = importlib.import_module(f"patches.{name}")
patch_objs.append(Patch(name, module))
patches.sort(key=lambda x: x.package.priority, reverse=True) patch_objs.sort(key=lambda p: p.priority, reverse=True)
for patch in tqdm(patches, colour="green", desc="Применение патчей"): console.print("[cyan]Применение патчей")
tqdm.write(f"Применение патча: {patch.name}") with Progress() as progress:
patch.apply(conf) task = progress.add_task("Патчи", total=len(patch_objs))
for p in patch_objs:
ok = p.apply(
patch_settings.get(p.name, {}) | conf.get("base", {})
)
progress.console.print(f"{'' if ok else ''} {p.name}")
progress.advance(task)
statuses = {} successes = sum(p.applied for p in patch_objs)
for patch in patches: if successes == len(patch_objs):
statuses[patch.name] = patch.applied compile(apk, patch_objs)
marker = colorama.Fore.GREEN + "" if patch.applied else colorama.Fore.RED + "" elif successes > 0 and (force or Prompt.ask("Продолжить сборку?", choices=["y", "n"]) == "y"):
print(f"{marker}{colorama.Style.RESET_ALL} {patch.name}") compile(apk, patch_objs)
if all(statuses.values()):
print(f"{colorama.Fore.GREEN}Все патчи успешно применены{colorama.Style.RESET_ALL}")
compile_apk(apk)
elif any(statuses.values()):
print(f"{colorama.Fore.YELLOW}{colorama.Style.RESET_ALL} Некоторые патчи не были успешно применены")
if args.force or input("Продолжить? (y/n): ").lower() == "y":
compile_apk(apk)
else:
print(colorama.Fore.RED + "Операция отменена" + colorama.Style.RESET_ALL)
else: else:
print(f"{colorama.Fore.RED}Ни один патч не был успешно применен{colorama.Style.RESET_ALL}") console.print("[red]Сборка отменена")
sys.exit(1) raise typer.Exit(1)
if __name__ == "__main__":
app()
+1 -1
View File
@@ -15,7 +15,7 @@ def apply(config: dict) -> bool:
if config.get("verbose", False): if config.get("verbose", False):
tqdm.write(f'Удалён файл: {item_path}') tqdm.write(f'Удалён файл: {item_path}')
elif os.path.isdir(item_path): elif os.path.isdir(item_path):
if item not in config["cleanup"]["keep_dirs"]: if item not in config["keep_dirs"]:
shutil.rmtree(item_path) shutil.rmtree(item_path)
if config.get("verbose", False): if config.get("verbose", False):
tqdm.write(f'Удалена папка: {item_path}') tqdm.write(f'Удалена папка: {item_path}')
+5 -5
View File
@@ -11,11 +11,11 @@ from utils.public import (
) )
def apply(config: dict) -> bool: def apply(config: dict) -> bool:
main_color = config["theme"]["colors"]["primary"] main_color = config["colors"]["primary"]
splash_color = config["theme"]["colors"]["secondary"] splash_color = config["colors"]["secondary"]
gradient_angle = config["theme"]["gradient"]["angle"] gradient_angle = config["gradient"]["angle"]
gradient_from = config["theme"]["gradient"]["from"] gradient_from = config["gradient"]["from"]
gradient_to = config["theme"]["gradient"]["to"] gradient_to = config["gradient"]["to"]
# No connection alert coolor # No connection alert coolor
with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file: with open("./decompiled/assets/no_connection.html", "r", encoding="utf-8") as file:
-2
View File
@@ -11,8 +11,6 @@ def rename_dir(src, dst):
def apply(config: dict) -> bool: def apply(config: dict) -> bool:
assert config["new_package_name"] is not None, "new_package_name is not configured"
for root, dirs, files in os.walk("./decompiled"): for root, dirs, files in os.walk("./decompiled"):
for filename in files: for filename in files:
file_path = os.path.join(root, filename) file_path = os.path.join(root, filename)
+1 -1
View File
@@ -32,7 +32,7 @@ def apply(config: dict) -> bool:
# Insert new PreferenceCategory before the last element # Insert new PreferenceCategory before the last element
last = root[-1] # last element last = root[-1] # last element
pos = root.index(last) pos = root.index(last)
for section, items in config["settings_urls"].items(): for section, items in config["menu"].items():
root.insert(pos, make_category(config["xml_ns"], section, items)) root.insert(pos, make_category(config["xml_ns"], section, items))
pos += 1 pos += 1
+8
View File
@@ -0,0 +1,8 @@
typer[all]>=0.9.0
rich>=13.0.0
httpx>=1.2.0
pydantic>=2.2.0
plumbum>=1.8.0
lxml>=4.9.3
PyYAML>=6.0
tqdm>=4.66.0