Metadata-Version: 2.5
Name: download-apk
Version: 0.2.0
Summary: Search Google Play and download apps' APKs into versioned folders.
Project-URL: Homepage, https://github.com/jeremy-morren/download-apk
Project-URL: Source, https://github.com/jeremy-morren/download-apk/tree/v0.2.0
Project-URL: Documentation, https://github.com/jeremy-morren/download-apk/blob/v0.2.0/README.md
Project-URL: Changelog, https://github.com/jeremy-morren/download-apk/releases/tag/v0.2.0
Project-URL: Issues, https://github.com/jeremy-morren/download-apk/issues
Author: Jeremy Morren
License-Expression: MIT
License-File: LICENSE
Keywords: android,apk,apkeep,download,google-play
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Requires-Dist: google-play-scraper<2,>=1.2.7
Requires-Dist: patchright>=1.50
Requires-Dist: platformdirs>=4
Description-Content-Type: text/markdown

# download-apk

Search Google Play and download an app's APKs into versioned folders, from the command line.

```
download-apk search "firefox"
download-apk download org.mozilla.firefox
```

Downloads land in `apps/<appId>/<version>/`: the base APK, its split APKs, and a `metadata.json` with
each file's SHA-256. Re-running a download picks up new releases and skips versions you already have.

It uses [apkeep](https://github.com/EFForg/apkeep) from the EFF to talk to Google Play, and
[google-play-scraper](https://github.com/JoMingyu/google-play-scraper) for search.

## Requirements

- Python 3.10 or later.
- Google Chrome, for the one-time sign-in.
- A Google account. Use a spare one: Google may flag accounts that download apps outside a phone.
- Windows (x64) or Linux (x64 or arm64). apkeep publishes no macOS build.

## Install

```
pip install download-apk
```

This adds a `download-apk` command. To keep it out of your other Python environments, install it
as a tool instead:

```
pipx install download-apk
uv tool install download-apk
```

To upgrade: `pip install --upgrade download-apk`, `pipx upgrade download-apk` or
`uv tool upgrade download-apk`. To run it once without installing: `uvx download-apk search firefox`.

Other ways in:

- From GitHub's newest release, without PyPI:
  `pip install https://github.com/jeremy-morren/download-apk/releases/latest/download/download-apk.tar.gz`.
- A specific release: `pip install download-apk==0.2.0`, or the wheel attached to that release on
  GitHub.
- The newest commit on `main`: `pip install git+https://github.com/jeremy-morren/download-apk`.

## Use

### Search

```
download-apk search firefox --limit 3
```

```
App ID                    Title                           Developer  Installs     Rating  Price
------------------------  ------------------------------  ---------  -----------  ------  -----
org.mozilla.focus         Firefox Focus: No Fuss Browser  Mozilla    10,000,000+  4.7     Free
org.mozilla.firefox_beta  Firefox Beta for Testers        Mozilla    10,000,000+  4.6     Free
org.mozilla.fenix         Firefox Nightly for Developers  Mozilla    1,000,000+   4.5     Free
```

`--limit` sets the number of results (default 20).

### Download

```
download-apk download com.example.app
```

```
Downloading com.example.app from Google Play...
Saved com.example.app 1.4.2 to /home/me/apks/apps/com.example.app/1.4.2
```

`-o`/`--out` changes the output folder for one run. To change it for every run, set the
`DOWNLOAD_APK_DIR` environment variable; `--out` still wins over it. Without either, apps go to
`./apps`. Google Play only serves an app's current
version, so older versions can't be downloaded. Paid apps download only if the account owns them.

### Options for every command

| Option            | Default              | What it does                                     |
|-------------------|----------------------|--------------------------------------------------|
| `--ini PATH`      | `./.google-play.ini` | Where the Google Play credentials are kept       |
| `--sign-in`       |                      | Sign in again, replacing the saved credentials   |
| `--email ADDRESS` | asked for            | The Google account's email, used when signing in |
| `--country CODE`  | detected             | The store country for search and version lookups |
| `--lang CODE`     | detected             | The store language, e.g. `en` or `en-GB`         |

Without `--country`, the country is where your IP address is, which is also where Google Play places
you. It's looked up from Cloudflare's trace endpoint (`https://www.cloudflare.com/cdn-cgi/trace`), or
from `https://ipinfo.io/country` if Cloudflare doesn't answer, so those services see your IP address.
Offline, it falls back to your system's region setting, then to `us`. Every run logs which it used:

```
Determined Google Play country 'vc' from your IP address; use --country to override.
```

Without `--lang`, the language is your system's display language: on Windows, the Windows display
language; on Linux and macOS, `LC_ALL`, `LC_MESSAGES` or `LANG`. It falls back to `en`, and is logged
the same way:

```
Determined Google Play language 'en-GB' from your system's display language; use --lang to override.
```

## Signing in

Every command first makes sure you're signed in to Google Play:

1. With no saved credentials, Chrome opens on Google's sign-in page. Sign in; the window closes by
   itself. Then type the account's email when asked.
2. The one-time sign-in cookie is exchanged for a long-lived token, saved in `.google-play.ini` in the
   current folder (or wherever `--ini` points).
3. On later runs the token is checked first. If Google no longer accepts it, the browser sign-in
   runs again.

Downloading accepts the Google Play terms of service for the account.

### Keep the credentials out of git

`.google-play.ini` holds a token that works like a password for the Google account. On every run,
download-apk checks whether that file is in a git repository and warns you:

- **"credentials are already in git"**: the file is tracked. Remove it from the index with
  `git rm --cached .google-play.ini`, add it to `.gitignore`, and sign in again with `--sign-in` so
  the token in the repository's history stops being the current one.
- **"credentials will be added to git"**: the file isn't ignored, so the next `git add .` would
  commit it. Add `.google-play.ini` to `.gitignore`.

Outside a git repository, or without git installed, nothing is checked.

## Where files go

| What                    | Where                                                                                                                                                |
|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| Downloaded apps         | `./apps/<appId>/<version>/`, or `--out`, or `$DOWNLOAD_APK_DIR`                                                                                      |
| Google Play credentials | `./.google-play.ini`, or `--ini`                                                                                                                     |
| The apkeep program      | Your user cache folder, e.g. `%LOCALAPPDATA%\download-apk\Cache\tools` on Windows or `~/.cache/download-apk/tools` on Linux, downloaded on first use |

## Develop

```
git clone https://github.com/jeremy-morren/download-apk
cd download-apk
python -m venv .venv
.venv/Scripts/activate            # Windows; on Linux: source .venv/bin/activate
pip install -e . --group test     # pip 25.1 or later
pytest
```

The tests run offline: Google Play, sign-in and apkeep are replaced with fakes, and the APKs are
generated. `pytest -m network` runs the few checks that talk to the real Google Play.

You can also run the command without installing it: `python -m download_apk ...` from the `src`
folder.

### Releasing

GitHub Actions tests every push and pull request on Windows and Linux, with the oldest and newest
supported Python, then builds the package on Linux and checks that both files install and run. For a
push to `main`:

1. Raise `version` in `pyproject.toml` and `__version__` in `src/download_apk/__init__.py`; a test
   fails if they differ.
2. Push to `main`. `ci.yml` creates a draft release `v<version>` with two files: the wheel, and the
   source tarball as `download-apk.tar.gz`, both attested (see below). If that release already exists
   it does nothing; if only a tag is left from a deleted release, it deletes the tag first.
3. Review the draft on GitHub and publish it. That creates the tag, and `release.yml` uploads the
   release's wheel to PyPI.

PyPI accepts the upload through trusted publishing: its publisher settings for `download-apk` name
this repository, the workflow `release.yml` and the GitHub environment `pypi`, so no PyPI token is
stored anywhere. A version can be uploaded to PyPI only once, even if it is later deleted there.

### Verifying a download

Every file is signed with a record of where it was built: which repository, workflow and commit.

- **GitHub release files** carry a GitHub artifact attestation:

  ```
  gh attestation verify download-apk.tar.gz --repo jeremy-morren/download-apk
  ```

- **PyPI files** carry a PyPI attestation, made by the upload: the file's page on pypi.org shows it,
  and `pypi-attestations verify pypi --repository https://github.com/jeremy-morren/download-apk
  pypi:download_apk-0.2.0-py3-none-any.whl` checks it.

Both are signed through Sigstore with a certificate GitHub issues to the workflow run, so no key is
kept anywhere. For a stronger check, rebuild the release yourself (next section).

### Reproducible builds

Building the same commit on Linux gives byte-identical files, so anyone can check a release against
its source:

```
git clone https://github.com/jeremy-morren/download-apk && cd download-apk && git checkout v0.2.0
python -m build
sha256sum dist/*       # compare with the SHA-256 values in the release notes
```

- `hatch_build.py` dates every file with the commit's time (through `SOURCE_DATE_EPOCH`, which wins if
  you set it yourself) and reads the repository links from the `origin` remote. The source tarball
  records both in `.git-info.json`, so a wheel built from the tarball, as `python -m build` does,
  matches one built from a checkout.
- hatchling, the build backend, writes files in a fixed order with fixed permissions. `pyproject.toml`
  pins it to one version, since another version may lay out the files differently.
- `.gitattributes` keeps every text file LF in every checkout; a single CRLF file changes the wheel.

Windows builds hold the same files with the same contents, but not the same bytes: Python's zip
writer records the building OS in each entry, and Python 3.14 on Windows compresses with zlib-ng
rather than zlib. On Windows, compare contents instead: the `RECORD` file inside the wheel lists every
file's SHA-256, and it's identical on every OS.

## Troubleshooting

- **"Google Play has no app …, or doesn't offer it to this account."** Check the app ID with
  `search`. Some apps are limited to certain countries or devices.
- **"Google Play refused the saved sign-in."** Run again with `--sign-in`.
- **Chrome doesn't open.** Install Google Chrome. Sign-in drives your installed Chrome through
  [patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python); no separate browser
  download is needed.

## License

[MIT](https://github.com/jeremy-morren/download-apk/blob/v0.2.0/LICENSE).
