def _download_with_progress( self, package_name: str, file_name: str = None, download_obb: bool = False, show_progress_bar: bool = False, ) -> Iterable[int]: """ Internal method to download a certain app (identified by the package name) from the Google Play Store and report the progress (using a generator that reports the download progress in the range 0-100). :param package_name: The package name of the app (e.g., "com.example.myapp"). :param file_name: The location where to save the downloaded app (by default "package_name.apk"). :param download_obb: Flag indicating whether to also download the additional .obb files for an application (if any). :param show_progress_bar: Flag indicating whether to show a progress bar in the terminal during the download of the file(s). :return: A generator that returns the download progress (0-100) at each iteration. """ # Set the default file name if none is provided. if not file_name: file_name = "{0}.apk".format(package_name) # Get the app details before downloading it. details = self.app_details(package_name) if details is None: self.logger.error( "Can't proceed with the download: there was an error when " "requesting details for app '{0}''".format(package_name)) raise RuntimeError( "Can't proceed with the download: there was an error when " "requesting details for app '{0}''".format(package_name)) version_code = details.docV2.details.appDetails.versionCode offer_type = details.docV2.offer[0].offerType # Check if the app was already downloaded by this account. path = "delivery" query = {"ot": offer_type, "doc": package_name, "vc": version_code} response = self._execute_request(path, query) if response.payload.deliveryResponse.appDeliveryData.downloadUrl: # The app already belongs to the account. temp_url = response.payload.deliveryResponse.appDeliveryData.downloadUrl cookie = response.payload.deliveryResponse.appDeliveryData.downloadAuthCookie[ 0] additional_files = [ additional_file for additional_file in response.payload. deliveryResponse.appDeliveryData.additionalFile ] split_apks = ([ split_apk for split_apk in response.payload.deliveryResponse.appDeliveryData.split ] if response.payload.deliveryResponse.appDeliveryData.split else None) else: # The app doesn't belong to the account, so it has to be added to the account. path = "purchase" data = "ot={0}&doc={1}&vc={2}".format(offer_type, package_name, version_code) # Execute the first query to get the download link. response = self._execute_request(path, data=data) # If the query went completely wrong. if "payload" not in self.protobuf_to_dict(response): try: self.logger.error("Error for app '{0}': {1}".format( package_name, response.commands.displayErrorMessage)) raise RuntimeError("Error for app '{0}': {1}".format( package_name, response.commands.displayErrorMessage)) except AttributeError: self.logger.error( "There was an error when requesting the download link " "for app '{0}'".format(package_name)) raise RuntimeError( "Unable to download the application, please see the logs for more information" ) else: # The url where to download the apk file. temp_url = (response.payload.buyResponse. purchaseStatusResponse.appDeliveryData.downloadUrl) # Additional files (.obb) to be downloaded with the application. additional_files = [ additional_file for additional_file in response.payload.buyResponse. purchaseStatusResponse.appDeliveryData.additionalFile ] # Additional split apk(s) to be downloaded with the application. split_apks = ([ split_apk for split_apk in response.payload.deliveryResponse.appDeliveryData.split ] if response.payload.deliveryResponse.appDeliveryData.split else None) try: cookie = response.payload.buyResponse.purchaseStatusResponse.appDeliveryData.downloadAuthCookie[ 0] except IndexError: self.logger.error( "DownloadAuthCookie was not received for '{0}'".format( package_name)) raise RuntimeError( "DownloadAuthCookie was not received for '{0}'".format( package_name)) cookies = {str(cookie.name): str(cookie.value)} headers = { "User-Agent": "AndroidDownloadManager/4.1.1 (Linux; U; Android 4.1.1; Nexus S Build/JRO03E)", "Accept-Encoding": "", } # Execute another query to get the actual apk file. response = requests.get(temp_url, headers=headers, cookies=cookies, verify=True, stream=True) chunk_size = 1024 apk_size = int(response.headers["content-length"]) # Download the apk file and save it, yielding the progress (in the range 0-100). try: with open(file_name, "wb") as f: last_progress = 0 for index, chunk in enumerate( Util.show_list_progress( response.iter_content(chunk_size=chunk_size), interactive=show_progress_bar, unit=" KB", total=(apk_size // chunk_size), description="Downloading {0}".format(package_name), )): current_progress = 100 * index * chunk_size // apk_size if current_progress > last_progress: last_progress = current_progress yield last_progress if chunk: f.write(chunk) f.flush() # Download complete. yield 100 except ChunkedEncodingError: # There was an error during the download so not all the file was written to disk, hence there will # be a mismatch between the expected size and the actual size of the downloaded file, but the next # code block will handle that. pass # Check if the entire apk was downloaded correctly, otherwise raise an exception. if not self._check_entire_file_downloaded(apk_size, file_name): raise RuntimeError("Unable to download the entire application") if split_apks: # Save the split apk(s) for this application. for split_apk in split_apks: # Execute another query to get the actual file. response = requests.get( split_apk.downloadUrl, headers=headers, cookies=cookies, verify=True, stream=True, ) chunk_size = 1024 file_size = int(response.headers["content-length"]) split_apk_file_name = os.path.join( os.path.dirname(file_name), "{0}.{1}.{2}.apk".format(split_apk.name, version_code, package_name), ) # Download the split apk and save it, yielding the progress (in the range 0-100). try: with open(split_apk_file_name, "wb") as f: last_progress = 0 for index, chunk in enumerate( Util.show_list_progress( response.iter_content( chunk_size=chunk_size), interactive=show_progress_bar, unit=" KB", total=(file_size // chunk_size), description="Downloading split apk for {0}" .format(package_name), )): current_progress = 100 * index * chunk_size // file_size if current_progress > last_progress: last_progress = current_progress yield last_progress if chunk: f.write(chunk) f.flush() # Download complete. yield 100 except ChunkedEncodingError: # There was an error during the download so not all the file was written to disk, hence there will # be a mismatch between the expected size and the actual size of the downloaded file, but the next # code block will handle that. pass # Check if the entire additional file was downloaded correctly, otherwise raise an exception. if not self._check_entire_file_downloaded( file_size, split_apk_file_name): raise RuntimeError( "Unable to download completely the additional split apk file(s)" ) if download_obb: # Save the additional obb files for this application. for obb in additional_files: # Execute another query to get the actual file. response = requests.get( obb.downloadUrl, headers=headers, cookies=cookies, verify=True, stream=True, ) chunk_size = 1024 file_size = int(response.headers["content-length"]) obb_file_name = os.path.join( os.path.dirname(file_name), "{0}.{1}.{2}.obb".format( "main" if obb.fileType == 0 else "patch", obb.versionCode, package_name, ), ) # Download the additional obb file and save it, yielding the progress (in the range 0-100). try: with open(obb_file_name, "wb") as f: last_progress = 0 for index, chunk in enumerate( Util.show_list_progress( response.iter_content( chunk_size=chunk_size), interactive=show_progress_bar, unit=" KB", total=(file_size // chunk_size), description= "Downloading additional obb file for {0}". format(package_name), )): current_progress = 100 * index * chunk_size // file_size if current_progress > last_progress: last_progress = current_progress yield last_progress if chunk: f.write(chunk) f.flush() # Download complete. yield 100 except ChunkedEncodingError: # There was an error during the download so not all the file was written to disk, hence there will # be a mismatch between the expected size and the actual size of the downloaded file, but the next # code block will handle that. pass # Check if the entire additional file was downloaded correctly, otherwise raise an exception. if not self._check_entire_file_downloaded( file_size, obb_file_name): raise RuntimeError( "Unable to download completely the additional obb file(s)" )
def _download_single_file( self, destination_file: str, server_response: requests.Response, show_progress_bar: bool = False, download_str: str = "Downloading file", error_str: str = "Unable to download the entire file", ) -> Iterable[int]: """ Internal method to download a file contained in a server response and save it to a specific destination. :param destination_file: The destination path where to save the downloaded file. :param server_response: The response from the server, containing the content of the file to be saved. :param show_progress_bar: Flag indicating whether to show a progress bar in the terminal during the download of the file. :param download_str: The message to show next to the progress bar during the download of the file :param error_str: The error message of the exception that will be raised if the download of the file fails. :return: A generator that returns the download progress (0-100) at each iteration. """ chunk_size = 1024 file_size = int(server_response.headers["Content-Length"]) # Download the file and save it, yielding the progress (in the range 0-100). try: with open(destination_file, "wb") as f: last_progress = 0 for index, chunk in enumerate( Util.show_list_progress( server_response.iter_content( chunk_size=chunk_size), interactive=show_progress_bar, unit=" KB", total=(file_size // chunk_size), description=download_str, )): current_progress = 100 * index * chunk_size // file_size if current_progress > last_progress: last_progress = current_progress yield last_progress if chunk: f.write(chunk) f.flush() # Download complete. yield 100 except ChunkedEncodingError: # There was an error during the download so not all the file was written # to disk, hence there will be a mismatch between the expected size and # the actual size of the downloaded file, but the next code block will # handle that. pass # Check if the entire file was downloaded correctly, otherwise raise an # exception. if file_size != os.path.getsize(destination_file): self.logger.error( f"Download of '{destination_file}' not completed, please retry, " f"the file '{destination_file}' is corrupted and will be removed" ) try: os.remove(destination_file) except OSError: self.logger.warning( f"The file '{destination_file}' is corrupted and should be " f"removed manually") raise RuntimeError(error_str)