Add support direct download from Google Play

This commit is contained in:
siritami 2026-06-25 20:40:21 +07:00
parent 043f4a95ec
commit 1400ea2b4c
10 changed files with 1436 additions and 27 deletions

View file

@ -4,6 +4,18 @@ inputs:
description: 'Install Bouncy Castle provider for BKS keystore'
required: false
default: 'false'
install_usque:
description: 'Install usque for Cloudflare WARP proxy'
required: false
default: 'false'
install_python_deps:
description: 'Install Python dependencies for Google Play downloader'
required: false
default: 'false'
python_requirements:
description: 'Path to Python requirements.txt file'
required: false
default: ''
runs:
using: "composite"
steps:
@ -12,6 +24,19 @@ runs:
with:
distribution: 'zulu'
java-version: '21'
- name: Setup usque
if: inputs.install_usque == 'true'
shell: bash
run: |
wget -q -O usque.zip https://github.com/Diniboy1123/usque/releases/download/v4.2.0/usque_4.2.0_linux_amd64.zip
unzip -o usque.zip -d usque
chmod +x usque/usque
sudo mv usque/usque /usr/local/bin/usque
rm -rf usque usque.zip
usque register --accept-tos
usque socks -b 127.0.0.1 -p 9099 &
sleep 5
curl -x socks5://127.0.0.1:9099 -s -o /dev/null -w "%{http_code}" https://www.google.com && echo "[+] usque SOCKS5 proxy is ready" || echo "[-] usque proxy failed"
- name: Start FlareSolverr
shell: bash
run: |
@ -24,6 +49,15 @@ runs:
fi
sleep 2
done
- name: Set up Python
if: inputs.install_python_deps == 'true'
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install Python dependencies
if: inputs.install_python_deps == 'true'
shell: bash
run: pip install -r ${{ inputs.python_requirements }}
- name: Install Bouncy Castle provider for BKS keystore
if: inputs.install_bouncy_castle == 'true'
shell: bash
@ -36,6 +70,3 @@ runs:
echo "security.provider.$((LAST_PROV+1))=org.bouncycastle.jce.provider.BouncyCastleProvider" \
| sudo tee -a "$JAVA_HOME/conf/security/java.security"
echo "[+] Bouncy Castle registered as provider #$((LAST_PROV+1))"
# - name: Setup Cloudflare Warp
# shell: bash
# run: bash src/etc/cf_warp.sh

View file

@ -106,6 +106,10 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Preparing to patch
uses: ./.github/actions/preparing
# with:
# install_python_deps: 'true'
# install_usque: 'true'
# python_requirements: 'src/build/helper/ggplay_dl/requirements.txt'
- name: Check github connection
id: check-gh-derv
run: bash src/etc/connection.sh

View file

