2 Commits

Author SHA1 Message Date
e0fb84ccb9 chore(release): prepare 0.2.3 2026-07-30 22:22:11 +02:00
30025a4ebf fix(release): download Play APK media 2026-07-30 22:19:40 +02:00
3 changed files with 91 additions and 13 deletions

View File

@@ -184,6 +184,19 @@ def generated_apks_url(package_name: str, version_code: int) -> str:
return f"{API_ROOT}/applications/{package}/generatedApks/{version_code}" return f"{API_ROOT}/applications/{package}/generatedApks/{version_code}"
def generated_apk_download_url(
package_name: str,
version_code: int,
download_id: str,
) -> str:
package = urllib.parse.quote(package_name, safe="")
download = urllib.parse.quote(download_id, safe="")
return (
f"{API_ROOT}/applications/{package}/generatedApks/"
f"{version_code}/downloads/{download}:download?alt=media"
)
def get_generated_apks( def get_generated_apks(
client: PlayClient, client: PlayClient,
package_name: str, package_name: str,
@@ -216,22 +229,23 @@ def download_universal_apk(
selected = find_universal_apk(response, expected_fingerprint) selected = find_universal_apk(response, expected_fingerprint)
if selected is not None: if selected is not None:
fingerprint, download_id = selected fingerprint, download_id = selected
package = urllib.parse.quote(package_name, safe="") apk = client.request(
download = urllib.parse.quote(download_id, safe="") "GET",
url = ( generated_apk_download_url(
f"{API_ROOT}/applications/{package}/generatedApks/" package_name,
f"{version_code}/downloads/{download}:download" version_code,
download_id,
),
) )
output.parent.mkdir(parents=True, exist_ok=True) if apk:
output.write_bytes(client.request("GET", url)) output.parent.mkdir(parents=True, exist_ok=True)
if output.stat().st_size == 0: output.write_bytes(apk)
raise RuntimeError("Google Play returned an empty universal APK") return fingerprint
return fingerprint
if attempt < attempts: if attempt < attempts:
time.sleep(interval_seconds) time.sleep(interval_seconds)
raise RuntimeError( raise RuntimeError(
"Google Play did not provide a universal APK signed with the expected " "Google Play did not provide a non-empty universal APK signed with the "
f"certificate after {attempts} attempts" f"expected certificate after {attempts} attempts"
) )

View File

@@ -1,4 +1,5 @@
import importlib.util import importlib.util
import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
@@ -44,6 +45,69 @@ class PublishPlayTests(unittest.TestCase):
("aabb", "correct"), ("aabb", "correct"),
) )
def test_downloads_generated_apk_as_media(self):
class FakePlayClient:
def __init__(self):
self.download_urls = []
self.media_attempts = 0
def request_json(self, method, url):
self.assert_request(method, url)
return {
"generatedApks": [
{
"certificateSha256Hash": "AA:BB",
"generatedUniversalApk": {
"downloadId": "download/id+=",
},
}
]
}
def request(self, method, url):
self.assert_request(method, url)
self.download_urls.append(url)
if not url.endswith("?alt=media"):
return b""
self.media_attempts += 1
return b"apk" if self.media_attempts == 2 else b""
@staticmethod
def assert_request(method, url):
if method != "GET" or not url.startswith(publish_play.API_ROOT):
raise AssertionError(f"unexpected request: {method} {url}")
client = FakePlayClient()
with tempfile.TemporaryDirectory() as scratch:
output = Path(scratch) / "universal.apk"
fingerprint = publish_play.download_universal_apk(
client,
"com.example app",
2002,
"aa:bb",
output,
attempts=2,
interval_seconds=0,
)
self.assertEqual(fingerprint, "aabb")
self.assertEqual(output.read_bytes(), b"apk")
self.assertEqual(
client.download_urls,
[
(
f"{publish_play.API_ROOT}/applications/com.example%20app/"
"generatedApks/2002/downloads/"
"download%2Fid%2B%3D:download?alt=media"
),
(
f"{publish_play.API_ROOT}/applications/com.example%20app/"
"generatedApks/2002/downloads/"
"download%2Fid%2B%3D:download?alt=media"
),
],
)
def test_track_update_preserves_existing_releases_and_adds_draft(self): def test_track_update_preserves_existing_releases_and_adds_draft(self):
track = { track = {
"track": "closed-beta", "track": "closed-beta",

View File

@@ -1,3 +1,3 @@
PRODUCT_VERSION=0.2.2 PRODUCT_VERSION=0.2.3
RELEASE_CHANNEL=beta RELEASE_CHANNEL=beta
WINDOWS_VERSION_EPOCH=1 WINDOWS_VERSION_EPOCH=1