Files
docker-py-revanced/src/app.py
T
Nikhil Badyal c436c4bc98 🚨 Lint Fixes
2025-06-25 09:32:44 +05:30

270 lines
12 KiB
Python

"""Class to represent apk to be patched."""
import concurrent
import hashlib
import pathlib
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from typing import Any, Self
from loguru import logger
from pytz import timezone
from src.config import RevancedConfig
from src.downloader.sources import apk_sources
from src.exceptions import BuilderError, DownloadError, PatchingFailedError
from src.utils import slugify, time_zone
class APP(object):
"""Patched APK."""
def __init__(self: Self, app_name: str, package_name: str, config: RevancedConfig) -> None:
"""Initialize APP.
Args:
----
app_name (str): Name of the app.
config (RevancedConfig): Configuration object.
"""
self.app_name = app_name
self.app_version = config.env.str(f"{app_name}_VERSION".upper(), None)
self.experiment = False
self.cli_dl = config.env.str(f"{app_name}_CLI_DL".upper(), config.global_cli_dl)
# Support multiple patch bundles via comma-separated URLs
patches_dl_raw = config.env.str(f"{app_name}_PATCHES_DL".upper(), config.global_patches_dl)
self.patches_dl_list = [url.strip() for url in patches_dl_raw.split(",") if url.strip()]
# Keep backward compatibility
self.patches_dl = patches_dl_raw
self.exclude_request: list[str] = config.env.list(f"{app_name}_EXCLUDE_PATCH".upper(), [])
self.include_request: list[str] = config.env.list(f"{app_name}_INCLUDE_PATCH".upper(), [])
self.resource: dict[str, dict[str, str]] = {}
self.patch_bundles: list[dict[str, str]] = [] # Store multiple patch bundles
self.no_of_patches: int = 0
self.keystore_name = config.env.str(f"{app_name}_KEYSTORE_FILE_NAME".upper(), config.global_keystore_name)
self.archs_to_build = config.env.list(f"{app_name}_ARCHS_TO_BUILD".upper(), config.global_archs_to_build)
self.options_file = config.env.str(f"{app_name}_OPTIONS_FILE".upper(), config.global_options_file)
self.download_file_name = ""
self.download_dl = config.env.str(f"{app_name}_DL".upper(), "")
self.download_source = config.env.str(f"{app_name}_DL_SOURCE".upper(), "")
self.package_name = package_name
self.old_key = config.env.bool(f"{app_name}_OLD_KEY".upper(), config.global_old_key)
self.patches: list[dict[Any, Any]] = []
self.space_formatted = config.env.bool(
f"{app_name}_SPACE_FORMATTED_PATCHES".upper(),
config.global_space_formatted,
)
def download_apk_for_patching(
self: Self,
config: RevancedConfig,
download_cache: dict[tuple[str, str], tuple[str, str]],
) -> None:
"""Download apk to be patched, skipping if already downloaded (matching source and version)."""
from src.downloader.download import Downloader # noqa: PLC0415
from src.downloader.factory import DownloaderFactory # noqa: PLC0415
if self.download_dl:
logger.info("Downloading apk to be patched using provided dl")
self.download_file_name = f"{self.app_name}.apk"
Downloader(config).direct_download(self.download_dl, self.download_file_name)
else:
logger.info("Downloading apk to be patched by scrapping")
try:
if not self.download_source:
self.download_source = apk_sources[self.app_name.lower()].format(self.package_name)
except KeyError as key:
msg = f"App {self.app_name} not supported officially yet. Please provide download source in env."
raise DownloadError(msg) from key
cache_key = (self.download_source, self.app_version)
if cache_key in download_cache:
logger.info(f"Skipping download. Reusing APK from cache for {self.app_name} ({self.app_version})")
self.download_file_name, self.download_dl = download_cache[cache_key]
return
downloader = DownloaderFactory.create_downloader(config=config, apk_source=self.download_source)
self.download_file_name, self.download_dl = downloader.download(self.app_version, self)
# Save to cache using (source, version) tuple
download_cache[cache_key] = (self.download_file_name, self.download_dl)
def get_output_file_name(self: Self) -> str:
"""The function returns a string representing the output file name.
Returns
-------
a string that represents the output file name for an APK file.
"""
current_date = datetime.now(timezone(time_zone))
formatted_date = current_date.strftime("%Y%b%d.%I%M%p").upper()
return f"Re{self.app_name}-Version{slugify(self.app_version)}-PatchVersion{slugify(self.resource["patches"]["version"])}-{formatted_date}-output.apk" # noqa: E501
def __str__(self: "APP") -> str:
"""Returns the str representation of the app."""
attrs = vars(self)
return ", ".join([f"{key}: {value}" for key, value in attrs.items()])
def for_dump(self: Self) -> dict[str, Any]:
"""Convert the instance of this class to json."""
return self.__dict__
@staticmethod
def download(url: str, config: RevancedConfig, assets_filter: str, file_name: str = "") -> tuple[str, str]:
"""The `download` function downloads a file from a given URL & filters the assets based on a given filter.
Parameters
----------
url : str
The `url` parameter is a string that represents the URL of the resource you want to download.
It can be a URL from GitHub or a local file URL.
config : RevancedConfig
The `config` parameter is an instance of the `RevancedConfig` class. It is used to provide
configuration settings for the download process.
assets_filter : str
The `assets_filter` parameter is a string that is used to filter the assets to be downloaded
from a GitHub repository. It is used when the `url` parameter starts with "https://github". The
`assets_filter` string is matched against the names of the assets in the repository, and only
file_name : str
The `file_name` parameter is a string that represents the name of the file that will be
downloaded. If no value is provided for `file_name`, the function will generate a filename based
on the URL of the file being downloaded.
Returns
-------
tuple of strings, which is the tag,file name of the downloaded file.
"""
from src.downloader.download import Downloader # noqa: PLC0415
url = url.strip()
tag = "latest"
if url.startswith("https://github"):
from src.downloader.github import Github # noqa: PLC0415
tag, url = Github.patch_resource(url, assets_filter, config)
if tag.startswith("tags/"):
tag = tag.split("/")[-1]
elif url.startswith("local://"):
return tag, url.split("/")[-1]
if not file_name:
extension = pathlib.Path(url).suffix
file_name = APP.generate_filename(url) + extension
Downloader(config).direct_download(url, file_name)
return tag, file_name
def _setup_download_tasks(self: Self) -> list[tuple[str, str, None, str]]:
"""Setup download tasks for CLI and patch bundles."""
download_tasks = [
("cli", self.cli_dl, None, ".*jar"),
]
# Download multiple patch bundles
for i, patches_url in enumerate(self.patches_dl_list):
bundle_name = f"patches_{i}" if len(self.patches_dl_list) > 1 else "patches"
download_tasks.append((bundle_name, patches_url, None, ".*rvp"))
return download_tasks
def _handle_cached_resource(self: Self, resource_name: str, tag: str, file_name: str) -> None:
"""Handle cached resource and update appropriate data structures."""
if resource_name.startswith("patches"):
self.patch_bundles.append(
{
"name": resource_name,
"file_name": file_name,
"version": tag,
},
)
# Keep backward compatibility for single bundle
if resource_name == "patches" or len(self.patches_dl_list) == 1:
self.resource["patches"] = {
"file_name": file_name,
"version": tag,
}
else:
self.resource[resource_name] = {
"file_name": file_name,
"version": tag,
}
def _handle_downloaded_resource(
self: Self,
resource_name: str,
tag: str,
file_name: str,
download_tasks: list[tuple[str, str, RevancedConfig, str]],
resource_cache: dict[str, tuple[str, str]],
) -> None:
"""Handle newly downloaded resource and update cache."""
self._handle_cached_resource(resource_name, tag, file_name)
# Update cache for the corresponding URL
for task_name, task_url, _, _ in download_tasks:
if task_name == resource_name:
resource_cache[task_url.strip()] = (tag, file_name)
break
def download_patch_resources(
self: Self,
config: RevancedConfig,
resource_cache: dict[str, tuple[str, str]],
) -> None:
"""The function `download_patch_resources` downloads various resources req. for patching.
Parameters
----------
config : RevancedConfig
The `config` parameter is an instance of the `RevancedConfig` class. It is used to provide
configuration settings for the resource download tasks.
resource_cache: dict[str, tuple[str, str]]
"""
logger.info("Downloading resources for patching.")
base_tasks = self._setup_download_tasks()
# Update download tasks with config
download_tasks: list[tuple[str, str, RevancedConfig, str]] = [
(name, url, config, filter_pattern) for name, url, _, filter_pattern in base_tasks
]
with ThreadPoolExecutor(config.max_resource_workers) as executor: # Use configurable worker count
futures: dict[str, concurrent.futures.Future[tuple[str, str]]] = {}
for resource_name, raw_url, cfg, assets_filter in download_tasks:
url = raw_url.strip()
if url in resource_cache:
logger.info(f"Skipping {resource_name} download, using cached resource: {url}")
tag, file_name = resource_cache[url]
self._handle_cached_resource(resource_name, tag, file_name)
continue
futures[resource_name] = executor.submit(self.download, url, cfg, assets_filter)
concurrent.futures.wait(futures.values())
for resource_name, future in futures.items():
try:
tag, file_name = future.result()
self._handle_downloaded_resource(resource_name, tag, file_name, download_tasks, resource_cache)
except BuilderError as e:
msg = f"Failed to download {resource_name} resource."
raise PatchingFailedError(msg) from e
@staticmethod
def generate_filename(url: str) -> str:
"""The function `generate_filename` takes URL as input and returns a hashed version of the URL as the filename.
Parameters
----------
url : str
The `url` parameter is a string that represents a URL.
Returns
-------
the encoded URL as a string.
"""
encoded_url: str = hashlib.sha256(url.encode()).hexdigest()
return encoded_url