@ -1008,6 +1008,8 @@ Web version:
</details>
[AuroraStore](https://gitlab.com/AuroraOSS/AuroraStore) - [GPL-3.0](https://gitlab.com/AuroraOSS/AuroraStore/-/blob/master/LICENSE)
[Markdown Badges](https://github.com/Ileriayo/markdown-badges) - [MIT](https://github.com/Ileriayo/markdown-badges/blob/master/LICENSE)
[pup](https://github.com/ericchiang/pup) - [MIT](https://github.com/ericchiang/pup/blob/master/LICENSE)

View file

@ -89,9 +89,9 @@ derevanced_dl(){
split_editor "photomath" "photomath"
patch "photomath" "derevanced" "morphe"
# Patch Strava:
get_patches_key "strava-derevanced"
get_apkpure "com.strava" "strava-arm64-v8a" "bundle"
patch "strava-arm64-v8a" "derevanced" "morphe"
#get_patches_key "strava-derevanced"
#get_apk_chplay "com.strava" "strava-arm64-v8a"
#patch "strava-arm64-v8a" "derevanced" "morphe"
}
7() {
derevanced_dl

View file

@ -0,0 +1,698 @@
#!/usr/bin/env python3
#
# Rewritten from: https://gitlab.com/AuroraOSS/AuroraStore
# Licensed under GNU General Public License v3.0
#
"""
Usage:
python3 ggplay_dl.py <package_name> <output_path> [--dispenser <url>]
"""
import argparse
import hashlib
import json
import os
import random
import sys
import time
import base64
import requests
from google.protobuf import descriptor as _descriptor
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ggplay_pb2 import (
AndroidCheckinRequest,
AndroidCheckinProto,
AndroidBuildProto,
AndroidCheckinResponse,
DeviceConfigurationProto,
UploadDeviceConfigRequest,
ResponseWrapper,
)
DISPENSER_URL = "https://auroraoss.com/api/auth"
URL_BASE = "https://android.clients.google.com"
URL_FDFE = f"{URL_BASE}/fdfe"
URL_CHECK_IN = f"{URL_BASE}/checkin"
URL_AUTH = f"{URL_BASE}/auth"
URL_UPLOAD_DEVICE_CONFIG = f"{URL_FDFE}/uploadDeviceConfig"
URL_DETAILS = f"{URL_FDFE}/details"
URL_PURCHASE = f"{URL_FDFE}/purchase"
URL_DELIVERY = f"{URL_FDFE}/delivery"
URL_ACQUIRE = f"{URL_FDFE}/acquire"
DEVICE_PROPERTIES = {
"UserReadableName": "Google Pixel 9a",
"Build.DEVICE": "tegu",
"Build.MODEL": "Pixel 9a",
"Build.MANUFACTURER": "Google",
"Build.BRAND": "google",
"Build.PRODUCT": "tegu",
"Build.HARDWARE": "tegu",
"Build.FINGERPRINT": "google/tegu/tegu:15/BD4A.250405.003/13238919:user/release-keys",
"Build.VERSION.SDK_INT": "35",
"Build.VERSION.RELEASE": "15",
"Build.ID": "BD4A.250405.003",
"Build.BOOTLOADER": "tegu-16.0-13238451",
"Build.RADIO": "g5300t-241101-241226-B-12850354",
"GSF.version": "251333035",
"Vending.version": "84582130",
"Vending.versionString": "45.8.21-31 [0] [PR] 747433787",
"Client": "android-google",
"SimOperator": "38",
"CellOperator": "310",
"Screen.Density": "420",
"Screen.Width": "1080",
"Screen.Height": "2424",
"Platforms": "arm64-v8a",
"Features": "android.hardware.bluetooth,android.hardware.camera,android.hardware.location,android.hardware.microphone,android.hardware.nfc,android.hardware.screen.landscape,android.hardware.screen.portrait,android.hardware.telephony,android.hardware.touchscreen,android.hardware.wifi",
"Locales": "en_US",
"SharedLibraries": "",
"TouchScreen": "3",
"Keyboard": "1",
"Navigation": "1",
"ScreenLayout": "2",
"HasHardKeyboard": "false",
"HasFiveWayNavigation": "false",
"GL.Version": "196610",
"GL.Extensions": "",
"TimeZone": "UTC-0",
"Roaming": "mobile-notroaming",
}
def build_device_config():
config = DeviceConfigurationProto()
config.touchScreen = int(DEVICE_PROPERTIES["TouchScreen"])
config.keyboard = int(DEVICE_PROPERTIES["Keyboard"])
config.navigation = int(DEVICE_PROPERTIES["Navigation"])
config.screenLayout = int(DEVICE_PROPERTIES["ScreenLayout"])
config.hasHardKeyboard = DEVICE_PROPERTIES["HasHardKeyboard"] == "true"
config.hasFiveWayNavigation = DEVICE_PROPERTIES["HasFiveWayNavigation"] == "true"
config.screenDensity = int(DEVICE_PROPERTIES["Screen.Density"])
config.glEsVersion = int(DEVICE_PROPERTIES["GL.Version"])
config.screenWidth = int(DEVICE_PROPERTIES["Screen.Width"])
config.screenHeight = int(DEVICE_PROPERTIES["Screen.Height"])
for platform in DEVICE_PROPERTIES["Platforms"].split(","):
config.nativePlatform.append(platform.strip())
for feature in DEVICE_PROPERTIES["Features"].split(","):
if feature.strip():
config.systemAvailableFeature.append(feature.strip())
for locale in DEVICE_PROPERTIES["Locales"].split(","):
if locale.strip():
config.systemSupportedLocale.append(locale.strip())
for lib in DEVICE_PROPERTIES["SharedLibraries"].split(","):
if lib.strip():
config.systemSharedLibrary.append(lib.strip())
for ext in DEVICE_PROPERTIES["GL.Extensions"].split(","):
if ext.strip():
config.glExtension.append(ext.strip())
return config
def build_checkin_request():
build = AndroidBuildProto()
build.device = DEVICE_PROPERTIES["Build.DEVICE"]
build.model = DEVICE_PROPERTIES["Build.MODEL"]
build.manufacturer = DEVICE_PROPERTIES["Build.MANUFACTURER"]
build.product = DEVICE_PROPERTIES["Build.PRODUCT"]
build.buildProduct = DEVICE_PROPERTIES["Build.PRODUCT"]
build.client = DEVICE_PROPERTIES["Client"]
build.id = DEVICE_PROPERTIES["Build.ID"]
build.bootloader = DEVICE_PROPERTIES["Build.BOOTLOADER"]
build.radio = DEVICE_PROPERTIES["Build.RADIO"]
build.timestamp = int(time.time() * 1000)
build.sdkVersion = int(DEVICE_PROPERTIES["Build.VERSION.SDK_INT"])
build.googleServices = int(DEVICE_PROPERTIES["GSF.version"])
checkin = AndroidCheckinProto()
checkin.build.CopyFrom(build)
checkin.lastCheckinMsec = 0
checkin.cellOperator = DEVICE_PROPERTIES["CellOperator"]
checkin.simOperator = DEVICE_PROPERTIES["SimOperator"]
checkin.roaming = DEVICE_PROPERTIES["Roaming"]
checkin.userNumber = 0
device_config = build_device_config()
request = AndroidCheckinRequest()
request.checkin.CopyFrom(checkin)
request.timeZone = DEVICE_PROPERTIES["TimeZone"]
request.locale = "en-US"
request.version = 3
request.deviceConfiguration.CopyFrom(device_config)
return request.SerializeToString()
def build_upload_device_config_request():
request = UploadDeviceConfigRequest()
request.deviceConfiguration.CopyFrom(build_device_config())
request.manufacturer = DEVICE_PROPERTIES["Build.MANUFACTURER"]
return request.SerializeToString()
def build_acquire_request(package_name, version_code, offer_type):
def encode_varint(value):
bits = value & 0x7f
value >>= 7
result = b""
while value:
result += bytes([0x80 | bits])
bits = value & 0x7f
value >>= 7
result += bytes([bits])
return result
def encode_field(field_number, wire_type, data):
tag = (field_number << 3) | wire_type
return encode_varint(tag) + data
def encode_varint_field(field_number, value):
return encode_field(field_number, 0, encode_varint(value))
def encode_string_field(field_number, value):
encoded = value.encode("utf-8")
return encode_field(field_number, 2, encode_varint(len(encoded)) + encoded)
def encode_length_delimited_field(field_number, sub_message_bytes):
return encode_field(field_number, 2, encode_varint(len(sub_message_bytes)) + sub_message_bytes)
payload = b""
payload += encode_varint_field(2, 1)
payload += encode_varint_field(3, 3)
payload += encode_string_field(4, package_name)
package = b""
package += encode_length_delimited_field(1, payload)
package += encode_varint_field(2, 1)
version = b""
version += encode_varint_field(1, version_code)
version += encode_varint_field(3, 0)
msg30 = b""
msg30 += encode_varint_field(1, 2)
msg30 += encode_varint_field(2, 0)
nonce_bytes = bytes(random.getrandbits(8) for _ in range(256))
nonce = base64.urlsafe_b64encode(nonce_bytes).rstrip(b"=").decode("ascii")
request = b""
request += encode_length_delimited_field(1, package)
request += encode_length_delimited_field(2, version)
request += encode_varint_field(15, 0)
request += encode_varint_field(16, offer_type)
request += encode_string_field(20, f"nonce={nonce}")
request += encode_varint_field(25, 2)
request += encode_length_delimited_field(30, msg30)
return request
FLARESOLVERR_URL = "http://localhost:8191/v1"
SOCKS5_PROXY = "socks5://127.0.0.1:9099"
def _detect_socks5():
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2)
s.connect(("127.0.0.1", 9099))
s.close()
return SOCKS5_PROXY
except Exception:
return None
class GooglePlaySession:
def __init__(self, dispenser_url=None):
self.dispenser_url = dispenser_url or DISPENSER_URL
self.flaresolverr_url = self._detect_flaresolverr()
self.socks5_proxy = _detect_socks5()
self.email = None
self.aas_token = None
self.gsf_id = None
self.auth_token = None
self.device_checkin_consistency_token = None
self.device_config_token = None
self.session = requests.Session()
if self.socks5_proxy:
import socks
self.session.proxies = {
"http": self.socks5_proxy,
"https": self.socks5_proxy,
}
print(f"[+] Using SOCKS5 proxy: {self.socks5_proxy}", file=sys.stderr)
self.session.headers.update({
"User-Agent": "com.aurora.store-4.8.3-75",
})
def _get_auth_headers(self):
headers = {
"app": "com.google.android.gms",
"User-Agent": f"GoogleAuth/1.4 ({DEVICE_PROPERTIES['Build.DEVICE']} {DEVICE_PROPERTIES['Build.ID']})",
}
if self.gsf_id:
headers["device"] = self.gsf_id
return headers
def _get_default_headers(self):
ua = (
f"Android-Finsky/{DEVICE_PROPERTIES['Vending.versionString']} "
f"(api=3,versionCode={DEVICE_PROPERTIES['Vending.version']},"
f"sdk={DEVICE_PROPERTIES['Build.VERSION.SDK_INT']},"
f"device={DEVICE_PROPERTIES['Build.DEVICE']},"
f"hardware={DEVICE_PROPERTIES['Build.HARDWARE']},"
f"product={DEVICE_PROPERTIES['Build.PRODUCT']},"
f"platformVersionRelease={DEVICE_PROPERTIES['Build.VERSION.RELEASE']},"
f"model={DEVICE_PROPERTIES['Build.MODEL']},"
f"buildId={DEVICE_PROPERTIES['Build.ID']},"
f"isWideScreen=0,supportedAbis={DEVICE_PROPERTIES['Platforms']})"
)
headers = {
"Authorization": f"Bearer {self.auth_token}",
"User-Agent": ua,
"X-DFE-Device-Id": self.gsf_id,
"Accept-Language": "en-US",
"X-DFE-Encoded-Targets": (
"CAESN/qigQYC2AMBFfUbyA7SM5Ij/CvfBoIDgxHqGP8R3xzIBvoQtBKFDZ4HAY4FrwSVMasHBOO2Q8akgYRAQECAQO7AQEpKZ0CnwECAwRrAQYBr9PPAoK7sQMBAQMCBAkIDAgBAwEDBAICBAUZEgMEBAMLAQEBBQEBAcYBARYED+cBfS8CHQEKkAEMMxcBIQoUDwYHIjd3DQ4MFk0JWGYZEREYAQOLAYEBFDMIEYMBAgICAgICOxkCD18LGQKEAcgDBIQBAgGLARkYCy8oBTJlBCUocxQn0QUBDkkGxgNZQq0BZSbeAmIDgAEBOgGtAaMCDAOQAZ4BBIEBKUtQUYYBQscDDxPSARA1oAEHAWmnAsMB2wFyywGLAxol+wImlwOOA80CtwN26A0WjwJVbQEJPAH+BRDeAfkHK/ABASEBCSAaHQemAzkaRiu2Ad8BdXeiAwEBGBUBBN4LEIABK4gB2AFLfwECAdoENq0CkQGMBsIBiQEtiwGgA1zyAUQ4uwS8AwhsvgPyAcEDF27vApsBHaICGhl3GSKxAR8MC6cBAgItmQYG9QIeywLvAeYBDArLAh8HASI4ELICDVmVBgsY/gHWARtcAsMBpALiAdsBA7QBpAJmIArpByn0AyAKBwHTARIHAX8D+AMBcRIBBbEDmwUBMacCHAciNp0BAQF0OgQLJDuSAh54kwFSP0eeAQQ4M5EBQgMEmwFXywFo0gFyWwMcapQBBugBPUW2AVgBKmy3AR6PAbMBGQxrUJECvQR+8gFoWDsYgQNwRSczBRXQAgtRswEW0ALMAREYAUEBIG6yATYCRE8OxgER8gMBvQEDRkwLc8MBTwHZAUOnAXiiBakDIbYBNNcCIUmuArIBSakBrgFHKs0EgwV/G3AD0wE6LgECtQJ4xQFwFbUCjQPkBS6vAQqEAUZF3QIM9wEhCoYCQhXsBCyZArQDugIziALWAdIBlQHwBdUErQE6qQaSA4EEIvYBHir9AQVLmgMCApsCKAwHuwgrENsBAjNYswEVmgIt7QJnN4wDEnta+wGfAcUBxgEtEFXQAQWdAUAeBcwBAQM7rAEJATJ0LENrdh73A6UBhAE+qwEeASxLZUMhDREuH0CGARbd7K0GlQo"
),
"X-DFE-Phenotype": (
"H4sIAAAAAAAAB3OO3KjMAAA0KRNuWXukBkBQkAJ2MhgAZb5u2GCwQZbCH_EJ77QHmgvtDtbv-Z9_H63zXXU0NVPB1odlyGy7751Q3CitlPDvFd8lxhz3tpNmz7P92CFw73zdHU2Ie0Ad2kmR8lxhiErTFLt3RPGfJQHSDy7Clw0bg8kqf2owLokN4SecJTLoSwBnzQSd652_MOf2d1vKBNVedzg4ciPoLz2mQ8efGAgYeLou-l-PXn_7Sa1MfhHuySxt-4esulEDp8Sbq54CPPKjpANW-lkU2IZ0F92LBI-ukCKSptqeq1eXU96LD9nZfhKHdtjSwJqUm_2r6pMHOxk01saVanmNopjX3YxQafC4iC6T55aRbC8nTI98AF_kItIQAJb5EQxnKTO7TZDWnr01HVPxelb9A2OWX6poidMWl16K54kcu_jhXw-JSBQkVcD_fPsLSZu6joIBAAA"
),
"X-DFE-Client-Id": "am-android-google",
"X-DFE-Network-Type": "4",
"X-DFE-Content-Filters": "",
"X-Limit-Ad-Tracking-Enabled": "false",
"X-Ad-Id": "",
"X-DFE-UserLanguages": "en_US",
"X-DFE-Request-Params": "timeoutMs=4000",
}
if self.device_checkin_consistency_token:
headers["X-DFE-Device-Checkin-Consistency-Token"] = self.device_checkin_consistency_token
if self.device_config_token:
headers["X-DFE-Device-Config-Token"] = self.device_config_token
return headers
def _get_auth_params(self):
params = {
"androidId": self.gsf_id,
"sdk_version": DEVICE_PROPERTIES["Build.VERSION.SDK_INT"],
"Email": self.email,
"google_play_services_version": "251333035",
"device_country": "us",
"lang": "en",
"callerSig": "38918a453d07199354f8b19af05ec6562ced5788",
}
return params
def _get_token_params(self):
params = self._get_auth_params()
params.update({
"app": "com.android.vending",
"client_sig": "38918a453d07199354f8b19af05ec6562ced5788",
"callerPkg": "com.google.android.gms",
"Token": self.aas_token,
"oauth2_foreground": "1",
"token_request_options": "CAA4AVAB",
"check_email": "1",
"system_partition": "1",
"droidguard_results": "null",
"service": "oauth2:https://www.googleapis.com/auth/googleplay",
})
return params
def _detect_flaresolverr(self):
try:
resp = requests.get(FLARESOLVERR_URL.replace("/v1", "/"), timeout=3)
if resp.status_code == 200:
print(f"[+] FlareSolverr detected at {FLARESOLVERR_URL}", file=sys.stderr)
return FLARESOLVERR_URL
except Exception:
pass
return None
def _fs_get_cookies(self):
for url in [self.dispenser_url, "https://auroraoss.com/"]:
try:
resp = requests.post(
self.flaresolverr_url,
json={"cmd": "request.get", "url": url, "maxTimeout": 60000, "returnOnlyCookies": True},
timeout=90,
)
resp.raise_for_status()
data = resp.json()
if data.get("status") == "ok":
cookies = {}
for c in data.get("solution", {}).get("cookies", []):
cookies[c["name"]] = c["value"]
ua = data.get("solution", {}).get("userAgent", "")
if cookies:
return cookies, ua
except Exception as e:
print(f"[!] FlareSolverr failed for {url}: {e}", file=sys.stderr)
return {}, ""
def _post_dispenser(self, url, json_data):
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "en-US,en;q=0.9",
"Origin": "https://auroraoss.com",
"Referer": "https://auroraoss.com/",
}
proxies = {}
if self.socks5_proxy:
proxies = {"https": self.socks5_proxy, "http": self.socks5_proxy}
resp = self.session.post(url, json=json_data, headers=headers, proxies=proxies, timeout=30)
if resp.status_code == 403:
if self.flaresolverr_url:
print(f"[!] Direct request blocked (403), trying FlareSolverr...", file=sys.stderr)
cookies, fs_ua = self._fs_get_cookies()
if cookies:
print(f"[+] Got {len(cookies)} cookies from FlareSolverr", file=sys.stderr)
resp = self.session.post(url, json=json_data, headers=headers, cookies=cookies, proxies=proxies, timeout=30)
resp.raise_for_status()
return resp.json()
def step1_get_dispenser_credentials(self):
print("[+] Step 1: Getting dispenser credentials...", file=sys.stderr)
data = self._post_dispenser(self.dispenser_url, DEVICE_PROPERTIES)
self.email = data.get("email")
self.auth_token = data.get("authToken") or data.get("auth")
if not self.email or not self.auth_token:
raise Exception(f"Dispenser returned invalid data: {data}")
print(f"[+] Got credentials for: {self.email}", file=sys.stderr)
def step2_checkin(self):
print("[+] Step 2: Performing checkin...", file=sys.stderr)
request_bytes = build_checkin_request()
headers = self._get_auth_headers()
headers["Content-Type"] = "application/x-protobuffer"
headers["Host"] = "android.clients.google.com"
resp = self.session.post(URL_CHECK_IN, data=request_bytes, headers=headers, timeout=30)
resp.raise_for_status()
response = AndroidCheckinResponse()
response.ParseFromString(resp.content)
self.gsf_id = format(response.androidId, "x")
self.device_checkin_consistency_token = response.deviceCheckinConsistencyToken
print(f"[+] GSF ID: {self.gsf_id}", file=sys.stderr)
def step3_upload_device_config(self):
print("[+] Step 3: Uploading device config...", file=sys.stderr)
request_bytes = build_upload_device_config_request()
headers = self._get_default_headers()
headers["Content-Type"] = "application/x-protobuffer"
resp = self.session.post(URL_UPLOAD_DEVICE_CONFIG, data=request_bytes, headers=headers, timeout=30)
resp.raise_for_status()
wrapper = ResponseWrapper()
wrapper.ParseFromString(resp.content)
if wrapper.HasField("payload"):
payload = wrapper.payload
if payload.HasField("uploadDeviceConfigResponse"):
self.device_config_token = payload.uploadDeviceConfigResponse.uploadDeviceConfigToken
print(f"[+] Device config token: {self.device_config_token[:30]}..." if self.device_config_token else "[!] No device config token", file=sys.stderr)
def step4_generate_token(self):
print("[+] Step 4: Generating OAuth token...", file=sys.stderr)
headers = self._get_auth_headers()
headers["app"] = "com.google.android.gms"
params = self._get_token_params()
resp = self.session.post(URL_AUTH, data=params, headers=headers, timeout=30)
resp.raise_for_status()
response_text = resp.text
auth_token = None
for line in response_text.split("\n"):
if line.startswith("Auth="):
auth_token = line[5:].strip()
break
if not auth_token:
for part in response_text.split("&"):
if part.startswith("Auth="):
auth_token = part[5:].strip()
break
if not auth_token:
raise Exception(f"Failed to generate OAuth token. Response: {response_text[:500]}")
self.auth_token = auth_token
print(f"[+] OAuth token obtained", file=sys.stderr)
def step5_get_app_details(self, package_name):
print(f"[+] Step 5: Getting details for {package_name}...", file=sys.stderr)
headers = self._get_default_headers()
params = {"doc": package_name}
resp = self.session.get(URL_DETAILS, params=params, headers=headers, timeout=30)
resp.raise_for_status()
wrapper = ResponseWrapper()
wrapper.ParseFromString(resp.content)
if not wrapper.HasField("payload") or not wrapper.payload.HasField("detailsResponse"):
raise Exception("No detailsResponse in payload")
details = wrapper.payload.detailsResponse
if not details.HasField("item"):
raise Exception("No item in detailsResponse")
item = details.item
version_code = 0
offer_type = 1
if item.HasField("details") and item.details.HasField("appDetails"):
version_code = item.details.appDetails.versionCode
if len(item.offer) > 0:
offer_type = item.offer[0].offerType
if version_code == 0:
raise Exception(f"Could not find versionCode for {package_name}")
print(f"[+] versionCode={version_code}, offerType={offer_type}", file=sys.stderr)
return version_code, offer_type
def step6_purchase(self, package_name, version_code, offer_type):
print(f"[+] Step 6: Purchasing {package_name} v{version_code}...", file=sys.stderr)
try:
acquire_bytes = build_acquire_request(package_name, version_code, offer_type)
headers = self._get_default_headers()
headers["Content-Type"] = "application/x-protobuf"
self.session.post(URL_ACQUIRE, data=acquire_bytes, headers=headers, timeout=30)
except Exception:
print("[!] Acquire failed (non-critical), continuing...", file=sys.stderr)
headers = self._get_default_headers()
params = {
"ot": str(offer_type),
"doc": package_name,
"vc": str(version_code),
}
resp = self.session.post(URL_PURCHASE, params=params, headers=headers, timeout=30)
resp.raise_for_status()
wrapper = ResponseWrapper()
wrapper.ParseFromString(resp.content)
delivery_token = ""
if wrapper.HasField("payload") and wrapper.payload.HasField("buyResponse"):
delivery_token = wrapper.payload.buyResponse.encodedDeliveryToken
if not delivery_token:
raise Exception("Failed to get delivery token")
print(f"[+] Delivery token obtained", file=sys.stderr)
headers = self._get_default_headers()
params = {
"ot": str(offer_type),
"doc": package_name,
"vc": str(version_code),
"dtok": delivery_token,
}
resp = self.session.get(URL_DELIVERY, params=params, headers=headers, timeout=30)
resp.raise_for_status()
wrapper = ResponseWrapper()
wrapper.ParseFromString(resp.content)
if not wrapper.HasField("payload") or not wrapper.payload.HasField("deliveryResponse"):
raise Exception("No deliveryResponse in payload")
delivery = wrapper.payload.deliveryResponse
if delivery.status != 1:
error_map = {2: "AppNotSupported", 3: "AppNotPurchased", 7: "AppRemoved", 9: "AppNotSupported"}
error_name = error_map.get(delivery.status, f"Unknown({delivery.status})")
raise Exception(f"Delivery failed with status {delivery.status}: {error_name}")
if not delivery.HasField("appDeliveryData"):
raise Exception("No appDeliveryData in delivery response")
app_data = delivery.appDeliveryData
download_url = app_data.downloadUrl
download_size = app_data.downloadSize
sha256 = None
if app_data.sha256:
try:
sha256 = base64.urlsafe_b64decode(app_data.sha256 + "==").hex()
except Exception:
sha256 = app_data.sha256
splits = []
for split in app_data.splitDeliveryData:
splits.append({
"name": split.name,
"downloadUrl": split.downloadUrl,
"downloadSize": split.downloadSize,
})
if splits:
print(f"[+] Download URL obtained (size={download_size}, splits={len(splits)})", file=sys.stderr)
else:
print(f"[+] Download URL obtained (size={download_size})", file=sys.stderr)
return download_url, download_size, sha256, splits
def build_session(self):
self.step1_get_dispenser_credentials()
self.step2_checkin()
def _download_file(self, url, dest):
resp = self.session.get(url, stream=True, timeout=120)
resp.raise_for_status()
sha256_hash = hashlib.sha256()
total = 0
with open(dest, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
sha256_hash.update(chunk)
total += len(chunk)
return total, sha256_hash.hexdigest()
def download_apk(self, package_name, output_path):
self.build_session()
version_code, offer_type = self.step5_get_app_details(package_name)
download_url, download_size, expected_sha256, splits = self.step6_purchase(
package_name, version_code, offer_type
)
is_split = len(splits) > 0
if is_split:
import zipfile
import tempfile
print(f"[+] Split APK detected ({len(splits)} splits)", file=sys.stderr)
tmp_dir = tempfile.mkdtemp()
base_apk = os.path.join(tmp_dir, "base.apk")
print(f"[+] Downloading base APK...", file=sys.stderr)
total, _ = self._download_file(download_url, base_apk)
print(f"[+] Downloaded base APK ({total} bytes)", file=sys.stderr)
for i, split in enumerate(splits):
split_name = split["name"]
split_dest = os.path.join(tmp_dir, split_name)
print(f"[+] Downloading split {i+1}/{len(splits)}: {split_name}...", file=sys.stderr)
total, _ = self._download_file(split["downloadUrl"], split_dest)
print(f"[+] Downloaded {split_name} ({total} bytes)", file=sys.stderr)
print(f"[+] Packaging splits into {output_path}...", file=sys.stderr)
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
zf.write(base_apk, "base.apk")
for split in splits:
zf.write(os.path.join(tmp_dir, split["name"]), split["name"])
total_size = os.path.getsize(output_path)
for f in os.listdir(tmp_dir):
os.remove(os.path.join(tmp_dir, f))
os.rmdir(tmp_dir)
print(f"[+] Packaged {len(splits)+1} APKs into {output_path} ({total_size} bytes)", file=sys.stderr)
result = {
"success": True,
"package": package_name,
"versionCode": version_code,
"offerType": offer_type,
"size": total_size,
"sha256": "",
"isSplit": True,
"outputPath": output_path,
}
print(json.dumps(result))
return output_path
print(f"[+] Downloading APK to {output_path}...", file=sys.stderr)
resp = self.session.get(download_url, stream=True, timeout=120)
resp.raise_for_status()
sha256_hash = hashlib.sha256()
total = 0
with open(output_path, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
sha256_hash.update(chunk)
total += len(chunk)
actual_sha256 = sha256_hash.hexdigest()
if expected_sha256 and actual_sha256 != expected_sha256:
print(f"[!] SHA256 mismatch: expected {expected_sha256}, got {actual_sha256}", file=sys.stderr)
os.remove(output_path)
raise Exception("SHA256 verification failed")
print(f"[+] Downloaded {total} bytes to {output_path}", file=sys.stderr)
if expected_sha256:
print(f"[+] SHA256 verified: {actual_sha256}", file=sys.stderr)
result = {
"success": True,
"package": package_name,
"versionCode": version_code,
"offerType": offer_type,
"downloadUrl": download_url,
"size": total,
"sha256": actual_sha256,
"isSplit": False,
"outputPath": output_path,
}
print(json.dumps(result))
return output_path
def main():
parser = argparse.ArgumentParser(description="Download APK from Google Play Store")
parser.add_argument("package_name", help="Android package name (e.g., com.android.chrome)")
parser.add_argument("output_path", help="Output file path for the APK")
parser.add_argument("--dispenser", help="Dispenser URL (default: AuroraStore dispenser)")
args = parser.parse_args()
try:
session = GooglePlaySession(dispenser_url=args.dispenser)
session.download_apk(args.package_name, args.output_path)
except Exception as e:
print(f"[-] Error: {e}", file=sys.stderr)
result = {"success": False, "error": str(e)}
print(json.dumps(result))
sys.exit(1)
if __name__ == "__main__":
main()

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,3 @@
requests
protobuf
PySocks

View file

@ -299,7 +299,7 @@ get_apk() {
local base_url="https://www.apkmirror.com"
local html=""
local apps_json="./src/build/apps.json"
local apps_json="./src/build/helper/apps.json"
local list_url example_url
list_url=$(jq -r --arg pkg "$pkg_name" '.apkmirror[$pkg].list_url // empty' "$apps_json")
example_url=$(jq -r --arg pkg "$pkg_name" '.apkmirror[$pkg].example_url // empty' "$apps_json")
@ -508,7 +508,7 @@ get_apkpure() {
local pkg_name=$1 apk_name=$2 pkg_type=${3:-apk}
local html=""
local apps_json="./src/build/apps.json"
local apps_json="./src/build/helper/apps.json"
local base_download_url
base_download_url=$(jq -r --arg pkg "$pkg_name" '.apkpure[$pkg].download_url // empty' "$apps_json")
@ -590,6 +590,63 @@ get_apkpure() {
fi
}
# Download APK from Google Play Store via AuroraStore anonymous auth
get_apk_chplay() {
local pkg_name=$1 apk_name=$2
local pkg_type=${3:-apk} dispenser_url=${4:-}
local helper_script="./src/build/helper/ggplay_dl/ggplay_dl.py"
green_log "[+] Downloading $apk_name from Google Play (Ch Play) [$pkg_name]"
local dispenser_arg=""
[[ -n "$dispenser_url" ]] && dispenser_arg="--dispenser $dispenser_url"
local result
result=$(python "$helper_script" "$pkg_name" "./download/$apk_name.apk" $dispenser_arg 2>&1)
local exit_code=$?
local logs
logs=$(echo "$result" | grep -v '^{' | grep -v '^}')
local json_result
json_result=$(echo "$result" | grep '^{')
if [[ $exit_code -ne 0 ]]; then
red_log "[-] Failed to download $apk_name from Google Play"
[[ -n "$logs" ]] && echo "$logs" >&2
return 1
fi
local success
success=$(echo "$json_result" | jq -r '.success // false' 2>/dev/null)
if [[ "$success" != "true" ]]; then
local error_msg
error_msg=$(echo "$json_result" | jq -r '.error // "Unknown error"' 2>/dev/null)
red_log "[-] Download failed: $error_msg"
return 1
fi
local dl_size
dl_size=$(echo "$json_result" | jq -r '.size // 0' 2>/dev/null)
local is_split
is_split=$(echo "$json_result" | jq -r '.isSplit // false' 2>/dev/null)
green_log "[+] Successfully downloaded $apk_name (${dl_size} bytes)"
if [[ "$is_split" == "true" ]]; then
if [[ "$pkg_type" == "bundle_extract" ]]; then
green_log "[+] Extracting split APKs"
unzip "./download/$apk_name.apk" -d "./download/$apk_name" > /dev/null 2>&1
else
green_log "[+] Merging split APKs to standalone apk"
java -jar $APKEditor m -i "./download/$apk_name.apk" -o "./download/$apk_name.apk.merged" > /dev/null 2>&1
if [[ -f "./download/$apk_name.apk.merged" ]]; then
mv "./download/$apk_name.apk.merged" "./download/$apk_name.apk"
fi
fi
fi
}
# Download files from Telegram channel/group
# Required secret in github setting TDL_BACKUP base64 backup file from https://docs.iyear.me/tdl/more/cli/tdl_backup/
# You must login your telegram (recommend use clone account) before backup https://docs.iyear.me/tdl/getting-started/quick-start/#login

View file

@ -1,19 +0,0 @@
#!/bin/bash
# Setup Cloudflare warp for bypass cloudflare anti ddos APKMirror:
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | sudo gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflare-client.list
sudo apt-get update && sudo apt-get install cloudflare-warp -y
sudo systemctl enable --now warp-svc.service
warp-cli --accept-tos registration new
warp-cli --accept-tos mode warp
warp-cli --accept-tos connect
sleep 3
output=$(wget -qO- https://www.cloudflare.com/cdn-cgi/trace | awk -F'=' '/ip|colo|warp/ {printf "%s: %s\n", $1, $2}')
echo "$output"
warp=$(echo "$output" | awk -F':' '/warp/ {print $2}')
if [ "$warp" = " on" ]; then
echo -e "\e[32m[+] Successful install Cloudflare Warp\e[0m"
else
echo -e "\e[31m[-] Can't install Cloudflare Warp\e[0m"
fi