Example #1
0
 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
Example #2
0
    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
Example #3
0
 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
Example #4
0
 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")
Example #7
0
    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
Example #8
0
    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
Example #9
0
    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
Example #10
0
    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)",
                )