Skip to content

Instantly share code, notes, and snippets.

@danielcarr
Last active June 27, 2018 12:46
Show Gist options
  • Save danielcarr/d4e454c1586436b0502eca38ccd10897 to your computer and use it in GitHub Desktop.
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
#! /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