def main(): # get args sys.argv.pop(0) searchQuery = ' '.join(sys.argv) # read token cache tokenCache = configparser.ConfigParser() tokenCache.read('.cache.ini') try: gsfId = int(tokenCache['DEFAULT'] ['gsfId']) # does not get saved as int, but as str! authSubToken = tokenCache['DEFAULT']['authSubToken'] timezone = tokenCache['DEFAULT']['timezone'] locale = tokenCache['DEFAULT']['locale'] except KeyError: print( "Missing login data. Please, check your cache file, or run login.py" ) sys.exit(1) except configparser.ParsingError: print( "The cache file could not be read correctly. Please, check it, or run login.py" ) sys.exit(1) except configparser.Error as e: print("Error ", e, " while reading cache file") sys.exit(1) server = GooglePlayAPI(locale, timezone) # log in with saved credentials try: server.login(None, None, gsfId, authSubToken) except: print("Error while trying to login to GP servers") sys.exit(2) resultRaw = server.search(searchQuery, 20) for app in resultRaw: docid = app.get('docId') details = server.details(docid) filename = app.get('filename') if filename is None: filename = details.get('docId') + '.apk' if details is None: print('Package ', docid, ' does not exist') continue print(docid, ' , ', filename, details['versionCode'])
server = GooglePlayAPI('en_EN', 'Usa/Chicago') # LOGIN print('\nLogging in with email and password\n') server.login(args.email, args.password, None, None) gsfId = server.gsfId authSubToken = server.authSubToken print('\nNow trying secondary login with ac2dm token and gsfId saved\n') # server = GooglePlayAPI('it_IT', 'Europe/Rome') server.login(None, None, gsfId, authSubToken) # SEARCH apps = server.search('telegram', 34, None) print('\nSearch suggestion for "fir"\n') print(server.searchSuggest('fir')) print('nb_result: 34') print('number of results: %d' % len(apps)) print('\nFound those apps:\n') for a in apps: print(a['docId']) # DOWNLOAD docid = apps[0]['docId'] print('\nTelegram docid is: %s\n' % docid) print('\nAttempting to download %s\n' % docid)
class GPlaycli(object): def __init__(self, args=None, credentials=None): # no config file given, look for one if credentials is None: # default local user configs cred_paths_list = [ 'gplaycli.conf', os.path.expanduser("~") + '/.config/gplaycli/gplaycli.conf', '/etc/gplaycli/gplaycli.conf' ] tmp_list = list(cred_paths_list) while not os.path.isfile(tmp_list[0]): tmp_list.pop(0) if not tmp_list: raise OSError("No configuration file found at %s" % cred_paths_list) credentials = tmp_list[0] default_values = dict() self.configparser = configparser.ConfigParser(default_values) self.configparser.read(credentials) self.config = {key: value for key, value in self.configparser.items("Credentials")} self.tokencachefile = os.path.expanduser(self.configparser.get("Cache", "token")) self.playstore_api = None # default settings, ie for API calls if args is None: self.yes = False self.verbose = False self.progress_bar = False self.logging_enable = False self.device_codename = 'bacon' self.addfiles_enable = False # if args are passed else: self.yes = args.yes_to_all self.verbose = args.verbose if self.verbose: logger.setLevel(logging.INFO) handler = logging.StreamHandler() formatter = logging.Formatter("[%(levelname)s] %(message)s") handler.setFormatter(formatter) logger.addHandler(handler) logger.propagate = False logger.info('GPlayCli version %s', __version__) logger.info('Configuration file is %s', credentials) self.progress_bar = args.progress_bar self.set_download_folder(args.update_folder) self.logging_enable = args.logging_enable self.device_codename = args.device_codename self.addfiles_enable = args.addfiles_enable if args.token_enable is None: self.token_enable = self.configparser.getboolean('Credentials', 'token') else: self.token_enable = args.token_enable if self.token_enable: if args.token_url is None: self.token_url = self.configparser.get('Credentials', 'token_url') else: self.token_url = args.token_url self.token, self.gsfid = self.retrieve_token() if self.logging_enable: self.success_logfile = "apps_downloaded.log" self.failed_logfile = "apps_failed.log" self.unavail_logfile = "apps_not_available.log" def get_cached_token(self): try: with open(self.tokencachefile, 'r') as tcf: token, gsfid = tcf.readline().split() if not token: token = None gsfid = None except (IOError, ValueError): # cache file does not exists or is corrupted token = None gsfid = None logger.error('cache file does not exists or is corrupted') return token, gsfid def write_cached_token(self, token, gsfid): try: # creates cachedir if not exists cachedir = os.path.dirname(self.tokencachefile) if not os.path.exists(cachedir): os.mkdir(cachedir) with open(self.tokencachefile, 'w') as tcf: tcf.write("%s %s" % (token, gsfid)) except IOError as e: err_str = "Failed to write token to cache file: %s %s" % (self.tokencachefile, e.strerror) logger.error(err_str) raise IOError(err_str) def retrieve_token(self, force_new=False): token, gsfid = self.get_cached_token() if token is not None and not force_new: logger.info("Using cached token.") return token, gsfid logger.info("Retrieving token ...") r = requests.get(self.token_url) if r.text == 'Auth error': print('Token dispenser auth error, probably too many connections') sys.exit(ERRORS.TOKEN_DISPENSER_AUTH_ERROR) elif r.text == "Server error": print('Token dispenser server error') sys.exit(ERRORS.TOKEN_DISPENSER_SERVER_ERROR) token, gsfid = r.text.split(" ") logger.info("Token: %s", token) logger.info("GSFId: %s", gsfid) self.token = token self.gsfid = gsfid self.write_cached_token(token, gsfid) return token, gsfid def set_download_folder(self, folder): self.config["download_folder_path"] = folder def connect_to_googleplay_api(self): self.playstore_api = GooglePlayAPI(device_codename=self.device_codename) error = None email = None password = None authSubToken = None gsfId = None if self.token_enable is False: logger.info("Using credentials to connect to API") email = self.config["gmail_address"] if self.config["gmail_password"]: logger.info("Using plaintext password") password = self.config["gmail_password"] elif self.config["keyring_service"] and HAVE_KEYRING is True: password = keyring.get_password(self.config["keyring_service"], email) elif self.config["keyring_service"] and HAVE_KEYRING is False: print("You asked for keyring service but keyring package is not installed") sys.exit(ERRORS.KEYRING_NOT_INSTALLED) else: logger.info("Using token to connect to API") authSubToken = self.token gsfId = int(self.gsfid, 16) try: self.playstore_api.login(email=email, password=password, authSubToken=authSubToken, gsfId=gsfId) except (ValueError, IndexError, LoginError, DecodeError) as ve: # invalid token or expired logger.info("Token has expired or is invalid. Retrieving a new one...") self.retrieve_token(force_new=True) self.playstore_api.login(authSubToken=self.token, gsfId=int(self.gsfid, 16)) success = True return success, error def list_folder_apks(self, folder): list_of_apks = [filename for filename in os.listdir(folder) if filename.endswith(".apk")] return list_of_apks def prepare_analyse_apks(self): download_folder_path = self.config["download_folder_path"] list_of_apks = [filename for filename in os.listdir(download_folder_path) if os.path.splitext(filename)[1] == ".apk"] if list_of_apks: logger.info("Checking apks ...") self.analyse_local_apks(list_of_apks, self.playstore_api, download_folder_path, self.prepare_download_updates) def analyse_local_apks(self, list_of_apks, playstore_api, download_folder_path, return_function): list_apks_to_update = [] package_bunch = [] version_codes = [] for position, filename in enumerate(list_of_apks): filepath = os.path.join(download_folder_path, filename) logger.info("Analyzing %s", filepath) a = APK(filepath) packagename = a.package package_bunch.append(packagename) version_codes.append(a.version_code) # BulkDetails requires only one HTTP request # Get APK info from store details = playstore_api.bulkDetails(package_bunch) for detail, packagename, filename, apk_version_code in zip(details, package_bunch, list_of_apks, version_codes): store_version_code = detail['versionCode'] # Compare if apk_version_code != "" and int(apk_version_code) < int(store_version_code) and int( store_version_code) != 0: # Add to the download list list_apks_to_update.append([packagename, filename, int(apk_version_code), int(store_version_code)]) return_function(list_apks_to_update) def prepare_download_updates(self, list_apks_to_update): if list_apks_to_update: list_of_packages_to_download = [] # Ask confirmation before downloading message = "The following applications will be updated :" for packagename, filename, apk_version_code, store_version_code in list_apks_to_update: message += "\n%s Version : %s -> %s" % (filename, apk_version_code, store_version_code) list_of_packages_to_download.append([packagename, filename]) message += "\n" print(message) if not self.yes: print("\nDo you agree?") return_value = input('y/n ?') if self.yes or return_value == 'y': logger.info("Downloading ...") downloaded_packages = self.download_selection(self.playstore_api, list_of_packages_to_download, self.after_download) return_string = str() for package in downloaded_packages: return_string += package + " " print("Updated: " + return_string[:-1]) else: print("Everything is up to date !") sys.exit(ERRORS.OK) def download_selection(self, playstore_api, list_of_packages_to_download, return_function): success_downloads = list() failed_downloads = list() unavail_downloads = list() # BulkDetails requires only one HTTP request # Get APK info from store details = playstore_api.bulkDetails([pkg[0] for pkg in list_of_packages_to_download]) position = 1 for detail, item in zip(details, list_of_packages_to_download): packagename, filename = item logger.info("%s / %s %s", position, len(list_of_packages_to_download), packagename) # Check for download folder download_folder_path = self.config["download_folder_path"] if not os.path.isdir(download_folder_path): os.mkdir(download_folder_path) # Get the version code and the offer type from the app details # m = playstore_api.details(packagename) vc = detail['versionCode'] # Download try: data_dict = playstore_api.download(packagename, vc, progress_bar=self.progress_bar, expansion_files=self.addfiles_enable) success_downloads.append(packagename) except IndexError as exc: logger.error("Error while downloading %s : %s" % (packagename, "this package does not exist, " "try to search it via --search before")) unavail_downloads.append((item, exc)) except Exception as exc: logger.error("Error while downloading %s : %s" % (packagename, exc)) failed_downloads.append((item, exc)) else: if filename is None: filename = packagename + ".apk" filepath = os.path.join(download_folder_path, filename) data = data_dict['data'] additional_data = data_dict['additionalData'] try: open(filepath, "wb").write(data) if additional_data: for obb_file in additional_data: obb_filename = "%s.%s.%s.obb" % (obb_file["type"], obb_file["versionCode"], data_dict["docId"]) obb_filename = os.path.join(download_folder_path, obb_filename) open(obb_filename, "wb").write(obb_file["data"]) except IOError as exc: logger.error("Error while writing %s : %s" % (packagename, exc)) failed_downloads.append((item, exc)) position += 1 success_items = set(success_downloads) failed_items = set([item[0] for item, error in failed_downloads]) unavail_items = set([item[0] for item, error in unavail_downloads]) to_download_items = set([item[0] for item in list_of_packages_to_download]) if self.logging_enable: self.write_logfiles(success_items, failed_items, unavail_items) return_function(failed_downloads + unavail_downloads) return to_download_items - failed_items def after_download(self, failed_downloads): # Info message if not failed_downloads: message = "Download complete" else: message = "A few packages could not be downloaded :" for item, exception in failed_downloads: package_name, filename = item if filename is not None: message += "\n%s : %s" % (filename, package_name) else: message += "\n%s" % package_name message += "\n%s\n" % exception print(message) def raw_search(self, results_list, search_string, nb_results): # Query results return self.playstore_api.search(search_string, nb_result=nb_results) def search(self, results_list, search_string, nb_results, free_only=True, include_headers=True): try: results = self.raw_search(results_list, search_string, nb_results) except IndexError: results = list() if not results: print("No result") return all_results = list() if include_headers: # Name of the columns col_names = ["Title", "Creator", "Size", "Downloads", "Last Update", "AppID", "Version", "Rating"] all_results.append(col_names) # Compute results values for result in results: # skip that app if it not free # or if it's beta (pre-registration) if (len(result['offer']) == 0 # beta apps (pre-registration) or free_only and result['offer'][0]['checkoutFlowRequired'] # not free to download ): continue l = [result['title'], result['author'], util.sizeof_fmt(result['installationSize']), result['numDownloads'], result['uploadDate'], result['docId'], result['versionCode'], "%.2f" % result["aggregateRating"]["starRating"] ] if len(all_results) < int(nb_results) + 1: all_results.append(l) if self.verbose: # Print a nice table col_width = list() for column_indice in range(len(all_results[0])): col_length = max([len("%s" % row[column_indice]) for row in all_results]) col_width.append(col_length + 2) for result in all_results: print("".join(str("%s" % item).strip().ljust(col_width[indice]) for indice, item in enumerate(result))) return all_results def download_packages(self, list_of_packages_to_download): self.download_selection(self.playstore_api, [(pkg, None) for pkg in list_of_packages_to_download], self.after_download) def write_logfiles(self, success, failed, unavail): for result, logfile in [(success, self.success_logfile), (failed, self.failed_logfile), (unavail, self.unavail_logfile) ]: if result: with open(logfile, 'w') as _buffer: for package in result: print(package, file=_buffer)
server = GooglePlayAPI(debug=True) # LOGIN print('\nLogging in with email and password\n') server.login(args.email, args.password, None, None) gsfId = server.gsfId authSubToken = server.authSubToken print('\nNow trying secondary login with ac2dm token and gsfId saved\n') server = GooglePlayAPI(debug=True) server.login(None, None, gsfId, authSubToken) # SEARCH apps = server.search('telegram', 34, None) print('nb_result: 34') print('number of results: %d' % len(apps)) print('\nFound those apps:\n') for a in apps: print(a['docId']) # DOWNLOAD docid = apps[0]['docId'] print('\nTelegram docid is: %s\n' % docid) print('\nAttempting to download %s\n' % docid) fl = server.download(docid, None, progress_bar=True) with open(docid + '.apk', 'wb') as f: f.write(fl['data'])
class Play(object): def __init__(self, debug=True, fdroid=False): self.currentSet = [] self.totalNumOfApps = 0 self.debug = debug self.fdroid = fdroid self.firstRun = True self.loggedIn = False self._email = None self._passwd = None self._gsfId = None self._token = None self._last_fdroid_update = None # configuring download folder if self.fdroid: self.download_path = os.path.join(os.getcwd(), 'repo') else: self.download_path = os.getcwd() # configuring fdroid data if self.fdroid: self.fdroid_exe = 'fdroid' self.fdroid_path = os.getcwd() self.fdroid_init() # language settings locale = os.environ.get('LANG_LOCALE') if locale is None: locale = locale_service.getdefaultlocale()[0] timezone = os.environ.get('LANG_TIMEZONE') if timezone is None: timezone = 'Europe/Berlin' device = os.environ.get('DEVICE_CODE') if device is None: self.service = GooglePlayAPI(locale, timezone) else: self.service = GooglePlayAPI(locale, timezone, device_codename=device) def fdroid_init(self): found = False for path in os.environ['PATH'].split(':'): exe = os.path.join(path, self.fdroid_exe) if os.path.isfile(exe): found = True break if not found: print('Please install fdroid') sys.exit(1) elif os.path.isfile('config.py'): print('Repo already initalized, skipping init') else: p = Popen([self.fdroid_exe, 'init', '-v'], stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: sys.stderr.write("error initializing fdroid repository " + stderr.decode('utf-8')) sys.exit(1) # backup config.py if self.debug: print('Checking config.py file') with open('config.py', 'r') as config_file: content = config_file.readlines() with open('config.py', 'w') as config_file: # copy all the original content of config.py # if the file was not modified with custom values, do it modified = False for line in content: if '# playmaker' in line: modified = True config_file.write(line) if not modified: if self.debug: print('Appending playmaker data to config.py') config_file.write( '\n# playmaker\nrepo_name = "playmaker"\n' 'repo_description = "repository managed with ' 'playmaker https://github.com/NoMore201/playmaker"\n') # ensure all folder and files are setup p = Popen([self.fdroid_exe, 'update', '--create-key', '-v'], stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: print('Skipping fdroid update') else: print('Fdroid repo initialized successfully') def get_last_fdroid_update(self): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} return {'status': 'SUCCESS', 'message': str(self._last_fdroid_update)} def fdroid_update(self): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} if self.fdroid: try: p = Popen([self.fdroid_exe, 'update', '-c', '--clean'], stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: sys.stderr.write("error updating fdroid repository " + stderr.decode('utf-8')) return makeError(FDROID_ERR) else: print('Fdroid repo updated successfully') self._last_fdroid_update = dt.today().replace( microsecond=0) return {'status': 'SUCCESS'} except Exception as e: return makeError(FDROID_ERR) else: return {'status': 'SUCCESS'} def get_apps(self): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} if self.firstRun: return { 'status': 'PENDING', 'total': self.totalNumOfApps, 'current': len(self.currentSet) } return { 'status': 'SUCCESS', 'message': sorted(self.currentSet, key=lambda k: k['title']) } def set_encoded_credentials(self, email, password): self._email = base64.b64decode(email).decode('utf-8') self._passwd = base64.b64decode(password).decode('utf-8') def set_credentials(self, email, password): self._email = email self._passwd = password def set_token_credentials(self, gsfId, token): self._gsfId = int(gsfId, 16) self._token = token def has_credentials(self): passwd_credentials = self._email is not None and self._passwd is not None token_credentials = self._gsfId is not None and self._token is not None return passwd_credentials or token_credentials def login(self): if self.loggedIn: return { 'status': 'SUCCESS', 'securityCheck': False, 'message': 'OK' } try: if not self.has_credentials(): raise LoginError("missing credentials") self.service.login(self._email, self._passwd, self._gsfId, self._token) self.loggedIn = True return { 'status': 'SUCCESS', 'securityCheck': False, 'message': 'OK' } except LoginError as e: print('LoginError: {0}'.format(e)) self.loggedIn = False return { 'status': 'ERROR', 'securityCheck': False, 'message': 'Wrong credentials' } except SecurityCheckError as e: print('SecurityCheckError: {0}'.format(e)) self.loggedIn = False return { 'status': 'ERROR', 'securityCheck': True, 'message': 'Need security check' } except RequestError as e: # probably tokens are invalid, so it is better to # invalidate them print('RequestError: {0}'.format(e)) self.loggedIn = False return { 'status': 'ERROR', 'securityCheck': False, 'message': 'Request error, probably invalid token' } def update_state(self): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} print('Updating cache') with concurrent.futures.ProcessPoolExecutor() as executor: # get application ids from apk files apkFiles = [ apk for apk in os.listdir(self.download_path) if os.path.splitext(apk)[1] == '.apk' ] self.totalNumOfApps = len(apkFiles) if self.totalNumOfApps != 0: future_to_app = [ executor.submit(get_details_from_apk, a, self.download_path, self.service) for a in apkFiles ] for future in concurrent.futures.as_completed(future_to_app): app = future.result() if app is not None: self.currentSet.append(app) print('Cache correctly initialized') self.firstRun = False def insert_app_into_state(self, newApp): found = False result = list( filter(lambda x: x['docid'] == newApp['docid'], self.currentSet)) if len(result) > 0: found = True if self.debug: print('%s is already cached, updating..' % newApp['docid']) i = self.currentSet.index(result[0]) self.currentSet[i] = newApp if not found: if self.debug: print('Adding %s into cache..' % newApp['docid']) self.currentSet.append(newApp) def search(self, appName, numItems=15): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} try: apps = self.service.search(appName) except RequestError as e: print(e) self.loggedIn = False return {'status': 'ERROR', 'message': SESSION_EXPIRED_ERR} except LoginError as e: print(SESSION_EXPIRED_ERR) self.loggedIn = False except IndexError as e: print(SESSION_EXPIRED_ERR) self.loggedIn = False return {'status': 'SUCCESS', 'message': apps} def details(self, app): try: details = self.service.details(app) except RequestError: details = None return details def get_bulk_details(self, apksList): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} try: apps = [self.details(a) for a in apksList] except LoginError as e: print(e) self.loggedIn = False return apps def download_selection(self, apps): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} success = [] failed = [] unavail = [] for app in apps: docid = app.get('docid') details = self.details(docid) filename = app.get('filename') if filename is None: filename = details.get('docid') + '.apk' if details is None: print('Package %s does not exits' % docid) unavail.append(docid) continue print('Downloading %s' % docid) try: if details.get('offer')[0].get('micros') == 0: data_gen = self.service.download( docid, details.get('details').get('appDetails') ['versionCode']) else: data_gen = self.service.delivery( docid, details.get('details').get('appDetails') ['versionCode']) data_gen = data_gen.get('file').get('data') except IndexError as exc: print(exc) print('Package %s does not exists' % docid) unavail.append(docid) except Exception as exc: print(exc) print('Failed to download %s' % docid) failed.append(docid) else: filepath = os.path.join(self.download_path, filename) try: with open(filepath, 'wb') as apk_file: for chunk in data_gen: apk_file.write(chunk) except IOError as exc: print('Error while writing %s: %s' % (filename, exc)) failed.append(docid) details['filename'] = filename success.append(details) for x in success: self.insert_app_into_state(x) return { 'status': 'SUCCESS', 'message': { 'success': success, 'failed': failed, 'unavail': unavail } } def check_local_apks(self): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} if len(self.currentSet) == 0: print('There is no package') return {'status': 'SUCCESS', 'message': []} else: toUpdate = [] for app in self.currentSet: details = self.details(app.get('docid')) #print(details) if details is None: print('%s not available in Play Store' % app['docid']) continue details['filename'] = app.get('filename') if self.debug: print('Checking %s' % app['docid']) print('%d == %d ?' % (app.get('details').get('appDetails')['versionCode'], details.get('details').get('appDetails') ['versionCode'])) if app.get('details').get( 'appDetails')['versionCode'] != details.get( 'details').get('appDetails')['versionCode']: toUpdate.append(details) return {'status': 'SUCCESS', 'message': toUpdate} def remove_local_app(self, docid): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} # get app from cache app = list(filter(lambda x: x['docid'] == docid, self.currentSet)) if len(app) < 1: return {'status': 'ERROR'} apkPath = os.path.join(self.download_path, app[0]['filename']) if os.path.isfile(apkPath): os.remove(apkPath) self.currentSet.remove(app[0]) return {'status': 'SUCCESS'} return {'status': 'ERROR'}
class GPlaycli(object): def __init__(self, credentials=None, proxies=None, device='bacon', locale=None): # no config file given, look for one if credentials is None: # default local user configs cred_paths_list = [ 'gplaycli.conf', os.path.expanduser("~") + '/.config/gplaycli/gplaycli.conf', '/etc/gplaycli/gplaycli.conf' ] tmp_list = list(cred_paths_list) while not os.path.isfile(tmp_list[0]): tmp_list.pop(0) if not tmp_list: raise OSError("No configuration file found at %s" % cred_paths_list) credentials = tmp_list[0] self.proxies = proxies default_values = dict() self.configparser = configparser.ConfigParser(default_values) self.configparser.read(credentials) self.config = {key: value for key, value in self.configparser.items("Credentials")} self.tokencachefile = os.path.expanduser(self.configparser.get("Cache", "token")) self.playstore_api = None self.token_enable = True self.token_url = self.configparser.get('Credentials', 'token_url') self.token, self.gsfid = self.retrieve_token() # default settings, ie for API calls self.yes = False self.verbose = False logging.basicConfig() self.progress_bar = False self.logging_enable = False self.device_codename = device self.locale = locale self.addfiles_enable = False def get_cached_token(self): try: with open(self.tokencachefile, 'r') as tcf: token, gsfid = tcf.readline().split() if not token: token = None gsfid = None except (IOError, ValueError): # cache file does not exists or is corrupted token = None gsfid = None return token, gsfid def write_cached_token(self, token, gsfid): try: # creates cachedir if not exists cachedir = os.path.dirname(self.tokencachefile) if not os.path.exists(cachedir): os.mkdir(cachedir) with open(self.tokencachefile, 'w') as tcf: tcf.write("%s %s" % (token, gsfid)) except IOError as error: raise IOError("Failed to write token to cache file: %s %s" % (self.tokencachefile, error.strerror)) def retrieve_token(self, force_new=False): token, gsfid = self.get_cached_token() if token is not None and not force_new: logging.info("Using cached token.") return token, gsfid logging.info("Retrieving token ...") resp = requests.get(self.token_url) if resp.text == 'Auth error': print('Token dispenser auth error, probably too many connections') sys.exit(ERRORS.TOKEN_DISPENSER_AUTH_ERROR) elif resp.text == "Server error": print('Token dispenser server error') sys.exit(ERRORS.TOKEN_DISPENSER_SERVER_ERROR) token, gsfid = resp.text.split(" ") self.token = token self.gsfid = gsfid self.write_cached_token(token, gsfid) return token, gsfid def set_download_folder(self, folder): self.config["download_folder_path"] = folder def connect_to_googleplay_api(self): if self.locale: self.playstore_api = GooglePlayAPI( device_codename=self.device_codename, proxies_config=self.proxies, locale=self.locale, timezone=None ) else: self.playstore_api = GooglePlayAPI( device_codename=self.device_codename, proxies_config=self.proxies, timezone=None ) error = None email = None password = None auth_sub_token = None gsf_id = None if self.token_enable is False: logging.info("Using credentials to connect to API") email = self.config["gmail_address"] if self.config["gmail_password"]: logging.info("Using plaintext password") password = self.config["gmail_password"] elif self.config["keyring_service"] and HAVE_KEYRING is True: password = keyring.get_password(self.config["keyring_service"], email) elif self.config["keyring_service"] and HAVE_KEYRING is False: print("You asked for keyring service but keyring package is not installed") sys.exit(ERRORS.KEYRING_NOT_INSTALLED) else: logging.info("Using token to connect to API") auth_sub_token = self.token gsf_id = int(self.gsfid, 16) try: self.playstore_api.login(email=email, password=password, authSubToken=auth_sub_token, gsfId=gsf_id) except (ValueError, IndexError, LoginError, GoogleDecodeError): # invalid token or expired logging.info("Token has expired or is invalid. Retrieving a new one...") self.retrieve_token(force_new=True) self.playstore_api.login(authSubToken=self.token, gsfId=int(self.gsfid, 16)) success = True return success, error def download_pkg(self, pkg, version): # Check for download folder download_folder_path = self.config["download_folder_path"] if not os.path.isdir(download_folder_path): os.mkdir(download_folder_path) #Download try: data_dict = self.playstore_api.download(pkg, version) except IndexError as exc: print("Error while downloading %s : %s" % (pkg, "this package does not exist, " "try to search it via --search before")) return False, None except LoginError: self.retrieve_token(force_new=True) self.playstore_api.login(authSubToken=self.token, gsfId=int(self.gsfid, 16)) try: data_dict = self.playstore_api.download(pkg, version) except IndexError as exc: print("Error while downloading %s : %s" % (pkg, "this package does not exist, " "try to search it via --search before")) return False, None except Exception as exc: print("Error while downloading %s : %s" % (pkg, exc)) return False, None except Exception as exc: print("Error while downloading %s : %s" % (pkg, exc)) return False, None else: filename = pkg + ".apk" filepath = os.path.join(download_folder_path, filename) data = data_dict['data'] additional_data = data_dict['additionalData'] try: open(filepath, "wb").write(data) if additional_data: for obb_file in additional_data: obb_filename = "%s.%s.%s.obb" % (obb_file["type"], obb_file["versionCode"], data_dict["docId"]) obb_filename = os.path.join(download_folder_path, obb_filename) open(obb_filename, "wb").write(obb_file["data"]) except IOError as exc: print("Error while writing %s : %s" % (pkg, exc)) return True, filepath def raw_search(self, search_string, nb_results): # Query results return self.playstore_api.search(search_string, nb_result=nb_results) def search(self, search_string, nb_results=1, free_only=True): try: results = self.raw_search(search_string, nb_results) except IndexError: results = list() except LoginError: self.retrieve_token(force_new=True) self.playstore_api.login(authSubToken=self.token, gsfId=int(self.gsfid, 16)) try: results = self.raw_search(search_string, nb_results) except IndexError: results = list() logging.info(results) if not results: print("No result") return all_results = list() # Compute results values for result in results: if free_only and result['offer'][0]['checkoutFlowRequired']: # if not Free to download continue entry = {"title": result["title"], "creator": result['author'], "size": util.sizeof_fmt(result['installationSize']), "downloads": result['numDownloads'], "last_update": result['uploadDate'], "app_id": result['docId'], "version": result['versionCode'], "rating": "%.2f" % result["aggregateRating"]["starRating"], "paid": result['offer'][0]['checkoutFlowRequired'], "stable": not result["unstable"]} all_results.append(entry) for result in all_results: if result["app_id"] == search_string: return result return "NOT_AT_PLAY"
server = GooglePlayAPI('it_IT', 'Europe/Rome') # LOGIN print('\nLogging in with email and password\n') server.login(args.email, args.password, None, None) gsfId = server.gsfId authSubToken = server.authSubToken print('\nNow trying secondary login with ac2dm token and gsfId saved\n') server = GooglePlayAPI('it_IT', 'Europe/Rome') server.login(None, None, gsfId, authSubToken) # SEARCH apps = server.search('telegram', 34, None) print('\nSearch suggestion for "fir"\n') print(server.searchSuggest('fir')) print('nb_result: 34') print('number of results: %d' % len(apps)) print('\nFound those apps:\n') for a in apps: print(a['docId']) # HOME APPS print('\nFetching apps from play store home\n') home = server.getHomeApps()
class Play(object): def __init__(self, debug=True, fdroid=False): self.currentSet = [] self.totalNumOfApps = 0 self.debug = debug self.fdroid = fdroid self.firstRun = True self.loggedIn = False self._email = None self._passwd = None self._last_fdroid_update = None # configuring download folder if self.fdroid: self.download_path = os.path.join(os.getcwd(), 'repo') else: self.download_path = os.getcwd() # configuring fdroid data if self.fdroid: self.fdroid_exe = 'fdroid' self.fdroid_path = os.getcwd() self.fdroid_init() self.service = GooglePlayAPI(self.debug) def fdroid_init(self): found = False for path in os.environ['PATH'].split(':'): exe = os.path.join(path, self.fdroid_exe) if os.path.isfile(exe): found = True break if not found: print('Please install fdroid') sys.exit(1) elif os.path.isfile('./config.py'): print('Repo already initalized, skipping') else: p = Popen([self.fdroid_exe, 'init', '-v'], stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: sys.stderr.write("error initializing fdroid repository " + stderr.decode('utf-8')) sys.exit(1) # backup config.py if self.debug: print('Backing up config.py') move('./config.py', './config-backup.py') with open('./config-backup.py') as f1: content = f1.readlines() # copy all content of backup in the main config.py # if the file was not modified with custom values, do it with open('./config.py', 'w') as f: modified = False for line in content: if '# playmaker' in line: modified = True f.write(line) if not modified: if self.debug: print('Appending playmaker data to config.py') f.write('\n# playmaker\nrepo_name = "playmaker"\n' 'repo_description = "repository managed with ' 'playmaker https://github.com/NoMore201/playmaker"\n') os.chmod('./config.py', 0o600) # ensure all folder and files are setup p = Popen([self.fdroid_exe, 'update', '--create-key', '-v'], stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: sys.stderr.write("error initializing fdroid repository " + stderr.decode('utf-8')) else: print('Fdroid repo initialized successfully') def get_last_fdroid_update(self): return {'status': 'SUCCESS', 'message': str(self._last_fdroid_update)} def fdroid_update(self): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} if self.fdroid: try: p = Popen([self.fdroid_exe, 'update', '-c', '--clean'], stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: sys.stderr.write("error updating fdroid repository " + stderr.decode('utf-8')) return makeError(FDROID_ERR) else: print('Fdroid repo updated successfully') self._last_fdroid_update = dt.today().replace( microsecond=0) return {'status': 'SUCCESS'} except Exception as e: return makeError(FDROID_ERR) else: return {'status': 'SUCCESS'} def get_apps(self): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} if self.firstRun: return { 'status': 'PENDING', 'total': self.totalNumOfApps, 'current': len(self.currentSet) } return { 'status': 'SUCCESS', 'message': sorted(self.currentSet, key=lambda k: k['title']) } def login(self, email=None, password=None): def unpad(s): return s[:-ord(s[len(s) - 1:])] try: if email is not None and password is not None: self._email = base64.b64decode(email).decode('utf-8') self._passwd = base64.b64decode(password).decode('utf-8') self.service.login(self._email, self._passwd, None, None) else: # otherwise we need only to refresh auth token encrypted = self.service.encrypt_password( self._email, self._passwd).decode('utf-8') self.service.getAuthSubToken(self._email, encrypted) self.loggedIn = True return {'status': 'SUCCESS', 'message': 'OK'} except LoginError as e: print('Wrong credentials: {0}'.format(e)) return {'status': 'ERROR', 'message': 'Wrong credentials'} except RequestError as e: # probably tokens are invalid, so it is better to # invalidate them print(e) return { 'status': 'ERROR', 'message': 'Request error, probably invalid token' } def update_state(self): print('Updating cache') with concurrent.futures.ProcessPoolExecutor() as executor: # get application ids from apk files apkFiles = [ apk for apk in os.listdir(self.download_path) if os.path.splitext(apk)[1] == '.apk' ] self.totalNumOfApps = len(apkFiles) if self.totalNumOfApps != 0: future_to_app = [ executor.submit(get_details_from_apk, a, self.download_path, self.service) for a in apkFiles ] for future in concurrent.futures.as_completed(future_to_app): app = future.result() if app is not None: self.currentSet.append(app) print('Cache correctly initialized') self.firstRun = False def insert_app_into_state(self, newApp): found = False result = list( filter(lambda x: x['docId'] == newApp['docId'], self.currentSet)) if len(result) > 0: found = True if self.debug: print('%s is already cached, updating..' % newApp['docId']) i = self.currentSet.index(result[0]) self.currentSet[i] = newApp if not found: if self.debug: print('Adding %s into cache..' % newApp['docId']) self.currentSet.append(newApp) def search(self, appName, numItems=15): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} try: apps = self.service.search(appName, numItems, None) except RequestError as e: print(e) self.loggedIn = False return {'status': 'ERROR', 'message': SESSION_EXPIRED_ERR} except LoginError as e: print(SESSION_EXPIRED_ERR) self.loggedIn = False except IndexError as e: print(SESSION_EXPIRED_ERR) self.loggedIn = False return {'status': 'SUCCESS', 'message': apps} def details(self, app): try: details = self.service.details(app) except RequestError: details = None return details def get_bulk_details(self, apksList): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} try: apps = [self.details(a) for a in apksList] except LoginError as e: print(e) self.loggedIn = False return apps def download_selection(self, appNames): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} success = [] failed = [] unavail = [] for app in appNames: details = self.details(app) if details is None: print('Package %s does not exits' % app) unavail.append(app) continue print('Downloading %s' % app) try: if details['offer'][0]['formattedAmount'] == 'Free': data = self.service.download(app, details['versionCode']) else: data = self.service.delivery(app, details['versionCode']) except IndexError as exc: print(exc) print('Package %s does not exists' % app) unavail.append(app) except Exception as exc: print(exc) print('Failed to download %s' % app) failed.append(app) else: filename = app + '.apk' filepath = os.path.join(self.download_path, filename) try: open(filepath, 'wb').write(data['data']) except IOError as exc: print('Error while writing %s: %s' % (filename, exc)) failed.append(app) details['filename'] = filename success.append(details) for x in success: self.insert_app_into_state(x) return { 'status': 'SUCCESS', 'message': { 'success': success, 'failed': failed, 'unavail': unavail } } def check_local_apks(self): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} if len(self.currentSet) == 0: print('There is no package') return {'status': 'SUCCESS', 'message': []} else: toUpdate = [] for app in self.currentSet: details = self.details(app['docId']) if details is None: print('%s not available in Play Store' % app['docId']) continue if self.debug: print('Checking %s' % app['docId']) print('%d == %d ?' % (app['versionCode'], details['versionCode'])) if app['versionCode'] != details['versionCode']: toUpdate.append(details['docId']) return {'status': 'SUCCESS', 'message': toUpdate} def remove_local_app(self, docId): if not self.loggedIn: return {'status': 'UNAUTHORIZED'} # get app from cache app = list(filter(lambda x: x['docId'] == docId, self.currentSet)) if len(app) < 1: return {'status': 'ERROR'} apkPath = os.path.join(self.download_path, app[0]['filename']) if os.path.isfile(apkPath): os.remove(apkPath) self.currentSet.remove(app[0]) return {'status': 'SUCCESS'} return {'status': 'ERROR'}
class Play(object): def __init__(self, debug=True, fdroid=False): self.currentSet = [] self.debug = debug self.fdroid = fdroid self.loggedIn = False self.firstRun = True # configuring download folder if self.fdroid: self.download_path = os.path.join(os.getcwd(), 'repo') else: self.download_path = os.getcwd() # configuring fdroid data if self.fdroid: self.fdroid_exe = 'fdroid' self.fdroid_path = os.getcwd() self.fdroid_init() self.service = GooglePlayAPI(self.debug) def fdroid_init(self): found = False for path in ['/usr/bin', '/usr/local/bin']: exe = os.path.join(path, self.fdroid_exe) if os.path.isfile(exe): found = True if not found: print('Please install fdroid') sys.exit(1) elif os.path.isfile('./config.py'): print('Repo already initalized, skipping') else: p = Popen([self.fdroid_exe, 'init'], stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: sys.stderr.write("error initializing fdroid repository " + stderr.decode('utf-8')) sys.exit(1) # ensure all folder and files are setup p = Popen([self.fdroid_exe, 'update', '--create-key'], stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: sys.stderr.write("error initializing fdroid repository " + stderr.decode('utf-8')) else: print('Fdroid repo initialized successfully') def fdroid_update(self): if not self.loggedIn: return makeError(NOT_LOGGED_IN_ERR) if self.fdroid: try: p = Popen([self.fdroid_exe, 'update', '-c', '--clean'], stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: sys.stderr.write("error updating fdroid repository " + stderr.decode('utf-8')) return makeError(FDROID_ERR) else: print('Fdroid repo updated successfully') return {'status': 'SUCCESS'} except: return makeError(FDROID_ERR) else: return {'status': 'SUCCESS'} def get_apps(self): if self.firstRun: return {'status': 'PENDING'} return { 'status': 'SUCCESS', 'message': sorted(self.currentSet, key=lambda k: k['title']) } def login(self, ciphertext, hashToB64): def unpad(s): return s[:-ord(s[len(s) - 1:])] try: cipher = base64.b64decode(ciphertext) passwd = base64.b64decode(hashToB64) # first 16 bytes corresponds to the init vector iv = cipher[0:16] cipher = cipher[16:] aes = AES.new(passwd, AES.MODE_CBC, iv) result = unpad(aes.decrypt(cipher)).split(b'\x00') email = result[0].decode('utf-8') password = result[1].decode('utf-8') self.service.login(email, password, None, None) self.loggedIn = True return {'status': 'SUCCESS', 'message': 'OK'} except LoginError as e: print('Wrong credentials') self.loggedIn = False return {'status': 'ERROR', 'message': 'Wrong credentials'} except RequestError as e: # probably tokens are invalid, so it is better to # invalidate them print('Request error, probably invalid token') self.loggedIn = False return { 'status': 'ERROR', 'message': 'Request error, probably invalid token' } def update_state(self): def get_details_from_apk(details): filepath = os.path.join(self.download_path, details['docId'] + '.apk') a = APK(filepath) details['versionCode'] = int(a.version_code) return details def fetch_details_for_local_apps(): # get application ids from apk files appList = [ os.path.splitext(apk)[0] for apk in os.listdir(self.download_path) if os.path.splitext(apk)[1] == '.apk' ] toReturn = [] if len(appList) > 0: details = self.get_bulk_details(appList) executor = concurrent.futures.ThreadPoolExecutor(max_workers=2) futures = { executor.submit(get_details_from_apk, app): app for app in details } for future in concurrent.futures.as_completed(futures): app = future.result() toReturn.append(app) if self.debug: print('Added %s to cache' % app['docId']) return toReturn print('Updating cache') self.currentSet = fetch_details_for_local_apps() self.firstRun = False def insert_app_into_state(self, newApp): found = False result = filter(lambda x: x['docId'] == newApp['docId'], self.currentSet) result = list(result) if len(result) > 0: found = True if self.debug: print('%s is already cached, updating..' % newApp['docId']) i = self.currentSet.index(result[0]) self.currentSet[i] = newApp if not found: if self.debug: print('Adding %s into cache..' % newApp['docId']) self.currentSet.append(newApp) def search(self, appName, numItems=15): if not self.loggedIn: return {'status': 'ERROR', 'message': NOT_LOGGED_IN_ERR} try: apps = self.service.search(appName, numItems, None) except RequestError as e: print(SESSION_EXPIRED_ERR) self.loggedIn = False return {'status': 'ERROR', 'message': SESSION_EXPIRED_ERR} return {'status': 'SUCCESS', 'message': apps} def get_bulk_details(self, apksList): if not self.loggedIn: return {'status': 'ERROR', 'message': NOT_LOGGED_IN_ERR} try: apps = self.service.bulkDetails(apksList) except RequestError as e: print(SESSION_EXPIRED_ERR) self.loggedIn = False return {'status': 'ERROR', 'message': SESSION_EXPIRED_ERR} return apps def download_selection(self, appNames): if not self.loggedIn: return {'status': 'ERROR', 'error': NOT_LOGGED_IN_ERR} success = [] failed = [] unavail = [] details = self.get_bulk_details(appNames) for appname, appdetails in zip(appNames, details): if appdetails['docId'] == '': print('Package does not exits') unavail.append(appname) continue print('Downloading %s' % appname) try: if appdetails['offer'][0]['formattedAmount'] == 'Free': data = self.service.download(appname, appdetails['versionCode']) else: data = self.service.delivery(appname, appdetails['versionCode']) success.append(appdetails) print('Done!') except IndexError as exc: print(exc) print('Package %s does not exists' % appname) unavail.append(appname) except Exception as exc: print(exc) print('Failed to download %s' % appname) failed.append(appname) else: filename = appname + '.apk' filepath = os.path.join(self.download_path, filename) try: open(filepath, 'wb').write(data) except IOError as exc: print('Error while writing %s: %s' % (filename, exc)) failed.append(appname) for x in success: self.insert_app_into_state(x) return { 'status': 'SUCCESS', 'message': { 'success': success, 'failed': failed, 'unavail': unavail } } def check_local_apks(self): if not self.loggedIn: return {'status': 'ERROR', 'error': NOT_LOGGED_IN_ERR} localDetails = self.currentSet onlineDetails = self.get_bulk_details( [app['docId'] for app in localDetails]) if len(localDetails) == 0 or len(onlineDetails) == 0: print('There is no package locally') return {'status': 'SUCCESS', 'message': []} else: toUpdate = [] for local, online in zip(localDetails, onlineDetails): if self.debug: print('Checking %s' % local['docId']) print('%d == %d ?' % (local['versionCode'], online['versionCode'])) if local['versionCode'] != online['versionCode']: toUpdate.append(online['docId']) return {'status': 'SUCCESS', 'message': toUpdate} def remove_local_app(self, appName): apkName = appName + '.apk' apkPath = os.path.join(self.download_path, apkName) if os.path.isfile(apkPath): os.remove(apkPath) for pos, app in enumerate(self.currentSet): if app['docId'] == appName: del self.currentSet[pos] return {'status': 'SUCCESS'} return {'status': 'ERROR'}
import os gsfId = int(os.environ["GPAPI_GSFID"]) authSubToken = os.environ["GPAPI_TOKEN"] server = GooglePlayAPI("it_IT", "Europe/Rome") # LOGIN print("\nLogin with ac2dm token and gsfId saved\n") server.login(None, None, gsfId, authSubToken) # SEARCH print("\nSearch suggestion for \"fir\"\n") print(server.searchSuggest("fir")) result = server.search("firefox") for doc in result: if 'docid' in doc: print("doc: {}".format(doc["docid"])) for cluster in doc["child"]: print("\tcluster: {}".format(cluster["docid"])) for app in cluster["child"]: print("\t\tapp: {}".format(app["docid"])) # HOME APPS print("\nFetching apps from play store home\n") result = server.home() for cluster in result: print("cluster: {}".format(cluster.get("docid"))) for app in cluster.get("child"): print("\tapp: {}".format(app.get("docid")))
print "Usage: %s request [nb_results] [offset]" % sys.argv[0] print "Search for an app." print "If request contains a space, don't forget to surround it with \"\"" sys.exit(0) request = sys.argv[1] nb_res = 20 offset = None if (len(sys.argv) >= 3): nb_res = int(sys.argv[2]) if (len(sys.argv) >= 4): offset = int(sys.argv[3]) server.login(GOOGLE_LOGIN, GOOGLE_PASSWORD, None, None) apps = server.search(request, nb_res, offset) # print('\nSearch suggestion for "fir"\n') # print(server.searchSuggest('fir')) print('nb_result: %d' % nb_res) print('number of results: %d' % len(apps)) print('\nFound those apps:\n') for a in apps: print("Title: " + a['title'] + ", PackageName: " + a['docId'] + ", Author: " + a['author']) # {'files': [{'fileType': 0, 'version': 7, 'size': 23246144L}], 'recentChanges': u'', 'docId': u'com.unity.unitystagingproject', 'description': u'', 'title': u'UDPgame', 'author': u'Stone Shi', 'containsAds': u'', 'versionString': u'', 'aggregateRating': {'commentCount': 0L, 'fourStarRatings': 0L, 'oneStarRatings': 0L, 'twoStarRatings': 0L, 'fiveStarRatings': 0L, 'type': 2, 'starRating': 0.0, 'threeStarRatings': 0L, 'ratingsCount': 0L}, 'versionCode': 7, 'offer': [{'offerType': 1, 'micros': 0L, 'currencyCode': u'USD', 'formattedAmount': u'', 'saleEnds': u'', 'checkoutFlowRequired': False}], 'installationSize': 23246144L, 'unstable': True, 'uploadDate': u'24 lug 2018', 'dependencies': [], 'detailsUrl': u'details?doc=com.unity.unitystagingproject', 'images': [{'url': u'https://lh3.googleusercontent.com/IqqRcOiztNHJKnJlrie6b0x1cFY3otyT6aVrmsendF04tboly4BEIf8cUT8S8G1yBaQ', 'width': 512, 'supportsFifeUrlOptions': True, 'imageType': 4, 'height': 512}, {'url': u'https://lh3.googleusercontent.com/V1pJi6Qx5FbvjybKYnBXZQF_zbrNxksp_dld9GTLKJq5o6yNmkFKubWL20oiTrabOCo', 'width': 1024, 'supportsFifeUrlOptions': True, 'imageType': 2, 'height': 500}], 'permission': [], 'category': {'appCategory': u'GAME_ROLE_PLAYING', 'appType': u'GAME'}, 'numDownloads': u'Pi\xf9 di 0 di download'}