Last active
June 27, 2018 12:46
-
-
Save danielcarr/d4e454c1586436b0502eca38ccd10897 to your computer and use it in GitHub Desktop.
Download the most up to date Google Play Services packages for all device configurations from APKMirror.com
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python3 | |
import os | |
import requests | |
from re import compile as compile_regex | |
from hashlib import md5 as checksum | |
from zipfile import ZipFile as zipfile | |
from html.parser import HTMLParser | |
# Class to extract the link to the download page of the most recent (non-beta) version | |
# of an apk from a version list page on APKMirror.com | |
class ApkLinkParser(HTMLParser): | |
def __init__(self): | |
HTMLParser.__init__(self) | |
self.finished = False | |
self.in_primary = False | |
self.in_table = False | |
self.check_text = False | |
self.check_link = False | |
self.found_link = None | |
def handle_starttag(self, tag, attrs): | |
if not self.finished: | |
if self.in_table: | |
if self.check_link and tag == 'a': | |
self.found_link = dict(attrs)['href'] | |
self.check_link = False | |
self.in_table = False | |
self.in_primary = False | |
self.finished = True | |
elif self.found_link is None: | |
if tag == 'h5': | |
attrs = dict(attrs) | |
attr_class = attrs.get('class') | |
attr_title = attrs.get('title') | |
is_row = attr_class is not None and 'appRowTitle' in attr_class | |
self.check_link = is_row and attr_title is not None and 'beta' not in attr_title | |
elif tag == 'p': | |
self.check_text = True | |
elif tag == 'div' and dict(attrs).get('class') == 'appRow center': | |
self.finished = True | |
elif self.in_primary: | |
if tag == 'div': | |
attrs = dict(attrs) | |
attr_class = attrs.get('class') | |
if attr_class == 'listWidget': | |
self.in_table = True | |
elif attr_class == 'gooWidget google-ad-leaderboard ' or attrs.get('id') == 'sidebar': | |
self.finished = True | |
else: | |
self.in_primary = tag == 'div' and dict(attrs).get('id') == 'primary' | |
def handle_data(self, data): | |
if self.check_text: | |
self.finished = data == 'No apps found' | |
def handle_endtag(self, tag): | |
if tag == 'p' and self.check_text: | |
self.check_text = False | |
def handle_startendtag(self, tag, attrs): | |
pass | |
def get_link(self, data): | |
self.feed(data) | |
return self.found_link | |
ARCHIVE_FILE = 'playservices.zip' | |
PACKAGE_FILE_NAME = 'playservices-{variant_code}' | |
PACKAGE_EXTENSION = os.extsep + 'apk' | |
CHECKSUM_EXTENSION = os.extsep + 'md5' | |
DOWNLOAD_SITE = 'https://www.apkmirror.com' | |
VARIANT_ENDPOINT = '/apk/google-inc/google-play-services/variant-{parameters}/' | |
# the format of the query parameter representing a device configuration variant | |
VARIANT_PARAM = '{{"arches_slug"%3A["{architecture}"]%2C"dpis_slug"%3A["{density}"]%2C"minapi_slug"%3A"minapi-{osversion}"}}' | |
DOWNLOAD_ENDPOINT = '/wp-content/themes/APKMirror/download.php?id={file_id}' | |
ARCHS = ('armeabi-v7a', 'arm64-v8a"%2C"armeabi-v7a', 'x86', 'x86"%2C"x86_64') | |
ARCH_CODES = ('3', '4', '7', '8') | |
DENSITIES = ('nodpi', '160', '240', '320', '480') | |
DENSITY_CODES = ('0', '2', '4', '6', '8') | |
OS_VERSIONS = (14, 21, 23, 26, 28) | |
SDK_CODES = ('0', '2', '4', '9', '10') | |
INDEX_PATTERN = compile_regex('</\?p=(\d*)>') | |
# all device configuration codes | |
CODES = [(sdk + arch + density) \ | |
for sdk in SDK_CODES for arch in ARCH_CODES for density in DENSITY_CODES] | |
# parameters for the version list page for each configuration code (variant), in the same order | |
VARIANTS = [VARIANT_PARAM.format(architecture=arch,density=density,osversion=sdk) \ | |
for sdk in OS_VERSIONS for arch in ARCHS for density in DENSITIES] | |
# map of configuration codes to configuration/variant endpoints | |
VARIANT_MAP = dict(zip(CODES, [VARIANT_ENDPOINT.format(parameters=variant_string) \ | |
for variant_string in VARIANTS])) | |
page_map = {} | |
download_map = {} | |
# map spec codes to urls for version to download (if available) | |
for code, variant_endpoint in VARIANT_MAP.items(): | |
link = ApkLinkParser().get_link(requests.get(DOWNLOAD_SITE + variant_endpoint).text) | |
if link is not None: | |
page_map[code] = DOWNLOAD_SITE + link | |
# map package download ids to downloaded file names | |
for code, download_page in page_map.items(): | |
page_headers = requests.head(download_page).headers | |
package_resource_id = INDEX_PATTERN.search(page_headers['Link']).group(1) | |
download_map[package_resource_id] = PACKAGE_FILE_NAME.format(variant_code=code) | |
# download the package for each configuration and write its checksum to a corresponding file | |
for resource_id, package_name in download_map.items(): | |
download_url = DOWNLOAD_SITE + DOWNLOAD_ENDPOINT.format(file_id=resource_id) | |
download_source = requests.get(download_url, stream=True) | |
package_file = package_name + PACKAGE_EXTENSION | |
with open(package_file, 'wb') as download_file: | |
for chunk in download_source.iter_content(chunk_size=1024): | |
download_file.write(chunk) | |
checksum_file = package_name + CHECKSUM_EXTENSION | |
with open(package_file, 'rb') as downloaded_file, open(checksum_file, 'wt') as hash_file: | |
hash_file.write(checksum(downloaded_file.read()).hexdigest()) | |
# should the old zipfile be backed up, in case there is something wrong with the new files? | |
# os.rename(ARCHIVE_FILE, ARCHIVE_FILE + '.bak') | |
# (the following could be done in the previous loop (by appending to a new zipfile), | |
# but this way is only one file opening and it captures previously | |
# downloaded files that don't have more recent versions available | |
# zip up all downloaded apk and checksum files (including what was there before the download) | |
with zipfile(ARCHIVE_FILE, 'w') as archive: | |
for f in os.listdir(): | |
if f.endswith(PACKAGE_EXTENSION) or f.endswith(CHECKSUM_EXTENSION): | |
archive.write(f) | |
# os.remove(f) # delete zipped file |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment