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:
commit
b6c058c40f
16 changed files with 400 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.python-venv
|
||||||
|
.venv
|
||||||
|
apks
|
||||||
|
original
|
||||||
|
tools
|
||||||
|
decompiled
|
||||||
|
dist
|
||||||
|
__pycache__
|
1
.python-version
Normal file
1
.python-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
3.13.7
|
15
config.json
Normal file
15
config.json
Normal 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
47
config.py
Normal 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
29
main.py
Normal 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
0
patches/__init__.py
Normal file
3
patches/change_package_name.config.json
Normal file
3
patches/change_package_name.config.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"new_package_name": "com.radiquum.anixart"
|
||||||
|
}
|
61
patches/change_package_name.py
Normal file
61
patches/change_package_name.py
Normal 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
|
3
patches/compress.config.json
Normal file
3
patches/compress.config.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"keep_dirs": ["META-INF", "kotlin"]
|
||||||
|
}
|
25
patches/compress.py
Normal file
25
patches/compress.py
Normal 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
5
requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
requests
|
||||||
|
tqdm
|
||||||
|
lxml
|
||||||
|
rich
|
||||||
|
beaupy
|
0
scripts/__init__.py
Normal file
0
scripts/__init__.py
Normal file
46
scripts/download_tools.py
Normal file
46
scripts/download_tools.py
Normal 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
27
scripts/select_apk.py
Normal 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
87
scripts/select_patches.py
Normal 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
43
scripts/utils.py
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue