diff --git a/.env.my b/.env.my index 7038d24..c3793d8 100644 --- a/.env.my +++ b/.env.my @@ -1,6 +1,6 @@ # Global EXTRA_FILES=https://github.com/ReVanced/GmsCore/releases/latest@Revanced-Microg.apk -PATCH_APPS=YOUTUBE_INOTIA00,YOUTUBE_ANDEA,YOUTUBE_MUSIC_INOTIA00,YOUTUBE_MUSIC_ANDEA,REDDIT_INOTIA00,REDDIT_ANDEA,YOUTUBE_REVANCED,YOUTUBE_MUSIC_REVANCED +PATCH_APPS=YOUTUBE_INOTIA00,YOUTUBE_ANDEA,YOUTUBE_MUSIC_INOTIA00,YOUTUBE_MUSIC_ANDEA,REDDIT_INOTIA00,REDDIT_ANDEA,YOUTUBE_REVANCED,YOUTUBE_MUSIC_REVANCED,SPOTIFY GLOBAL_CLI_DL=https://github.com/inotia00/revanced-cli/releases/latest GLOBAL_PATCHES_DL=https://github.com/revanced/revanced-patches/releases/latest @@ -19,7 +19,7 @@ YOUTUBE_ANDEA_PACKAGE_NAME=com.google.android.youtube # YouTube (Using ReVanced Patches) YOUTUBE_REVANCED_DL_SOURCE=https://www.apkmirror.com/apk/google-inc/youtube/ -YOUTUBE_REVANCED_PATCHES_DL=https://github.com/ReVanced/revanced-patches/releases/latest-prerelease +YOUTUBE_REVANCED_PATCHES_DL=https://github.com/ReVanced/revanced-patches/releases/latest YOUTUBE_REVANCED_EXCLUDE_PATCH=custom-branding-icon-youtube,custom-branding-name-youtube,enable-debug-logging,hide-fullscreen-button,custom-branding-icon-for-youtube,custom-branding-name-for-youtube YOUTUBE_REVANCED_PACKAGE_NAME=com.google.android.youtube @@ -38,7 +38,7 @@ YOUTUBE_MUSIC_ANDEA_PACKAGE_NAME=com.google.android.apps.youtube.music # YouTube Music (Using ReVanced Patches) YOUTUBE_MUSIC_REVANCED_DL_SOURCE=https://www.apkmirror.com/apk/google-inc/youtube-music/ -YOUTUBE_MUSIC_REVANCED_PATCHES_DL=https://github.com/ReVanced/revanced-patches/releases/latest-prerelease +YOUTUBE_MUSIC_REVANCED_PATCHES_DL=https://github.com/ReVanced/revanced-patches/releases/latest YOUTUBE_MUSIC_REVANCED_EXCLUDE_PATCH=custom-branding-icon-youtube-music,custom-branding-name-youtube-music,enable-compact-dialog,enable-debug-logging,enable-old-player-layout,custom-branding-icon-for-youtube-music,custom-branding-name-for-youtube-music,custom-header-for-youtube-music YOUTUBE_MUSIC_REVANCED_PACKAGE_NAME=com.google.android.apps.youtube.music @@ -54,5 +54,10 @@ REDDIT_ANDEA_PATCHES_DL=https://github.com/anddea/revanced-patches/releases/late REDDIT_ANDEA_EXCLUDE_PATCH=change-package-name,custom-branding-name-for-reddit REDDIT_ANDEA_PACKAGE_NAME=com.reddit.frontpage +#Spotify +SPOTIFY_DL_SOURCE=apkeep +SPOTIFY_PATCHES_DL=https://github.com/anddea/revanced-patches/releases/latest-prerelease +SPOTIFY_PACKAGE_NAME=com.spotify.music + # GitHub Repository GITHUB_REPOSITORY=nikhilbadyal/docker-py-revanced diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 04bcf39..2028855 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -45,6 +45,10 @@ jobs: echo "${{ secrets.ENVS }}" >> .env echo "GITHUB_REPOSITORY=${{ github.repository }}" >> .env + - name: Update Env from secrets for custom build + run: | + echo "${{ secrets.SECRETS }}" >> .env + - name: Setup python uses: actions/setup-python@main with: diff --git a/.gitignore b/.gitignore index 5a0dcc3..c229c9e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ status.md *.zip apks/* *.rvp +*.backup diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 122606f..a3176b8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: - id: ruff args: - "--config=pyproject.toml" - - "--fix" + - "--unsafe-fixes" - repo: https://github.com/psf/black rev: 25.1.0 diff --git a/Dockerfile-base b/Dockerfile-base index c9ff022..c23aae9 100644 --- a/Dockerfile-base +++ b/Dockerfile-base @@ -1,5 +1,5 @@ # Use a specific version of the base Python image -ARG PYTHON_VERSION=3.13.1-slim-bullseye +ARG PYTHON_VERSION=3.13.1-slim-bookworm FROM python:${PYTHON_VERSION} AS python @@ -33,4 +33,24 @@ RUN apt-get -qq update && \ # Set Java home environment variable ENV JAVA_HOME=/usr/lib/jvm/zulu17-ca-amd64 +# Ensure curl and jq are available for the next step +RUN apt-get update && \ + apt-get install -y curl jq libssl3 && \ + rm -rf /var/lib/apt/lists/* +ENV PATH="/usr/local/bin:${PATH}" +# Now use them safely +RUN set -eux; \ + ARCH="$(uname -m)"; \ + case "$ARCH" in \ + x86_64) ARCH_NAME="x86_64-unknown-linux-gnu" ;; \ + aarch64) ARCH_NAME="aarch64-unknown-linux-gnu" ;; \ + *) echo "Unsupported arch: $ARCH" && exit 1 ;; \ + esac; \ + URL=$(curl -s https://api.github.com/repos/EFForg/apkeep/releases/latest \ + | jq -r ".assets[] | select(.name == \"apkeep-${ARCH_NAME}\") | .browser_download_url"); \ + curl -L "$URL" -o /usr/local/bin/apkeep; \ + chmod +x /usr/local/bin/apkeep; \ + echo "Installed apkeep from $URL"; \ + /usr/local/bin/apkeep --version + CMD ["bash"] diff --git a/README.md b/README.md index 3c3a3cd..3686792 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,10 @@ You can use any of the following methods to build. 6. Google Drive - Supports downloading from Google Drive lint 1. Link Format - https://drive.google.com/uc? 2. Example Link - https://drive.google.com/uc?id=1ad44UTghbDty8o36Nrp3ZMyUzkPckIqY + 7. APKEEP - Support downloading using [APKEEP](https://github.com/EFForg/apkeep) + 1. Link Format - apkeep + 2. Example Link - apkeep + Note - You need to provide APKEEP_EMAIL and APKEEP_TOKEN in the SECRETS Env.
Please verify the source of original APKs yourself with links provided. I'm not responsible for any damage caused.If you know any better/safe source to download clean. Open a discussion. diff --git a/check_resource_updates.py b/check_resource_updates.py index a74291a..51aeb30 100644 --- a/check_resource_updates.py +++ b/check_resource_updates.py @@ -15,12 +15,13 @@ def check_if_build_is_required() -> bool: env.read_env() config = RevancedConfig(env) needs_to_repatched = [] + resource_cache: dict[str, tuple[str, str]] = {} for app_name in env.list("PATCH_APPS", default_build): logger.info(f"Checking {app_name}") app_obj = get_app(config, app_name) old_patches_version = GitHubManager(env).get_last_version(app_obj, patches_version_key) old_patches_source = GitHubManager(env).get_last_version_source(app_obj, patches_dl_key) - app_obj.download_patch_resources(config) + app_obj.download_patch_resources(config, resource_cache) if GitHubManager(env).should_trigger_build( old_patches_version, old_patches_source, diff --git a/main.py b/main.py index cd25dd7..8e4017e 100644 --- a/main.py +++ b/main.py @@ -34,15 +34,26 @@ def main() -> None: updates_info = load_older_updates(env) logger.info(f"Will Patch only {config.apps}") + + # Caches for reuse + download_cache: dict[str, tuple[str, str]] = {} + resource_cache: dict[str, tuple[str, str]] = {} + for possible_app in config.apps: logger.info(f"Trying to build {possible_app}") try: app = get_app(config, possible_app) - app.download_patch_resources(config) + + # Use shared resource cache + app.download_patch_resources(config, resource_cache) + patcher = Patches(config, app) parser = Parser(patcher, config) app_all_patches = patcher.get_app_configs(app) - app.download_apk_for_patching(config) + + # Use shared APK cache + app.download_apk_for_patching(config, download_cache) + parser.include_exclude_patch(app, app_all_patches, patcher.patches_dict) logger.info(app) updates_info = save_patch_info(app, updates_info) @@ -55,6 +66,7 @@ def main() -> None: logger.exception(e) except BuilderError as e: logger.exception(f"Failed to build {possible_app} because of {e}") + write_changelog_to_file(updates_info) diff --git a/src/app.py b/src/app.py index 8383a9e..a408a57 100644 --- a/src/app.py +++ b/src/app.py @@ -50,8 +50,12 @@ class APP(object): config.global_space_formatted, ) - def download_apk_for_patching(self: Self, config: RevancedConfig) -> None: - """Download apk to be patched.""" + def download_apk_for_patching( + self: Self, + config: RevancedConfig, + download_cache: dict[str, tuple[str, str]], + ) -> None: + """Download apk to be patched, skipping if already downloaded.""" from src.downloader.download import Downloader from src.downloader.factory import DownloaderFactory @@ -63,15 +67,23 @@ class APP(object): logger.info("Downloading apk to be patched by scrapping") try: if not self.download_source: - self.download_source = apk_sources[self.app_name].format(self.package_name) + 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 + raise DownloadError(msg) from key + + # Skip if already downloaded + if self.download_source in download_cache: + logger.info(f"Skipping download. Reusing APK from cache for {self.app_name}") + self.download_file_name, self.download_dl = download_cache[self.download_source] + 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 + download_cache[self.download_source] = (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. @@ -135,7 +147,11 @@ class APP(object): Downloader(config).direct_download(url, file_name) return tag, file_name - def download_patch_resources(self: Self, config: RevancedConfig) -> None: + 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 @@ -143,22 +159,33 @@ class APP(object): 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.") - # Create a list of resource download tasks + download_tasks = [ ("cli", self.cli_dl, config, ".*jar"), ("patches", self.patches_dl, config, ".*rvp"), ] - # Using a ThreadPoolExecutor for parallelism with ThreadPoolExecutor(1) as executor: - futures = {resource_name: executor.submit(self.download, *args) for resource_name, *args in download_tasks} + 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.resource[resource_name] = { + "file_name": file_name, + "version": tag, + } + continue + + futures[resource_name] = executor.submit(self.download, url, cfg, assets_filter) - # Wait for all tasks to complete concurrent.futures.wait(futures.values()) - # Retrieve results from completed tasks for resource_name, future in futures.items(): try: tag, file_name = future.result() @@ -166,8 +193,12 @@ class APP(object): "file_name": file_name, "version": tag, } + resource_cache[download_tasks[["cli", "patches"].index(resource_name)][1].strip()] = ( + tag, + file_name, + ) except BuilderError as e: - msg = "Failed to download resource." + msg = f"Failed to download {resource_name} resource." raise PatchingFailedError(msg) from e @staticmethod diff --git a/src/downloader/apkkeep.py b/src/downloader/apkkeep.py new file mode 100644 index 0000000..db4d903 --- /dev/null +++ b/src/downloader/apkkeep.py @@ -0,0 +1,66 @@ +"""Apkeep Downloader Class.""" + +import subprocess +from time import perf_counter +from typing import Any, Self + +from loguru import logger + +from src.app import APP +from src.downloader.download import Downloader +from src.exceptions import DownloadError + + +class Apkeep(Downloader): + """Apkeep-based Downloader.""" + + def _run_apkeep(self: Self, package_name: str, version: str = "") -> str: + """Run apkeep CLI to fetch APK from Google Play.""" + email = self.config.env.str("APKEEP_EMAIL") + token = self.config.env.str("APKEEP_TOKEN") + + if not email or not token: + msg = "APKEEP_EMAIL and APKEEP_TOKEN must be set in environment." + raise DownloadError(msg) + + file_name = f"{package_name}.apk" + file_path = self.config.temp_folder / file_name + + if file_path.exists(): + logger.debug(f"{file_name} already downloaded.") + return file_name + + cmd = [ + "apkeep", + "-a", + f"{package_name}@{version}" if version and version != "latest" else package_name, + "-d", + "google-play", + "-e", + email, + "-t", + token, + self.config.temp_folder_name, + ] + logger.debug(f"Running command: {cmd}") + + start = perf_counter() + process = subprocess.Popen(cmd, stdout=subprocess.PIPE) + output = process.stdout + if not output: + msg = "Failed to send request for patching." + raise DownloadError(msg) + for line in output: + logger.debug(line.decode(), flush=True, end="") + process.wait() + if process.returncode != 0: + msg = f"Command failed with exit code {process.returncode} for app {package_name}" + raise DownloadError(msg) + logger.info(f"Downloading completed for app {package_name} in {perf_counter() - start:.2f} seconds.") + return file_name + + def latest_version(self: Self, app: APP, **kwargs: Any) -> tuple[str, str]: + """Download latest version from Google Play via Apkeep.""" + file_name = self._run_apkeep(app.package_name) + logger.info(f"Got file name as {file_name}") + return file_name, f"apkeep://google-play/{app.package_name}" diff --git a/src/downloader/factory.py b/src/downloader/factory.py index 206b6f6..312caec 100644 --- a/src/downloader/factory.py +++ b/src/downloader/factory.py @@ -1,6 +1,7 @@ """Downloader Factory.""" from src.config import RevancedConfig +from src.downloader.apkkeep import Apkeep from src.downloader.apkmirror import ApkMirror from src.downloader.apkmonk import ApkMonk from src.downloader.apkpure import ApkPure @@ -12,6 +13,7 @@ from src.downloader.sources import ( APK_MIRROR_BASE_URL, APK_MONK_BASE_URL, APK_PURE_BASE_URL, + APKEEP, APKS_SOS_BASE_URL, DRIVE_DOWNLOAD_BASE_URL, GITHUB_BASE_URL, @@ -47,5 +49,7 @@ class DownloaderFactory(object): return ApkMonk(config) if apk_source.startswith(DRIVE_DOWNLOAD_BASE_URL): return GoogleDrive(config) + if apk_source.startswith(APKEEP): + return Apkeep(config) msg = "No download factory found." raise DownloadError(msg, url=apk_source) diff --git a/src/downloader/github.py b/src/downloader/github.py index cb2229d..214825c 100644 --- a/src/downloader/github.py +++ b/src/downloader/github.py @@ -35,7 +35,7 @@ class Github(Downloader): } if self.config.personal_access_token: logger.debug("Using personal access token") - headers["Authorization"] = f"token {self.config.personal_access_token}" + headers["Authorization"] = f"Bearer {self.config.personal_access_token}" response = requests.get(repo_url, headers=headers, timeout=request_timeout) handle_request_response(response, repo_url) if repo_name == "revanced-patches": @@ -80,7 +80,7 @@ class Github(Downloader): "Content-Type": "application/vnd.github.v3+json", } if config.personal_access_token: - headers["Authorization"] = f"token {config.personal_access_token}" + headers["Authorization"] = f"Bearer {config.personal_access_token}" response = requests.get(api_url, headers=headers, timeout=request_timeout) handle_request_response(response, api_url) update_changelog(f"{github_repo_owner}/{github_repo_name}", response.json()) diff --git a/src/downloader/sources.py b/src/downloader/sources.py index 0d5b3d9..9e4824d 100644 --- a/src/downloader/sources.py +++ b/src/downloader/sources.py @@ -19,6 +19,7 @@ APK_COMBO_GENERIC_URL = APK_COMBO_BASE_URL + "/genericApp/{}" not_found_icon = "https://img.icons8.com/bubbles/500/android-os.png" revanced_api = "https://api.revanced.app/v2/patches/latest" APK_MONK_BASE_URL = "https://www.apkmonk.com" +APKEEP = "apkeep" APK_MONK_APK_URL = APK_MONK_BASE_URL + "/app/{}/" APK_MONK_ICON_URL = "https://cdn.apkmonk.com/logos/{}" DRIVE_BASE_URL = "https://drive.google.com" diff --git a/src/parser.py b/src/parser.py index 0341fe1..16f2ccb 100644 --- a/src/parser.py +++ b/src/parser.py @@ -244,6 +244,7 @@ class Parser(object): excluded = set(possible_archs) - set(app.archs_to_build) for arch in excluded: args.extend(("--rip-lib", arch)) + args.extend(("--purge",)) start = perf_counter() logger.debug(f"Sending request to revanced cli for building with args java {args}") process = Popen(["java", *args], stdout=PIPE)