1
0
Fork 0
mirror of https://github.com/Radiquum/anixart-patcher.git synced 2025-09-06 03:03:50 +05:00

feat: initial commit

This commit is contained in:
Kentai Radiquum 2025-08-31 19:51:58 +05:00
commit b6c058c40f
Signed by: Radiquum
GPG key ID: 858E8EE696525EED
16 changed files with 400 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.python-venv
.venv
apks
original
tools
decompiled
dist
__pycache__

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.13.7

15
config.json Normal file
View file

@ -0,0 +1,15 @@
{
"tools": [
{
"tool": "apktool.jar",
"url": "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.12.0.jar"
}
],
"folders": {
"tools": "./tools",
"apks": "./apks",
"decompiled": "./decompiled",
"patches": "./patches",
"dist": "./dist"
}
}

47
config.py Normal file
View file

@ -0,0 +1,47 @@
import json
import logging
import os
from typing import TypedDict
from rich.logging import RichHandler
from rich.console import Console
FORMAT = "%(message)s"
logging.basicConfig(
level="NOTSET",
format=FORMAT,
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)],
)
log = logging.getLogger("rich")
console = Console()
class ConfigTools(TypedDict):
tool: str
url: str
class ConfigFolders(TypedDict):
tools: str
apks: str
decompiled: str
patches: str
dist: str
class Config(TypedDict):
tools: list[ConfigTools]
folders: ConfigFolders
def load_config() -> Config:
if not os.path.exists("config.json"):
log.exception("file `config.json` is not found!")
exit(1)
with open("./config.json", "r", encoding="utf-8") as file:
return json.loads(file.read())
config = load_config()

29
main.py Normal file
View file

@ -0,0 +1,29 @@
from scripts.download_tools import check_and_download_all_tools
from scripts.select_apk import get_apks, select_apk
from scripts.select_patches import apply_patches, get_patches, select_patches
from scripts.utils import check_java_version, decompile_apk
from config import log, console
if __name__ == "__main__":
check_and_download_all_tools()
check_java_version()
apks = get_apks()
if not apks:
log.fatal(f"apks folder is empty")
exit(1)
apk = select_apk(apks)
decompile_apk(apk)
patches = get_patches()
patches = select_patches(patches)
statuses = apply_patches(patches)
for status in statuses:
if status["status"]:
console.print(f"{status['name']}: ✔", style="bold green")
else:
console.print(f"{status['name']}: ✘", style="bold red")

0
patches/__init__.py Normal file
View file

View file

@ -0,0 +1,3 @@
{
"new_package_name": "com.radiquum.anixart"
}

View file

@ -0,0 +1,61 @@
"""Remove and Compress unnecessary resources"""
priority = -10
from tqdm import tqdm
import os
import shutil
from typing import TypedDict
class PatchConfig_ChangePackageName(TypedDict):
src: str
new_package_name: str
def rename_dir(src, dst):
os.makedirs(dst, exist_ok=True)
os.rename(src, dst)
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(f"{config['src']}"):
for filename in files:
file_path = os.path.join(root, filename)
if os.path.isfile(file_path):
try:
with open(file_path, "r", encoding="utf-8") as file:
file_contents = file.read()
new_contents = file_contents.replace(
"com.swiftsoft.anixartd", config["new_package_name"]
)
new_contents = new_contents.replace(
"com/swiftsoft/anixartd",
config["new_package_name"].replace(".", "/"),
)
with open(file_path, "w", encoding="utf-8") as file:
file.write(new_contents)
except:
pass
if os.path.exists(f"{config['src']}/smali/com/swiftsoft/anixartd"):
rename_dir(
f"{config['src']}/smali/com/swiftsoft/anixartd",
os.path.join(
f"{config['src']}", "smali", config["new_package_name"].replace(".", "/")
),
)
if os.path.exists(f"{config['src']}/smali_classes2/com/swiftsoft/anixartd"):
rename_dir(
f"{config['src']}/smali_classes2/com/swiftsoft/anixartd",
os.path.join(
f"{config['src']}",
"smali_classes2",
config["new_package_name"].replace(".", "/"),
),
)
return True

View file

@ -0,0 +1,3 @@
{
"keep_dirs": ["META-INF", "kotlin"]
}

25
patches/compress.py Normal file
View file

@ -0,0 +1,25 @@
"""Remove and Compress unnecessary resources"""
priority = 0
from tqdm import tqdm
import os
import shutil
from typing import TypedDict
class PatchConfig_Compress(TypedDict):
src: str
keep_dirs: list[str]
def apply(config: PatchConfig_Compress) -> bool:
for item in os.listdir(f"{config['src']}/unknown/"):
item_path = os.path.join(f"{config['src']}/unknown/", item)
if os.path.isfile(item_path):
os.remove(item_path)
tqdm.write(f"removed file: {item_path}")
elif os.path.isdir(item_path):
if item not in config["keep_dirs"]:
shutil.rmtree(item_path)
tqdm.write(f"removed directory: {item_path}")
return True

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
requests
tqdm
lxml
rich
beaupy

0
scripts/__init__.py Normal file
View file

46
scripts/download_tools.py Normal file
View file

@ -0,0 +1,46 @@
import requests
import os
from tqdm import tqdm
from config import config, log
def check_if_tool_exists(tool: str) -> bool:
if not os.path.exists(config["folders"]["tools"]):
log.info(f"creating `tools` folder: {config['folders']['tools']}")
os.mkdir(config["folders"]["tools"])
if not os.path.exists(f"{config['folders']['tools']}/{tool}"):
return False
elif os.path.exists(f"{config['folders']['tools']}/{tool}") and os.path.isdir(
f"{config['folders']['tools']}/{tool}"
):
log.warning(f"`{config['folders']['tools']}/{tool}` is a folder")
return True
else:
return True
def download_tool(url: str, tool: str):
if not check_if_tool_exists(tool):
log.info(f"downloading a tool: `{tool}`")
try:
response = requests.get(url, stream=True)
total = int(response.headers.get("content-length", 0))
with open(f"{config['folders']['tools']}/{tool}", "wb") as file, tqdm(
desc=tool,
total=total,
unit="iB",
unit_scale=True,
unit_divisor=1024,
) as bar:
for bytes in response.iter_content(chunk_size=8192):
size = file.write(bytes)
bar.update(size)
log.info(f"`{tool}` downloaded")
except Exception as e:
log.error(f"error while downloading `{tool}`: {e}")
def check_and_download_all_tools():
for tool in config["tools"]:
download_tool(tool["url"], tool["tool"])

27
scripts/select_apk.py Normal file
View file

@ -0,0 +1,27 @@
import os
from beaupy import select
from config import config, log, console
def get_apks() -> list[str]:
apks = []
if not os.path.exists(config["folders"]["apks"]):
log.info(f"creating `apks` folder: {config['folders']['apks']}")
os.mkdir(config["folders"]["apks"])
return apks
for file in os.listdir(config["folders"]["apks"]):
if file.endswith(".apk") and os.path.isfile(f"{config['folders']['apks']}/{file}"):
apks.append(file)
return apks
def select_apk(apks: list[str]) -> str:
console.print("select apk file to patch")
apks.append("cancel")
apk = select(apks, cursor="->", cursor_style="cyan")
if apk == "cancel":
log.info("patching cancelled")
exit(0)
return apk

87
scripts/select_patches.py Normal file
View file

@ -0,0 +1,87 @@
import os, json
import importlib
from typing import TypedDict
from beaupy import select_multiple
from tqdm import tqdm
from config import config, log, console
class Patch:
def __init__(self, name, pkg):
self.name = name
self.package = pkg
self.applied = False
try:
self.priority = pkg.priority
except AttributeError:
self.priority = 0
def apply(self, conf: dict) -> bool:
try:
self.applied = self.package.apply(conf)
return True
except Exception as e:
log.error(
f"error while applying a patch {self.name}: %s, with args: %s",
e,
e.args,
exc_info=True,
)
return False
def get_patches() -> list[str]:
patches = []
if not os.path.exists(config["folders"]["patches"]):
log.info(f"creating `patches` folder: {config['folders']['patches']}")
os.mkdir(config["folders"]["patches"])
return patches
for file in os.listdir(config["folders"]["patches"]):
if (
file.endswith(".py")
and os.path.isfile(f"{config['folders']['patches']}/{file}")
and file != "__init__.py"
):
patches.append(file[:-3])
return patches
def select_patches(patches: list[str]) -> list[str]:
console.print("select patches to apply")
applied = select_multiple(patches, tick_character="X")
return applied
class PatchStatus(TypedDict):
name: str
status: bool
def apply_patches(patches: list[str]) -> list[PatchStatus]:
modules = []
statuses = []
for name in patches:
module = importlib.import_module(
f"{config['folders']['patches'].removeprefix("./")}.{name}"
)
modules.append(Patch(name, module))
modules.sort(key=lambda x: x.package.priority, reverse=True)
for patch in tqdm(modules, colour="green", desc="patching apk"):
tqdm.write(f"patch apply: {patch.name}")
conf = {}
if os.path.exists(f"{config['folders']['patches']}/{patch.name}.config.json"):
with open(
f"{config['folders']['patches']}/{patch.name}.config.json",
"r",
encoding="utf-8",
) as conf:
conf = json.loads(conf.read())
conf["src"] = config["folders"]["decompiled"]
status = patch.apply(conf)
statuses.append({"name": patch.name, "status": status})
return statuses

43
scripts/utils.py Normal file
View file

@ -0,0 +1,43 @@
import os
import shutil
import subprocess
from config import log, config
def check_java_version():
try:
result = subprocess.run(
["java", "-version"], capture_output=True, text=True, check=True
)
version_line = result.stderr.splitlines()[0]
if not any(f"{i}." in version_line for i in range(9, 100)):
log.error(f"java 8+ is not installed")
exit(1)
except subprocess.CalledProcessError:
log.error(f"java 8+ is not found")
exit(1)
log.info(f"found java: {version_line}")
def decompile_apk(apk: str):
if not os.path.exists(config["folders"]["decompiled"]):
log.info(f"creating `decompiled` folder: {config['folders']['decompiled']}")
os.mkdir(config["folders"]["decompiled"])
else:
log.info(f"resetting `decompiled` folder: {config['folders']['decompiled']}")
shutil.rmtree(config["folders"]["decompiled"])
os.mkdir(config["folders"]["decompiled"])
log.info(f"decompile apk: `{apk}`")
try:
result = subprocess.run(
f"java -jar {config['folders']['tools']}/apktool.jar d -f -o {config['folders']['decompiled']} {config['folders']['apks']}/{apk}",
shell=True,
check=True,
text=True,
# stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
except subprocess.CalledProcessError as e:
log.fatal(f"error of running a command: %s", e.stderr, exc_info=True)
exit(1)