From 7b5ba163bd6fb809cdd12a6fcf16c011184ee096 Mon Sep 17 00:00:00 2001 From: Radiquum Date: Tue, 2 Sep 2025 08:47:52 +0500 Subject: [PATCH] refactor: tqdm -> rich.progress --- config.json | 5 ++ config.py | 15 +++- patches/change_color_theme.py | 133 ++++++++++++++++++--------------- patches/change_package_name.py | 54 +++++++++---- patches/compress.py | 36 +++++++-- patches/disable_ad.py | 19 +++-- patches/disable_beta_banner.py | 38 +++++----- requirements.txt | 1 - scripts/download_tools.py | 52 +++++++++---- scripts/select_patches.py | 48 +++++++----- 10 files changed, 260 insertions(+), 141 deletions(-) diff --git a/config.json b/config.json index 6337eae..8008491 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,5 @@ { + "log_level": "INFO", "tools": [ { "tool": "apktool.jar", @@ -11,5 +12,9 @@ "decompiled": "./decompiled", "patches": "./patches", "dist": "./dist" + }, + "xml_ns": { + "android": "http://schemas.android.com/apk/res/android", + "app": "http://schemas.android.com/apk/res-auto" } } \ No newline at end of file diff --git a/config.py b/config.py index a6c4912..d8a9199 100644 --- a/config.py +++ b/config.py @@ -30,18 +30,31 @@ class ConfigFolders(TypedDict): dist: str +class ConfigXmlNS(TypedDict): + android: str + app: str + + class Config(TypedDict): + log_level: str tools: list[ConfigTools] folders: ConfigFolders + xml_ns: ConfigXmlNS def load_config() -> Config: + config = None + 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 = json.loads(file.read()) + + log.setLevel(config.get("log_level", "NOTSET").upper()) + + return config config = load_config() diff --git a/patches/change_color_theme.py b/patches/change_color_theme.py index 16f1a72..889e035 100644 --- a/patches/change_color_theme.py +++ b/patches/change_color_theme.py @@ -1,73 +1,88 @@ """Change app color theme""" +# patch settings +# priority, default: -99 (run before last) priority = -99 -import os +# imports +## bundled from typing import TypedDict from beaupy import select -from tqdm import tqdm + +## installed from lxml import etree +## custom +from config import config, log, console + + +class PatchConfig_ChangeColorThemeValue(TypedDict): + attributes: list[dict[str, str]] + text: list[dict[str, str]] + files: list[dict[str, str]] + + class PatchConfig_ChangeColorTheme(TypedDict): - src: str - themes: str + themes: list[str] + key: PatchConfig_ChangeColorThemeValue -def apply(config: PatchConfig_ChangeColorTheme) -> bool: - print("select color theme to apply") - theme = select(config["themes"], cursor="->", cursor_style="cyan") - theme_attr = config[theme]['attributes'] - theme_text = config[theme]['text'] - theme_files = config[theme]['files'] +def apply(patch_config: PatchConfig_ChangeColorTheme) -> bool: - with tqdm( - total=len(theme_attr), - unit="attr", - unit_divisor=1, - desc="color attributes" - ) as bar: - for attr in theme_attr: - parser = etree.XMLParser(remove_blank_text=True) - tree = etree.parse(f"{config['src']}/{attr['file_path']}", parser) - root = tree.getroot() - root.find(attr['tag_path']).set(attr['attr_name'], attr['attr_value']['to']) - tree.write( - f"{config['src']}/{attr['file_path']}", - pretty_print=True, - xml_declaration=True, - encoding="utf-8", - ) - bar.update() - - with tqdm( - total=len(theme_text), - unit="attr", - unit_divisor=1, - desc="color values" - ) as bar: - for text in theme_text: - parser = etree.XMLParser(remove_blank_text=True) - tree = etree.parse(f"{config['src']}/{text['file_path']}", parser) - root = tree.getroot() - root.find(text['tag_path']).text = text['text']['to'] - tree.write( - f"{config['src']}/{text['file_path']}", - pretty_print=True, - xml_declaration=True, - encoding="utf-8", - ) - bar.update() + console.print("select color theme to apply (press [bold]enter[/bold] to confirm)") + theme = select(patch_config["themes"], cursor="->", cursor_style="cyan") + if not theme: + console.print(f"theme: default") + return False + console.print(f"theme: {theme}") + theme_attr = patch_config[theme]["attributes"] + theme_text = patch_config[theme]["text"] + theme_files = patch_config[theme]["files"] + + for item in theme_attr: + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse( + f"{config['folders']['decompiled']}/{item['file_path']}", parser + ) + root = tree.getroot() + root.find(item["tag_path"]).set(item["attr_name"], item["attr_value"]["to"]) + tree.write( + f"{config['folders']['decompiled']}/{item['file_path']}", + pretty_print=True, + xml_declaration=True, + encoding="utf-8", + ) + log.debug( + f"[CHANGE_COLOR_THEME/ATTRIBUTES] set attribute `{item['attr_name']}` from `{item['attr_value']['from']}` to `{item['attr_value']['to']}`" + ) + + for item in theme_text: + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse( + f"{config['folders']['decompiled']}/{item['file_path']}", parser + ) + root = tree.getroot() + root.find(item["tag_path"]).text = item["text"]["to"] + tree.write( + f"{config['folders']['decompiled']}/{item['file_path']}", + pretty_print=True, + xml_declaration=True, + encoding="utf-8", + ) + log.debug( + f"[CHANGE_COLOR_THEME/VALUES] set text from `{item['text']['from']}` to `{item['text']['to']}`" + ) + if len(theme_files) > 0: - with tqdm( - total=len(theme_files), - unit="files", - unit_divisor=1, - desc="color files" - ) as bar: - for file in theme_files: - with open(f"{config['src']}/{file['file_path']}", "w", encoding="utf-8") as f: - f.write("\n".join(file['file_content'])) - bar.update() - - return True \ No newline at end of file + for item in theme_files: + with open( + f"{config['folders']['decompiled']}/{item['file_path']}", + "w", + encoding="utf-8", + ) as f: + f.write("\n".join(item["file_content"])) + log.debug(f"[CHANGE_COLOR_THEME/FILES] replaced file {item['file_path']}") + + log.debug(f"[CHANGE_COLOR_THEME] color theme `{theme}` has been applied") + return True diff --git a/patches/change_package_name.py b/patches/change_package_name.py index 55120e4..daae8f9 100644 --- a/patches/change_package_name.py +++ b/patches/change_package_name.py @@ -1,36 +1,51 @@ """Change package name""" +# patch settings +# priority, default: -100 (run last) priority = -100 +# imports +## bundled import os from typing import TypedDict +## custom +from config import config, log + + 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']}"): +def apply(patch_config: PatchConfig_ChangePackageName) -> bool: + assert ( + patch_config["new_package_name"] is not None + ), "new_package_name is not configured" + + for root, dirs, files in os.walk(f"{config['folders']['decompiled']}"): + if len(files) < 0: + continue + + dir_name = root.removeprefix(f"{config['folders']['decompiled']}/") + 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"] + "com.swiftsoft.anixartd", patch_config["new_package_name"] ) new_contents = new_contents.replace( "com/swiftsoft/anixartd", - config["new_package_name"].replace(".", "/"), + patch_config["new_package_name"].replace(".", "/"), ) with open(file_path, "w", encoding="utf-8") as file: @@ -38,22 +53,31 @@ def apply(config: dict) -> bool: except: pass - if os.path.exists(f"{config['src']}/smali/com/swiftsoft/anixartd"): + if os.path.exists( + f"{config['folders']['decompiled']}/smali/com/swiftsoft/anixartd" + ): rename_dir( - f"{config['src']}/smali/com/swiftsoft/anixartd", + f"{config['folders']['decompiled']}/smali/com/swiftsoft/anixartd", os.path.join( - f"{config['src']}", "smali", config["new_package_name"].replace(".", "/") + f"{config['folders']['decompiled']}", + "smali", + patch_config["new_package_name"].replace(".", "/"), ), ) - if os.path.exists(f"{config['src']}/smali_classes2/com/swiftsoft/anixartd"): + if os.path.exists( + f"{config['folders']['decompiled']}/smali_classes2/com/swiftsoft/anixartd" + ): rename_dir( - f"{config['src']}/smali_classes2/com/swiftsoft/anixartd", + f"{config['folders']['decompiled']}/smali_classes2/com/swiftsoft/anixartd", os.path.join( - f"{config['src']}", + f"{config['folders']['decompiled']}", "smali_classes2", - config["new_package_name"].replace(".", "/"), + patch_config["new_package_name"].replace(".", "/"), ), ) - return True \ No newline at end of file + log.debug( + f"[CHANGE_PACKAGE_NAME] package name has been changed to {patch_config['new_package_name']}" + ) + return True diff --git a/patches/compress.py b/patches/compress.py index e7723de..5c92b84 100644 --- a/patches/compress.py +++ b/patches/compress.py @@ -1,25 +1,45 @@ """Remove unnecessary resources""" +# patch settings +# priority, default: 0 priority = 0 -from tqdm import tqdm +# imports +## bundled import os import shutil from typing import TypedDict +## installed +from rich.progress import track + +## custom +from config import config, log, console + + +# Patch + 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) +def apply(patch_config: PatchConfig_Compress) -> bool: + path = f"{config['folders']['decompiled']}/unknown" + items = os.listdir(path) + for item in track( + items, + console=console, + description="[COMPRESS]", + total=len(items), + ): + item_path = f"{path}/{item}" if os.path.isfile(item_path): os.remove(item_path) - tqdm.write(f"removed file: {item_path}") + log.debug(f"[COMPRESS] removed file: {item_path}") elif os.path.isdir(item_path): - if item not in config["keep_dirs"]: + if item not in patch_config["keep_dirs"]: shutil.rmtree(item_path) - tqdm.write(f"removed directory: {item_path}") + log.debug(f"[COMPRESS] removed directory: {item_path}") + + log.debug(f"[COMPRESS] resources have been removed") return True diff --git a/patches/disable_ad.py b/patches/disable_ad.py index 0131c90..6bac24e 100644 --- a/patches/disable_ad.py +++ b/patches/disable_ad.py @@ -1,8 +1,12 @@ """Disable ad banners""" +# patch settings +# priority, default: 0 priority = 0 -from typing import TypedDict +# imports +## custom +from config import config, log from scripts.smali_parser import ( find_smali_method_end, find_smali_method_start, @@ -11,10 +15,7 @@ from scripts.smali_parser import ( ) -class PatchConfig_DisableAdBanner(TypedDict): - src: str - - +# Patch replace = """ .locals 0 const/4 p0, 0x1 @@ -23,9 +24,10 @@ replace = """ .locals 0 """ -def apply(config: PatchConfig_DisableAdBanner) -> bool: - path = f"{config['src']}/smali_classes2/com/swiftsoft/anixartd/Prefs.smali" +def apply(__no_config__) -> bool: + path = f"{config['folders']['decompiled']}/smali_classes2/com/swiftsoft/anixartd/Prefs.smali" lines = get_smali_lines(path) + for index, line in enumerate(lines): if line.find("IS_SPONSOR") >= 0: method_start = find_smali_method_start(lines, index) @@ -33,7 +35,8 @@ def apply(config: PatchConfig_DisableAdBanner) -> bool: new_content = replace_smali_method_body( lines, method_start, method_end, replace ) - with open(path, "w", encoding="utf-8") as file: file.writelines(new_content) + + log.debug(f"[DISABLE_AD] file {path} has been modified") return True diff --git a/patches/disable_beta_banner.py b/patches/disable_beta_banner.py index 83a8af8..a8e02fc 100644 --- a/patches/disable_beta_banner.py +++ b/patches/disable_beta_banner.py @@ -1,22 +1,22 @@ """Remove beta banner""" +# patch settings +# priority, default: 0 priority = 0 + +# imports +## bundled import os -from tqdm import tqdm + +## installed from lxml import etree -from typing import TypedDict +## custom +from config import config, log -class PatchConfig_DisableBetaBanner(TypedDict): - src: str - - -def apply(config: PatchConfig_DisableBetaBanner) -> bool: - xml_ns = { - "android": "http://schemas.android.com/apk/res/android", - "app": "http://schemas.android.com/apk/res-auto", - } +def apply(__no_config__) -> bool: + beta_banner_xml = f"{config['folders']['decompiled']}/res/layout/item_beta.xml" attributes = [ "paddingTop", "paddingBottom", @@ -27,19 +27,23 @@ def apply(config: PatchConfig_DisableBetaBanner) -> bool: "layout_marginTop", "layout_marginBottom", "layout_marginStart", - "layout_marginEnd" + "layout_marginEnd", ] - beta_banner_xml = f"{config['src']}/res/layout/item_beta.xml" if os.path.exists(beta_banner_xml): parser = etree.XMLParser(remove_blank_text=True) tree = etree.parse(beta_banner_xml, parser) root = tree.getroot() - + for attr in attributes: - tqdm.write(f"set {attr} = 0.0dip") - root.set(f"{{{xml_ns['android']}}}{attr}", "0.0dip") + log.debug( + f"[DISABLE_BETA_BANNER] set attribute `{attr}` from `{root.get(attr)}` to `0.0dip`" + ) + root.set(f"{{{config['xml_ns']['android']}}}{attr}", "0.0dip") - tree.write(beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8") + tree.write( + beta_banner_xml, pretty_print=True, xml_declaration=True, encoding="utf-8" + ) + log.debug(f"[DISABLE_BETA_BANNER] file {beta_banner_xml} has been modified") return True diff --git a/requirements.txt b/requirements.txt index 05f639f..154e25e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ requests -tqdm lxml rich beaupy \ No newline at end of file diff --git a/scripts/download_tools.py b/scripts/download_tools.py index 9eb4ec9..fade69d 100644 --- a/scripts/download_tools.py +++ b/scripts/download_tools.py @@ -1,7 +1,28 @@ -import requests import os -from tqdm import tqdm -from config import config, log +import requests +import logging +from config import config, log, console +from rich.progress import ( + BarColumn, + DownloadColumn, + Progress, + TextColumn, + TimeRemainingColumn, + TransferSpeedColumn, +) + +progress = Progress( + TextColumn("[bold blue]{task.fields[filename]}", justify="right"), + BarColumn(bar_width=None), + "[progress.percentage]{task.percentage:>3.1f}%", + "•", + DownloadColumn(), + "•", + TransferSpeedColumn(), + "•", + TimeRemainingColumn(), + console=console +) def check_if_tool_exists(tool: str) -> bool: @@ -19,27 +40,30 @@ def check_if_tool_exists(tool: str) -> bool: else: return True +requests_log = logging.getLogger("urllib3.connectionpool") +requests_log.setLevel(logging.WARNING) def download_tool(url: str, tool: str): if not check_if_tool_exists(tool): - log.info(f"downloading a tool: `{tool}`") + progress.start() + try: + log.info(f"Requesting {url}") 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): + total = int(response.headers.get("content-length", None)) + task_id = progress.add_task("download", start=False, total=total, filename=tool) + + with open(f"{config['folders']['tools']}/{tool}", "wb") as file: + progress.start_task(task_id) + for bytes in response.iter_content(chunk_size=32768): size = file.write(bytes) - bar.update(size) + progress.update(task_id, advance=size) + log.info(f"`{tool}` downloaded") except Exception as e: log.error(f"error while downloading `{tool}`: {e}") + progress.stop() def check_and_download_all_tools(): for tool in config["tools"]: diff --git a/scripts/select_patches.py b/scripts/select_patches.py index c1e0969..2c44a79 100644 --- a/scripts/select_patches.py +++ b/scripts/select_patches.py @@ -1,8 +1,10 @@ import os, json import importlib from typing import TypedDict + from beaupy import select_multiple -from tqdm import tqdm +from rich.progress import BarColumn, Progress, TextColumn + from config import config, log, console @@ -59,6 +61,14 @@ class PatchStatus(TypedDict): status: bool +progress = Progress( + "[progress.description]{task.description}", + TextColumn(text_format="{task.fields[patch]}"), + BarColumn(bar_width=None), + "[blue]{task.completed}/{task.total}", +) + + def apply_patches(patches: list[str]) -> list[PatchStatus]: modules = [] statuses = [] @@ -69,25 +79,27 @@ def apply_patches(patches: list[str]) -> list[PatchStatus]: ) modules.append(Patch(name, module)) modules.sort(key=lambda x: x.package.priority, reverse=True) - - with tqdm( - total=len(modules), - unit="patch", - unit_divisor=1, - ) as bar: - for patch in modules: - bar.set_description(f"{patch.name}") - conf = {} - if os.path.exists(f"{config['folders']['patches']}/{patch.name}.config.json"): + + with progress: + task = progress.add_task("applying patch:", total=len(modules), patch="") + for module in modules: + progress.update(task, patch=module.name) + + patch_conf = {} + if os.path.exists( + f"{config['folders']['patches']}/{module.name}.config.json" + ): with open( - f"{config['folders']['patches']}/{patch.name}.config.json", + f"{config['folders']['patches']}/{module.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}) - bar.update() + ) as f: + patch_conf = json.loads(f.read()) + + status = module.apply(patch_conf) + statuses.append({"name": module.name, "status": status}) + progress.update(task, advance=1) + + progress.update(task, description="patches applied", patch="") return statuses