def test_download_bad_package_name(self, playstore, download_folder_path): meta = PackageMeta(playstore, BAD_PACKAGE_NAME) result = playstore.download( meta, OutDir(download_folder_path, meta=meta), ) assert result is False
def test_download_corrupted_apk(self, playstore, download_folder_path, monkeypatch): meta = PackageMeta(playstore, VALID_PACKAGE_NAME) def raise_exception(*args, **kwargs): raise ChunkedEncodingError() monkeypatch.setattr(Util, "show_list_progress", raise_exception) # Mock the function that gets the size of the file so that the downloaded # apk will be treated as corrupted. monkeypatch.setattr(os.path, "getsize", lambda x: 1) # Simulate an error with the file deletion. # noinspection PyUnusedLocal def raise_os_error(ignore): raise OSError monkeypatch.setattr(os, "remove", raise_os_error) result = playstore.download( meta, OutDir(download_folder_path, meta=meta), ) assert result is False
def test_download_valid_package_name(self, playstore, download_folder_path): meta = PackageMeta(playstore, VALID_PACKAGE_NAME) result = playstore.download( meta, OutDir(download_folder_path, meta=meta), show_progress_bar=True, ) assert result is True
def test_download_response_error(self, playstore, monkeypatch, download_folder_path): # Simulate a bad response from the server. monkeypatch.setattr(Playstore, "protobuf_to_dict", lambda: {}) meta = PackageMeta(playstore, VALID_PACKAGE_NAME) result = playstore.download( meta, OutDir(download_folder_path, meta=meta), ) assert result is False
def download(self, package_name): meta = PackageMeta( api=self.api, package_name=package_name.strip(" '\""), ) out_dir = OutDir(self.out, tag=self.tag, meta=meta) result = self.api.download( meta=meta, out_dir=out_dir, download_obb=self.blobs, download_split_apks=self.split_apks, ) return DownloadResult(result)
def on_start_download(package_name): if package_name_regex.match(package_name): try: api = Playstore(credentials_location) meta = PackageMeta(api, package_name) try: app = meta.app_details().docV2 except AttributeError: emit( "download_bad_package", f"Unable to retrieve application with " f"package name '{package_name}'", ) return details = { "package_name": app.docid, "title": app.title, "creator": app.creator, } downloaded_apk_file_path = os.path.join( downloaded_apk_location, re.sub( r"[^\w\-_.\s]", "_", f"{details['title']} by {details['creator']} - " f"{details['package_name']}.apk", ), ) # noinspection PyProtectedMember for progress in api._download_with_progress( meta, OutDir(downloaded_apk_file_path, meta=meta), ): emit("download_progress", progress) logger.info(f"The application was downloaded and " f"saved to '{downloaded_apk_file_path}'") emit("download_success", "The application was successfully downloaded") except Exception as e: emit("download_error", str(e)) else: emit("download_error", "Please specify a valid package name")
def test_download_cookie_error(self, playstore, monkeypatch, download_folder_path): # noinspection PyProtectedMember original = Playstore._execute_request def mock(*args, **kwargs): to_return = original(*args, **kwargs) del to_return.payload.deliveryResponse.appDeliveryData.downloadAuthCookie[:] del to_return.payload.buyResponse.purchaseStatusResponse.appDeliveryData.downloadAuthCookie[:] return to_return # Simulate a bad response from the server. monkeypatch.setattr(Playstore, "_execute_request", mock) meta = PackageMeta(playstore, VALID_PACKAGE_NAME) result = playstore.download( meta, OutDir(download_folder_path, meta=meta), ) assert result is False
def test_download_corrupted_obb(self, playstore, download_folder_path, monkeypatch): original = Util.show_list_progress meta = PackageMeta(playstore, APK_WITH_OBB) def raise_exception(*args, **kwargs): if " .obb ".lower() not in kwargs["description"].lower(): return original(*args, **kwargs) else: raise ChunkedEncodingError() monkeypatch.setattr(Util, "show_list_progress", raise_exception) result = playstore.download( meta, OutDir(download_folder_path, meta=meta), download_obb=True, show_progress_bar=False, ) assert result is False
def test_download_response_error_2(self, playstore, monkeypatch, download_folder_path): # noinspection PyProtectedMember original = Playstore._execute_request def mock(*args, **kwargs): if mock.counter < 1: mock.counter += 1 return original(*args, **kwargs) else: return playstore_protobuf.DocV2() mock.counter = 0 # Simulate a bad response from the server. monkeypatch.setattr(Playstore, "_execute_request", mock) meta = PackageMeta(playstore, VALID_PACKAGE_NAME) result = playstore.download( meta, OutDir(download_folder_path, meta=meta), ) assert result is False
def _download_with_progress( self, meta: PackageMeta, out_dir: OutDir, download_obb: bool = False, download_split_apks: 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 meta: PackageMeta object containing data about the app. :param out_dir: OutDir object containing 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 download_split_apks: Flag indicating whether to also download the additional split apks 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. """ def _handle_missing_payload(res, pkg): # If the query went completely wrong. if "payload" not in self.protobuf_to_dict(res): try: self.logger.error(f"Error for app '{pkg}': " f"{res.commands.displayErrorMessage}") raise RuntimeError(f"Error for app '{pkg}': " f"{res.commands.displayErrorMessage}") except AttributeError: self.logger.error( "There was an error when requesting the download link " f"for app '{pkg}'") raise RuntimeError( "Unable to download the application, please see the logs for more " "information") version_code = meta.docV2.details.appDetails.versionCode offer_type = meta.docV2.offer[0].offerType # Check if the app was already downloaded by this account. path = "delivery" query = { "ot": offer_type, "doc": meta.docV2.docid, "vc": version_code, } response = self._execute_request(path, query) _handle_missing_payload(response, meta.package_name) delivery_data = response.payload.deliveryResponse.appDeliveryData if not delivery_data.downloadUrl: # The app doesn't belong to the account, so it has to be added to the # account first. path = "purchase" response = self._execute_request(path, data=query) _handle_missing_payload(response, meta.package_name) delivery_data = (response.payload.buyResponse. purchaseStatusResponse.appDeliveryData) download_token = response.payload.buyResponse.downloadToken if not self.protobuf_to_dict(delivery_data) and download_token: path = "delivery" query["dtok"] = download_token response = self._execute_request(path, query) _handle_missing_payload(response, meta.package_name) delivery_data = response.payload.deliveryResponse.appDeliveryData # The url where to download the apk file. temp_url = delivery_data.downloadUrl # Additional files (.obb) to be downloaded with the application. # https://developer.android.com/google/play/expansion-files additional_files = [ additional_file for additional_file in delivery_data.additionalFile ] # Additional split apk(s) to be downloaded with the application. # https://developer.android.com/guide/app-bundle/dynamic-delivery split_apks = [split_apk for split_apk in delivery_data.split] try: cookie = delivery_data.downloadAuthCookie[0] except IndexError: self.logger.error( f"DownloadAuthCookie was not received for '{meta.package_name}'" ) raise RuntimeError( f"DownloadAuthCookie was not received for '{meta.package_name}'" ) cookies = {str(cookie.name): str(cookie.value)} headers = { "User-Agent": "AndroidDownloadManager/8.0.0 (Linux; U; Android 8.0.0; " "STF-L09 Build/HUAWEISTF-L09)", "Accept-Encoding": "", } # Execute another request to get the actual apk file. response = requests.get(temp_url, headers=headers, cookies=cookies, verify=True, stream=True) yield from self._download_single_file( out_dir.apk_path, response, show_progress_bar, f"Downloading {meta.package_name}", "Unable to download the entire application", ) # NOTE: expansion files (OBBs) will no longer be supported for new apps. # https://android-developers.googleblog.com/2020/11/new-android-app-bundle-and-target-api.html 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, ) obb_file_name = out_dir.obb_path(obb) yield from self._download_single_file( obb_file_name, response, show_progress_bar, f"Downloading additional .obb file for {meta.package_name}", "Unable to download completely the additional .obb file(s)", ) if download_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, ) split_apk_file_name = out_dir.split_apk_path(split_apk) yield from self._download_single_file( split_apk_file_name, response, show_progress_bar, f"Downloading split apk for {meta.package_name}", "Unable to download completely the additional split apk file(s)", )