Download APKs from Google Play Store. Can merge split APKs (App Bundles) into single installable APKs.
- Download any free app from Google Play (paid apps are detected early and rejected with the price shown)
- Automatic split APK merging using APKEditor, with Play Asset Delivery support (OBB/asset pack fusing)
- 23 device profiles with automatic rotation for reliable downloads
- Architecture support: ARM64 (modern phones) and ARMv7 (older phones)
- Modern dark web UI with real-time download progress and streaming activity log
- WebUSB ADB support — install APKs directly to a connected Android device from the browser (Chrome/Edge)
- Split APK direct install via ADB sessions — no merging needed, original signatures preserved
- Persistent download counter displayed in the UI
- ADB install from CLI — download and install directly to device via
adb - Backup & restore — export installed app list from device, batch restore later
- CLI tool with JSON output for scripting and automation
- Apps without splits preserve original signature
- Merged APKs are signed with debug keystore
- Auto-generated SEO app pages — each downloaded app gets its own detail page with description and icon, plus a browsable catalog at
/apps. Disable withDISABLE_APP_PAGES=1
- Python 3.8+
- Java 17+ (for APKEditor)
- apksigner (for APK signing)
git clone <repo-url> gplay-apk-downloader
cd gplay-apk-downloader
./setup.shsudo apt-get update
sudo apt-get install -y openjdk-17-jre-headless apksigner python3 python3-venv python3-pip curlThe setup script will:
- Check for required system dependencies
- Create Python virtual environment (
.venv/) - Install Python packages from
requirements.txt - Download APKEditor.jar
- Create debug keystore for signing (
~/.android/debug.keystore) - Generate wrapper scripts (
gplay,start-server.sh)
./start-server.sh # Production mode (gunicorn + gevent)
./start-server.sh dev # Development mode (Flask debug server)
PORT=8080 ./start-server.sh # Use a custom portIf port 5000 is already in use, the script will prompt for an alternate port interactively. In non-interactive mode (e.g., systemd, cron), set the PORT env var instead.
Production mode (default):
- Gunicorn with gevent async workers (capped at 8 workers)
- Handles concurrent users with connection pooling
- Disk-based temp storage (2GB limit, 10min TTL)
- Configurable CORS and log level via environment variables
- Runs in background via
nohup
Development mode (dev):
- Single-threaded Flask server with debug output
- Auto-reload on code changes
- Runs in foreground
Features:
- Logging: Outputs to
server.log(rotated after 12 hours, old logs kept 7 days) - Health check:
GET /healthfor monitoring - Concurrency limits: Max 10 concurrent downloads, 3 concurrent merges
Open http://localhost:5000 in your browser.
- Enter package name (e.g.,
com.google.android.youtube) or use the search box - Select architecture:
- ARM64 - Modern phones (2016+)
- ARMv7 - Older phones
- Choose merge option:
- Checked: Single installable APK (re-signed with debug key)
- Unchecked: ZIP with base + split APKs (original signatures)
- Click Download - real-time progress shows token attempts, download, merge, and signing steps. Total size shown includes base APK, config splits, and any asset pack data (e.g. OBB). Asset pack splits are labeled in the activity log.
- Activity Log - collapsible terminal-style panel streams all operations in real time (auto-opens on download). Shows fused modules patching for apps with Play Asset Delivery.
- Install to Device (optional) - connect an Android device via USB to install APKs directly from the browser
Signature Warning: Merged APKs are re-signed with a debug key and will NOT receive automatic updates from Google Play. Apps without splits keep their original signature.
Not recommended for apps from Meta, Uber, WhatsApp, any Google app requiring an account, or any banking app. Re-signing removes the original Play Store signature, which may result in account bans or restrictions. For these apps, use
./gplay download <pkg> -ito install splits directly via ADB (original signatures preserved), or download without-mto keep the original split APKs. Use your own judgement to decide if an app warrants not merging. We do not take responsibility for any account bans or restrictions resulting from the use of re-signed APKs.
Connect an Android device via USB to install APKs directly without downloading to your computer first.
Requirements:
- Chrome or Edge browser (WebUSB is not supported in Firefox/Safari)
- HTTPS or localhost
- USB debugging enabled on the Android device (Settings → Developer Options → USB Debugging)
How it works:
- Plug in your Android device via USB
- Click Connect Device in the web UI
- Select your device in the browser's USB picker and tap Allow on the phone
- After fetching download info, click Install to Device
For apps with split APKs, the tool uses Android's session-based install (pm install-create/pm install-write/pm install-commit) to install all splits directly — no merging needed, original signatures preserved. The merge checkbox is automatically disabled when a device is connected.
tail -f server.log # View logs
kill $(lsof -ti:5000) # Stop serverAuthenticate to get an anonymous token:
./gplay auth
./gplay auth -d https://custom-dispenser.example.com # Use custom dispenserToken is saved to ~/.gplay-auth.json and shared between CLI and web server.
./gplay search "youtube"
./gplay search "file manager" -l 20 # Show up to 20 results
./gplay search "spotify" --json # JSON output for scripting./gplay info com.google.android.youtube
./gplay info com.whatsapp --json./gplay check-version com.whatsapp
./gplay check-version com.whatsapp --jsonUses protobuf API with HTML fallback to get the version string and version code.
./gplay list-splits com.whatsapp
./gplay list-splits com.whatsapp --jsonShows all available split APKs including language splits.
# Basic download (ARM64 is default)
./gplay download com.google.android.youtube
# Download for ARM64 (modern phones)
./gplay download com.google.android.youtube -a arm64
# Download for ARMv7 (older phones)
./gplay download com.google.android.youtube -a armv7
# Download and merge splits into single APK
./gplay download com.google.android.youtube -m
# Merge for ARMv7
./gplay download com.google.android.youtube -m -a armv7
# Download for both architectures at once
./gplay download com.google.android.youtube --both-arch
# Download all available language splits (individual split APKs)
./gplay download com.google.android.youtube --all-locales
# Download all available language splits merged into a single APK
./gplay download com.google.android.youtube --all-locales -m
# Download and install to connected device via ADB
./gplay download com.google.android.youtube -i
# Download, merge, and install
./gplay download com.google.android.youtube -m -i
# Custom output directory
./gplay download com.google.android.youtube -m -o ~/apks/
# Download specific version code
./gplay download com.google.android.youtube -v 1234567ADB Install: The
-iflag installs directly to a connected device. For split APKs without-m, it usesadb install-multiple(session install, preserves original signatures). With-m, it installs the merged APK.Play Asset Delivery: Apps with asset pack splits (e.g.
obbassets) are automatically detected during merge. The manifest is patched withcom.android.dynamic.apk.fused.modulesso the app can find its fused asset data at runtime. The merged APK is zipaligned for Android 11+ compatibility.
| Option | Description |
|---|---|
--json |
Output results as JSON (for scripting) |
| Option | Description |
|---|---|
-d, --dispenser |
Custom dispenser URL |
| Option | Default | Description |
|---|---|---|
query |
(required) | Search query |
-l, --limit |
10 | Max results |
--json |
JSON output |
| Option | Description |
|---|---|
package |
Package name (required) |
--json |
JSON output |
| Option | Default | Description |
|---|---|---|
package |
(required) | Package name |
-a, --arch |
arm64 |
Architecture: arm64 or armv7 |
-m, --merge |
off | Merge split APKs into single installable APK |
-o, --output |
. |
Output directory |
-v, --version |
latest | Download specific version code |
-i, --install |
off | Install to connected ADB device after download |
--both-arch |
Download for both ARM64 and ARMv7 | |
--all-locales |
Download all available language splits (reads app manifest) |
# Get version as JSON and parse with jq
./gplay check-version com.whatsapp --json | jq '.version'
# Search and get package names
./gplay search "banking" --json | jq -r '.results[].package'
# Download multiple apps
for pkg in com.whatsapp com.spotify.music; do
./gplay download "$pkg" -m
doneBack up the list of user-installed apps from a connected ADB device. Checks each package against Google Play for availability.
./gplay backup # Save to app-backup-DATE.json
./gplay backup -o my-apps.json # Custom output file
./gplay backup -o - # Print to stdoutRestore apps from a backup JSON file. Downloads each available package.
# Download all apps from backup
./gplay restore my-apps.json
# Download and install directly to device
./gplay restore my-apps.json -i
# Restore merged APKs for ARMv7
./gplay restore my-apps.json -m -a armv7
# Restore to specific directory
./gplay restore my-apps.json -o ~/apks/| Option | Default | Description |
|---|---|---|
-o, --output |
app-backup-DATE.json |
Output file (- for stdout) |
| Option | Default | Description |
|---|---|---|
file |
(required) | Backup JSON file |
-o, --output |
. |
Output directory for downloaded APKs |
-a, --arch |
arm64 |
Architecture: arm64 or armv7 |
-m, --merge |
off | Merge split APKs into single APK |
-i, --install |
off | Install each app to connected ADB device |
The web UI also supports backup and restore when an ADB device is connected:
- Backup: Click Backup App List to read installed packages from the device and check Play Store availability
- Export: Save the list as a JSON file for later use
- Import: Load a previously exported backup JSON
- Restore: Install selected apps back to the device, or download them if no device is connected
The web server exposes these REST and SSE endpoints:
| Endpoint | Method | Description |
|---|---|---|
/ |
GET | Web UI |
/apps |
GET | Browse catalog of downloaded apps (auto-generated) |
/app/<package> |
GET | App detail page with download button (auto-generated) |
/health |
GET | Health check (system status, disk usage, worker info) |
/api/stats |
GET | Download counter (total APKs downloaded) |
/api/stats/increment |
POST | Increment download counter (rate-limited, for client-side installs) |
| Endpoint | Method | Description |
|---|---|---|
/api/auth |
POST | Get/validate cached auth token |
/api/auth/stream |
GET | SSE: acquire token with profile rotation |
/api/auth/status |
GET | Check if authenticated |
| Endpoint | Method | Description |
|---|---|---|
/api/search?q=<query> |
GET | Search apps (cached 6 hours) |
/api/info/<package> |
GET | Get app details |
| Endpoint | Method | Description |
|---|---|---|
/api/download-info/<package> |
GET | Get download URLs (requires auth header) |
/api/download-info-stream/<package> |
GET | SSE: get download info with auto token rotation |
/api/download-merged-stream/<package> |
GET | SSE: download + merge + sign with progress |
/api/download-merged/<package> |
GET | Non-streaming fallback for download + merge |
/api/download-temp/<id> |
GET | Download temporary merged APK (auto-cleanup) |
/download/<package> |
GET | Proxy download for base APK |
/download/<package>/<split_index> |
GET | Proxy download for specific split |
| Parameter | Values | Default | Used By |
|---|---|---|---|
arch |
arm64-v8a, armeabi-v7a |
arm64-v8a |
download-info-stream, download-merged-stream |
q |
search string | (required) | search |
# Search
curl "http://localhost:5000/api/search?q=youtube"
# Get info
curl "http://localhost:5000/api/info/com.google.android.youtube"
# Download merged APK for ARM64 (streams progress via SSE)
curl "http://localhost:5000/api/download-merged-stream/com.google.android.youtube?arch=arm64-v8a"
# Download merged APK for ARMv7
curl "http://localhost:5000/api/download-merged-stream/com.google.android.youtube?arch=armeabi-v7a"
# Health check
curl "http://localhost:5000/health"sudo tee /etc/systemd/system/gplay.service << EOF
[Unit]
Description=GPlay APK Downloader
After=network.target
[Service]
Type=simple
User=$USER
WorkingDirectory=$(pwd)
Environment=CORS_ORIGINS=https://yourdomain.com
ExecStart=$(pwd)/.venv/bin/gunicorn --bind 0.0.0.0:5000 --workers 2 server:app
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable gplay
sudo systemctl start gplaysudo systemctl status gplay # Check status
sudo systemctl restart gplay # Restart
sudo systemctl stop gplay # Stop
journalctl -u gplay -f # View logsThe tool includes 23 device profiles from Aurora Store, used to authenticate with Google Play's anonymous token dispenser. Profiles are rotated automatically during token acquisition to maximize compatibility with restricted apps (e.g., banking apps like Chase).
- Profiles are stored in
profiles/*.propertiesand loaded bydevice_profiles.py - Each profile represents a real Android device (Pixel 9a, Galaxy S25 Ultra, Xperia 5, etc.)
- Profiles are sorted by reliability — the most reliable ones are tried first
- The server cycles through all profiles up to 3 times before giving up
- A built-in fallback profile (Pixel 4a) is used if the
profiles/directory is missing
ARM64 (19 profiles): Pixel 9a, Samsung F34, Xperia 5, Oppo R17, and others ARMv7 (4 profiles): Samsung J5 Prime, Samsung A13 5G, Realme 5 Pro, BRAVIA VU2
python3 test_profiles.py # Test all profiles against restricted appspython3 device_profiles.py # Print all available profiles- Authentication: Gets anonymous token from Aurora Store's dispenser, rotating through device profiles for reliability
- Details: Fetches app metadata (version, size, splits, price) via Google Play's protobuf API. Paid apps are detected immediately and rejected with a clear error — no purchase or delivery is attempted
- Purchase: "Purchases" the free app to get download authorization
- Download: Fetches base APK + config splits from Google Play CDN
- Merge: Combines splits using APKEditor (proper resource table merging)
- Patch: For apps with Play Asset Delivery (e.g. OBB data), patches the manifest with
com.android.dynamic.apk.fused.modulesso the app can find its fused asset packs at runtime - Align: Runs
zipalignto ensureresources.arscis stored uncompressed and 4-byte aligned (required by Android 11+) - Sign: Signs merged APK with debug keystore via apksigner
- Deliver: Returns single installable APK
Modern Android apps use App Bundles which split into:
- Base APK: Core app code and resources
- Config splits: Device-specific resources (screen density, language, CPU architecture)
- Asset pack splits: Large game assets delivered via Play Asset Delivery (e.g.
obbassetscontaining OBB data)
This tool merges them back into a universal APK that works on any device of the target architecture. For apps with asset pack splits, the merged APK includes the asset data and the manifest is patched so the app's Play Core library can locate it.
gplay-apk-downloader/
├── server.py # Flask web server with SSE endpoints
├── gplay-downloader.py # CLI tool
├── gplay # CLI wrapper script (sources venv)
├── axml_patcher.py # Binary AndroidManifest.xml patcher for fused asset packs
├── device_profiles.py # Device profile loader and priority ordering
├── profiles/ # 23 Aurora Store device profiles (.properties)
├── test_profiles.py # Profile testing utility
├── public/ # Web UI static files
│ ├── index.html # HTML skeleton
│ ├── style.css # Styles
│ ├── app.js # Main application JS
│ └── adb.js # WebUSB ADB module
├── gunicorn.conf.py # Gunicorn production config
├── start-server.sh # Server startup script (dev/production)
├── setup.sh # Installation script
├── requirements.txt # Python dependencies
├── APKEditor.jar # Split APK merger (downloaded by setup)
├── server.log # Server logs (generated, auto-rotated)
└── .venv/ # Python virtual environment (generated)
| Variable | Default | Description |
|---|---|---|
CORS_ORIGINS |
(same-origin only) | Comma-separated allowed origins (e.g., https://yourdomain.com). Unset = no cross-origin requests allowed |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
PORT |
5000 |
Server port (used by start-server.sh) |
SITE_URL |
(none) | Site URL for SEO meta tags (e.g., https://apkdl.example.com). Unset = SEO tags stripped |
UMAMI_SCRIPT |
(none) | Full <script> tag for Umami analytics. Unset = no analytics |
UMAMI_REPLAY_SCRIPT |
(none) | Full <script> tag for Umami session replays. Unset = no replays |
DISABLE_APP_PAGES |
(none) | Set to 1 to disable auto-generated app pages (/app/*, /apps). Useful for self-hosters who don't need SEO pages |
Set these in your systemd service file or shell environment.
| Setting | Value | Description |
|---|---|---|
MAX_CONCURRENT_DOWNLOADS |
10 | Parallel download slots |
MAX_CONCURRENT_MERGES |
3 | Parallel merge operations |
SSE_MAX_DURATION |
300s (5 min) | Max time for any SSE stream |
MAX_PROFILE_CYCLES |
3 | Max times to cycle through all profiles |
TEMP_APK_TTL |
600s (10 min) | Temp file lifetime before cleanup |
MAX_TEMP_STORAGE_MB |
2048 (2 GB) | Max disk space for temp APKs |
SEARCH_CACHE_TTL |
21600s (6 hr) | Search result cache lifetime |
DOWNLOAD_QUEUE_TIMEOUT |
30s | Wait time for a free download slot |
| Setting | Value | Description |
|---|---|---|
worker_class |
gevent |
Async workers for SSE streaming |
workers |
min(CPU cores x 2 + 1, 8) | Automatic worker scaling (capped at 8) |
worker_connections |
1000 | Max connections per worker |
timeout |
300s | Request timeout |
keepalive |
65s | Keep-alive for SSE connections |
bind |
0.0.0.0:5000 |
Default listen address |
| File | Description |
|---|---|
~/.gplay-auth.json |
ARM64 auth token cache |
~/.gplay-auth-armv7.json |
ARMv7 auth token cache |
~/.gplay-download-count |
Persistent download counter |
- Content-Security-Policy: Enforcing CSP restricts scripts, styles, fonts, images, and connections to explicitly allowed origins
- Security headers: X-Content-Type-Options, X-Frame-Options (DENY), Referrer-Policy, Permissions-Policy (no camera/mic/geo)
- Reverse proxy support: ProxyFix middleware ensures correct client IP detection behind Cloudflare/nginx
- Rate limiting: Search API is limited to 10 requests/minute per IP; stats increment is limited to 1 request/second per IP
- Request body limits: 1MB max request body enforced at both Flask and gunicorn levels
- CORS: Defaults to same-origin only (no cross-origin requests). Set
CORS_ORIGINSto allow specific external domains - Input validation: Package names are validated against Android naming rules; temp file IDs are validated as strict UUIDs
- XSS protection: All user-controlled data is escaped before DOM insertion, including single quotes in JS contexts
- Auth tokens: Server-side only — auth tokens are never sent to the frontend
- Filename sanitization: All Content-Disposition headers are sanitized against path traversal and header injection
- Auth file permissions: Cached auth tokens are written with 600 permissions (owner read/write only)
- Download counter: Uses file-level locking for safe concurrent access across gunicorn workers
- Logging: Production defaults to
INFOlevel — no auth tokens logged. SetLOG_LEVEL=DEBUGonly for development - No authentication: API endpoints are open by default. Use a reverse proxy (nginx) to add auth if needed
- HTTPS: Not built-in — deploy behind a reverse proxy with TLS termination
- Analytics: The hosted instance at apkdl.dietdroid.com uses Umami to track anonymous page visits (no cookies, no personal data). Self-hosted deployments do not include any analytics unless you configure the
UMAMI_SCRIPTand/orUMAMI_REPLAY_SCRIPTenvironment variables
Run ./gplay auth first to get an authentication token.
Re-run ./setup.sh to download APKEditor.
Check if port 5000 is in use:
lsof -i:5000
kill $(lsof -ti:5000) # Kill existing processOr use a different port: PORT=8080 ./start-server.sh
Some VPS providers block Google CDN. Try:
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.confThe tool automatically rotates through device profiles when acquiring tokens. Some apps (especially banking apps) only work with specific profiles. If all profiles fail after 3 cycles, try again later — the dispenser may be rate-limited (HTTP 429).
The token's device profile isn't compatible with this app. The server will automatically try the next profile in the rotation.
GPLv3 — See LICENSE