Exemple #1
0
    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)