Added apkeep support

This commit is contained in:
Nikhil Badyal
2025-04-19 11:18:31 +05:30
committed by Nikhil Badyal
parent bb05ec0f6f
commit 05ca33164c
14 changed files with 173 additions and 23 deletions
+8 -3
View File
@@ -1,6 +1,6 @@
# Global # Global
EXTRA_FILES=https://github.com/ReVanced/GmsCore/releases/latest@Revanced-Microg.apk 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_CLI_DL=https://github.com/inotia00/revanced-cli/releases/latest
GLOBAL_PATCHES_DL=https://github.com/revanced/revanced-patches/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 (Using ReVanced Patches)
YOUTUBE_REVANCED_DL_SOURCE=https://www.apkmirror.com/apk/google-inc/youtube/ 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_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 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 (Using ReVanced Patches)
YOUTUBE_MUSIC_REVANCED_DL_SOURCE=https://www.apkmirror.com/apk/google-inc/youtube-music/ 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_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 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_EXCLUDE_PATCH=change-package-name,custom-branding-name-for-reddit
REDDIT_ANDEA_PACKAGE_NAME=com.reddit.frontpage 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
GITHUB_REPOSITORY=nikhilbadyal/docker-py-revanced GITHUB_REPOSITORY=nikhilbadyal/docker-py-revanced
+4
View File
@@ -45,6 +45,10 @@ jobs:
echo "${{ secrets.ENVS }}" >> .env echo "${{ secrets.ENVS }}" >> .env
echo "GITHUB_REPOSITORY=${{ github.repository }}" >> .env echo "GITHUB_REPOSITORY=${{ github.repository }}" >> .env
- name: Update Env from secrets for custom build
run: |
echo "${{ secrets.SECRETS }}" >> .env
- name: Setup python - name: Setup python
uses: actions/setup-python@main uses: actions/setup-python@main
with: with:
+1
View File
@@ -11,3 +11,4 @@ status.md
*.zip *.zip
apks/* apks/*
*.rvp *.rvp
*.backup
+1 -1
View File
@@ -28,7 +28,7 @@ repos:
- id: ruff - id: ruff
args: args:
- "--config=pyproject.toml" - "--config=pyproject.toml"
- "--fix" - "--unsafe-fixes"
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 25.1.0 rev: 25.1.0
+21 -1
View File
@@ -1,5 +1,5 @@
# Use a specific version of the base Python image # 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 FROM python:${PYTHON_VERSION} AS python
@@ -33,4 +33,24 @@ RUN apt-get -qq update && \
# Set Java home environment variable # Set Java home environment variable
ENV JAVA_HOME=/usr/lib/jvm/zulu17-ca-amd64 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"] CMD ["bash"]
+4
View File
@@ -250,6 +250,10 @@ You can use any of the following methods to build.
6. Google Drive - Supports downloading from Google Drive lint 6. Google Drive - Supports downloading from Google Drive lint
1. Link Format - https://drive.google.com/uc?<id> 1. Link Format - https://drive.google.com/uc?<id>
2. Example Link - https://drive.google.com/uc?id=1ad44UTghbDty8o36Nrp3ZMyUzkPckIqY 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.
<br>Please verify the source of original APKs yourself with links provided. I'm not responsible for any damage <br>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. caused.If you know any better/safe source to download clean. Open a discussion.
+2 -1
View File
@@ -15,12 +15,13 @@ def check_if_build_is_required() -> bool:
env.read_env() env.read_env()
config = RevancedConfig(env) config = RevancedConfig(env)
needs_to_repatched = [] needs_to_repatched = []
resource_cache: dict[str, tuple[str, str]] = {}
for app_name in env.list("PATCH_APPS", default_build): for app_name in env.list("PATCH_APPS", default_build):
logger.info(f"Checking {app_name}") logger.info(f"Checking {app_name}")
app_obj = get_app(config, app_name) app_obj = get_app(config, app_name)
old_patches_version = GitHubManager(env).get_last_version(app_obj, patches_version_key) 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) 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( if GitHubManager(env).should_trigger_build(
old_patches_version, old_patches_version,
old_patches_source, old_patches_source,
+14 -2
View File
@@ -34,15 +34,26 @@ def main() -> None:
updates_info = load_older_updates(env) updates_info = load_older_updates(env)
logger.info(f"Will Patch only {config.apps}") 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: for possible_app in config.apps:
logger.info(f"Trying to build {possible_app}") logger.info(f"Trying to build {possible_app}")
try: try:
app = get_app(config, possible_app) 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) patcher = Patches(config, app)
parser = Parser(patcher, config) parser = Parser(patcher, config)
app_all_patches = patcher.get_app_configs(app) 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) parser.include_exclude_patch(app, app_all_patches, patcher.patches_dict)
logger.info(app) logger.info(app)
updates_info = save_patch_info(app, updates_info) updates_info = save_patch_info(app, updates_info)
@@ -55,6 +66,7 @@ def main() -> None:
logger.exception(e) logger.exception(e)
except BuilderError as e: except BuilderError as e:
logger.exception(f"Failed to build {possible_app} because of {e}") logger.exception(f"Failed to build {possible_app} because of {e}")
write_changelog_to_file(updates_info) write_changelog_to_file(updates_info)
+44 -13
View File
@@ -50,8 +50,12 @@ class APP(object):
config.global_space_formatted, config.global_space_formatted,
) )
def download_apk_for_patching(self: Self, config: RevancedConfig) -> None: def download_apk_for_patching(
"""Download apk to be patched.""" 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.download import Downloader
from src.downloader.factory import DownloaderFactory from src.downloader.factory import DownloaderFactory
@@ -63,15 +67,23 @@ class APP(object):
logger.info("Downloading apk to be patched by scrapping") logger.info("Downloading apk to be patched by scrapping")
try: try:
if not self.download_source: 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: except KeyError as key:
msg = f"App {self.app_name} not supported officially yet. Please provide download source in env." msg = f"App {self.app_name} not supported officially yet. Please provide download source in env."
raise DownloadError( raise DownloadError(msg) from key
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) downloader = DownloaderFactory.create_downloader(config=config, apk_source=self.download_source)
self.download_file_name, self.download_dl = downloader.download(self.app_version, self) 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: def get_output_file_name(self: Self) -> str:
"""The function returns a string representing the output file name. """The function returns a string representing the output file name.
@@ -135,7 +147,11 @@ class APP(object):
Downloader(config).direct_download(url, file_name) Downloader(config).direct_download(url, file_name)
return tag, 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. """The function `download_patch_resources` downloads various resources req. for patching.
Parameters Parameters
@@ -143,22 +159,33 @@ class APP(object):
config : RevancedConfig config : RevancedConfig
The `config` parameter is an instance of the `RevancedConfig` class. It is used to provide The `config` parameter is an instance of the `RevancedConfig` class. It is used to provide
configuration settings for the resource download tasks. configuration settings for the resource download tasks.
resource_cache: dict[str, tuple[str, str]]
""" """
logger.info("Downloading resources for patching.") logger.info("Downloading resources for patching.")
# Create a list of resource download tasks
download_tasks = [ download_tasks = [
("cli", self.cli_dl, config, ".*jar"), ("cli", self.cli_dl, config, ".*jar"),
("patches", self.patches_dl, config, ".*rvp"), ("patches", self.patches_dl, config, ".*rvp"),
] ]
# Using a ThreadPoolExecutor for parallelism
with ThreadPoolExecutor(1) as executor: 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()) concurrent.futures.wait(futures.values())
# Retrieve results from completed tasks
for resource_name, future in futures.items(): for resource_name, future in futures.items():
try: try:
tag, file_name = future.result() tag, file_name = future.result()
@@ -166,8 +193,12 @@ class APP(object):
"file_name": file_name, "file_name": file_name,
"version": tag, "version": tag,
} }
resource_cache[download_tasks[["cli", "patches"].index(resource_name)][1].strip()] = (
tag,
file_name,
)
except BuilderError as e: except BuilderError as e:
msg = "Failed to download resource." msg = f"Failed to download {resource_name} resource."
raise PatchingFailedError(msg) from e raise PatchingFailedError(msg) from e
@staticmethod @staticmethod
+66
View File
@@ -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}"
+4
View File
@@ -1,6 +1,7 @@
"""Downloader Factory.""" """Downloader Factory."""
from src.config import RevancedConfig from src.config import RevancedConfig
from src.downloader.apkkeep import Apkeep
from src.downloader.apkmirror import ApkMirror from src.downloader.apkmirror import ApkMirror
from src.downloader.apkmonk import ApkMonk from src.downloader.apkmonk import ApkMonk
from src.downloader.apkpure import ApkPure from src.downloader.apkpure import ApkPure
@@ -12,6 +13,7 @@ from src.downloader.sources import (
APK_MIRROR_BASE_URL, APK_MIRROR_BASE_URL,
APK_MONK_BASE_URL, APK_MONK_BASE_URL,
APK_PURE_BASE_URL, APK_PURE_BASE_URL,
APKEEP,
APKS_SOS_BASE_URL, APKS_SOS_BASE_URL,
DRIVE_DOWNLOAD_BASE_URL, DRIVE_DOWNLOAD_BASE_URL,
GITHUB_BASE_URL, GITHUB_BASE_URL,
@@ -47,5 +49,7 @@ class DownloaderFactory(object):
return ApkMonk(config) return ApkMonk(config)
if apk_source.startswith(DRIVE_DOWNLOAD_BASE_URL): if apk_source.startswith(DRIVE_DOWNLOAD_BASE_URL):
return GoogleDrive(config) return GoogleDrive(config)
if apk_source.startswith(APKEEP):
return Apkeep(config)
msg = "No download factory found." msg = "No download factory found."
raise DownloadError(msg, url=apk_source) raise DownloadError(msg, url=apk_source)
+2 -2
View File
@@ -35,7 +35,7 @@ class Github(Downloader):
} }
if self.config.personal_access_token: if self.config.personal_access_token:
logger.debug("Using 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) response = requests.get(repo_url, headers=headers, timeout=request_timeout)
handle_request_response(response, repo_url) handle_request_response(response, repo_url)
if repo_name == "revanced-patches": if repo_name == "revanced-patches":
@@ -80,7 +80,7 @@ class Github(Downloader):
"Content-Type": "application/vnd.github.v3+json", "Content-Type": "application/vnd.github.v3+json",
} }
if config.personal_access_token: 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) response = requests.get(api_url, headers=headers, timeout=request_timeout)
handle_request_response(response, api_url) handle_request_response(response, api_url)
update_changelog(f"{github_repo_owner}/{github_repo_name}", response.json()) update_changelog(f"{github_repo_owner}/{github_repo_name}", response.json())
+1
View File
@@ -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" not_found_icon = "https://img.icons8.com/bubbles/500/android-os.png"
revanced_api = "https://api.revanced.app/v2/patches/latest" revanced_api = "https://api.revanced.app/v2/patches/latest"
APK_MONK_BASE_URL = "https://www.apkmonk.com" APK_MONK_BASE_URL = "https://www.apkmonk.com"
APKEEP = "apkeep"
APK_MONK_APK_URL = APK_MONK_BASE_URL + "/app/{}/" APK_MONK_APK_URL = APK_MONK_BASE_URL + "/app/{}/"
APK_MONK_ICON_URL = "https://cdn.apkmonk.com/logos/{}" APK_MONK_ICON_URL = "https://cdn.apkmonk.com/logos/{}"
DRIVE_BASE_URL = "https://drive.google.com" DRIVE_BASE_URL = "https://drive.google.com"
+1
View File
@@ -244,6 +244,7 @@ class Parser(object):
excluded = set(possible_archs) - set(app.archs_to_build) excluded = set(possible_archs) - set(app.archs_to_build)
for arch in excluded: for arch in excluded:
args.extend(("--rip-lib", arch)) args.extend(("--rip-lib", arch))
args.extend(("--purge",))
start = perf_counter() start = perf_counter()
logger.debug(f"Sending request to revanced cli for building with args java {args}") logger.debug(f"Sending request to revanced cli for building with args java {args}")
process = Popen(["java", *args], stdout=PIPE) process = Popen(["java", *args], stdout=PIPE)