diff --git a/openandroidinstaller/app_state.py b/openandroidinstaller/app_state.py index bb19afc8..9687c535 100644 --- a/openandroidinstaller/app_state.py +++ b/openandroidinstaller/app_state.py @@ -42,6 +42,7 @@ def __init__( self.config = None self.image_path = None self.recovery_path = None + self.is_ab = None # is this still needed? self.steps = None diff --git a/openandroidinstaller/assets/configs/akari.yaml b/openandroidinstaller/assets/configs/akari.yaml index cc4ff41b..8e971d37 100644 --- a/openandroidinstaller/assets/configs/akari.yaml +++ b/openandroidinstaller/assets/configs/akari.yaml @@ -42,21 +42,13 @@ steps: command: adb_reboot_bootloader - type: call_button content: Flash a custom recovery (temporarily) by pressing 'Confirm and run'. Once it's done continue. - command: fastboot_flash_recovery + command: fastboot_flash_boot - type: call_button command: adb_twrp_copy_partitions content: > In some cases, the inactive slot can be unpopulated or contain much older firmware than the active slot, leading to various issues including a potential hard-brick. We can ensure none of that will happen by copying the contents of the active slot to the inactive slot. Press 'confirm and run' to to this. Once you are in the bootloader again, continue. - type: call_button - command: fastboot_flash_recovery + command: fastboot_flash_boot content: > - Now we need to boot into recovery again. Press run and when you see the TWRP screen you can continue. - install_os: - - type: call_button - content: > - In the next steps, you finally flash the selected OS image. - Connect your device with your computer with the USB-Cable. - This step will format your phone and wipe all the data. It will also remove encryption and delete all files stored - in the internal storage. Then the OS image will be installed. Confirm to run. This might take a while. At the end your phone will boot into the new OS. - command: adb_twrp_wipe_and_install \ No newline at end of file + Now we need to boot into recovery again. Press run and when you see the TWRP screen you can continue. \ No newline at end of file diff --git a/openandroidinstaller/assets/configs/akatsuki.yaml b/openandroidinstaller/assets/configs/akatsuki.yaml index ca23c998..ecafe733 100644 --- a/openandroidinstaller/assets/configs/akatsuki.yaml +++ b/openandroidinstaller/assets/configs/akatsuki.yaml @@ -42,21 +42,13 @@ steps: command: adb_reboot_bootloader - type: call_button content: Flash a custom recovery (temporarily) by pressing 'Confirm and run'. Once it's done continue. - command: fastboot_flash_recovery + command: fastboot_flash_boot - type: call_button command: adb_twrp_copy_partitions content: > In some cases, the inactive slot can be unpopulated or contain much older firmware than the active slot, leading to various issues including a potential hard-brick. We can ensure none of that will happen by copying the contents of the active slot to the inactive slot. Press 'confirm and run' to to this. Once you are in the bootloader again, continue. - type: call_button - command: fastboot_flash_recovery + command: fastboot_flash_boot content: > - Now we need to boot into recovery again. Press run and when you see the TWRP screen you can continue. - install_os: - - type: call_button - content: > - In the next steps, you finally flash the selected OS image. - Connect your device with your computer with the USB-Cable. - This step will format your phone and wipe all the data. It will also remove encryption and delete all files stored - in the internal storage. Then the OS image will be installed. Confirm to run. This might take a while. At the end your phone will boot into the new OS. - command: adb_twrp_wipe_and_install \ No newline at end of file + Now we need to boot into recovery again. Press run and when you see the TWRP screen you can continue. \ No newline at end of file diff --git a/openandroidinstaller/assets/configs/kirin.yaml b/openandroidinstaller/assets/configs/kirin.yaml index 60f7db1f..6418fbaf 100644 --- a/openandroidinstaller/assets/configs/kirin.yaml +++ b/openandroidinstaller/assets/configs/kirin.yaml @@ -40,21 +40,13 @@ steps: command: adb_reboot_bootloader - type: call_button content: Flash a custom recovery (temporarily) by pressing 'Confirm and run'. Once it's done continue. - command: fastboot_flash_recovery + command: fastboot_flash_boot - type: call_button command: adb_twrp_copy_partitions content: > In some cases, the inactive slot can be unpopulated or contain much older firmware than the active slot, leading to various issues including a potential hard-brick. We can ensure none of that will happen by copying the contents of the active slot to the inactive slot. Press 'confirm and run' to to this. Once you are in the bootloader again, continue. - type: call_button - command: fastboot_flash_recovery + command: fastboot_flash_boot content: > - Now we need to boot into recovery again. Press run and when you see the TWRP screen you can continue. - install_os: - - type: call_button - content: > - In the next steps, you finally flash the selected OS image. - Connect your device with your computer with the USB-Cable. - This step will format your phone and wipe all the data. It will also remove encryption and delete all files stored - in the internal storage. Then the OS image will be installed. Confirm to run. This might take a while. At the end your phone will boot into the new OS. - command: adb_twrp_wipe_and_install \ No newline at end of file + Now we need to boot into recovery again. Press run and when you see the TWRP screen you can continue. \ No newline at end of file diff --git a/openandroidinstaller/assets/configs/mermaid.yaml b/openandroidinstaller/assets/configs/mermaid.yaml index 80fd8a04..8a0861ef 100644 --- a/openandroidinstaller/assets/configs/mermaid.yaml +++ b/openandroidinstaller/assets/configs/mermaid.yaml @@ -40,21 +40,13 @@ steps: command: adb_reboot_bootloader - type: call_button content: Flash a custom recovery (temporarily) by pressing 'Confirm and run'. Once it's done continue. - command: fastboot_flash_recovery + command: fastboot_flash_boot - type: call_button command: adb_twrp_copy_partitions content: > In some cases, the inactive slot can be unpopulated or contain much older firmware than the active slot, leading to various issues including a potential hard-brick. We can ensure none of that will happen by copying the contents of the active slot to the inactive slot. Press 'confirm and run' to to this. Once you are in the bootloader again, continue. - type: call_button - command: fastboot_flash_recovery + command: fastboot_flash_boot content: > - Now we need to boot into recovery again. Press run and when you see the TWRP screen you can continue. - install_os: - - type: call_button - content: > - In the next steps, you finally flash the selected OS image. - Connect your device with your computer with the USB-Cable. - This step will format your phone and wipe all the data. It will also remove encryption and delete all files stored - in the internal storage. Then the OS image will be installed. Confirm to run. This might take a while. At the end your phone will boot into the new OS. - command: adb_twrp_wipe_and_install \ No newline at end of file + Now we need to boot into recovery again. Press run and when you see the TWRP screen you can continue. \ No newline at end of file diff --git a/openandroidinstaller/assets/configs/pioneer.yaml b/openandroidinstaller/assets/configs/pioneer.yaml index 85303ed6..4d2afc39 100644 --- a/openandroidinstaller/assets/configs/pioneer.yaml +++ b/openandroidinstaller/assets/configs/pioneer.yaml @@ -44,19 +44,11 @@ steps: content: Flash a custom recovery (temporarily) by pressing 'Confirm and run'. Once it's done continue. command: fastboot_flash_recovery - type: call_button - command: adb_twrp_copy_partitions + command: adb_twrp_copy_boot content: > In some cases, the inactive slot can be unpopulated or contain much older firmware than the active slot, leading to various issues including a potential hard-brick. We can ensure none of that will happen by copying the contents of the active slot to the inactive slot. Press 'confirm and run' to to this. Once you are in the bootloader again, continue. - type: call_button - command: fastboot_flash_recovery - content: > - Now we need to boot into recovery again. Press run and when you see the TWRP screen you can continue. - install_os: - - type: call_button + command: fastboot_flash_boot content: > - In the next steps, you finally flash the selected OS image. - Connect your device with your computer with the USB-Cable. - This step will format your phone and wipe all the data. It will also remove encryption and delete all files stored - in the internal storage. Then the OS image will be installed. Confirm to run. This might take a while. At the end your phone will boot into the new OS. - command: adb_twrp_wipe_and_install \ No newline at end of file + Now we need to boot into recovery again. Press run and when you see the TWRP screen you can continue. \ No newline at end of file diff --git a/openandroidinstaller/assets/configs/yuga.yaml b/openandroidinstaller/assets/configs/yuga.yaml index d7b3a30b..16409933 100644 --- a/openandroidinstaller/assets/configs/yuga.yaml +++ b/openandroidinstaller/assets/configs/yuga.yaml @@ -37,12 +37,4 @@ steps: command: adb_reboot_bootloader - type: call_button content: Next, you need to flash a custom recovery image. Press the button to flash the selected image. Then continue. - command: fastboot_flash_recovery - install_os: - - type: call_button - content: > - In the next steps, you finally flash the selected OS image. - Connect your device with your computer with the USB-Cable. - This step will format your phone and wipe all the data. It will also remove encryption and delete all files stored - in the internal storage. Then the OS image will be installed. Confirm to run. This might take a while. At the end your phone will boot into the new OS. - command: adb_twrp_wipe_and_install \ No newline at end of file + command: fastboot_flash_boot \ No newline at end of file diff --git a/openandroidinstaller/assets/configs/z3.yaml b/openandroidinstaller/assets/configs/z3.yaml index 54eb3553..e9f5c797 100644 --- a/openandroidinstaller/assets/configs/z3.yaml +++ b/openandroidinstaller/assets/configs/z3.yaml @@ -39,12 +39,4 @@ steps: command: adb_reboot_bootloader - type: call_button content: Next, you need to flash a custom recovery image. Press the button to flash the selected image. Then continue. - command: fastboot_flash_recovery - install_os: - - type: call_button - content: > - In the next steps, you finally flash the selected OS image. - Connect your device with your computer with the USB-Cable. - This step will format your phone and wipe all the data. It will also remove encryption and delete all files stored - in the internal storage. Then the OS image will be installed. Confirm to run. This might take a while. At the end your phone will boot into the new OS. - command: adb_twrp_wipe_and_install \ No newline at end of file + command: fastboot_flash_boot \ No newline at end of file diff --git a/openandroidinstaller/installer_config.py b/openandroidinstaller/installer_config.py index 574864de..3efacf47 100644 --- a/openandroidinstaller/installer_config.py +++ b/openandroidinstaller/installer_config.py @@ -152,7 +152,7 @@ def validate_config(config: str) -> bool: ), "content": str, schema.Optional("command"): Regex( - r"adb_reboot|adb_reboot_bootloader|adb_reboot_download|adb_sideload|adb_twrp_wipe_and_install|adb_twrp_copy_partitions|fastboot_flash_recovery|fastboot_unlock_with_code|fastboot_get_unlock_data|fastboot_unlock|fastboot_oem_unlock|fastboot_reboot|heimdall_flash_recovery" + r"adb_reboot|adb_reboot_bootloader|adb_reboot_download|adb_sideload|adb_twrp_wipe_and_install|adb_twrp_copy_partitions|fastboot_flash_recovery|fastboot_flash_boot|fastboot_unlock_with_code|fastboot_get_unlock_data|fastboot_unlock|fastboot_oem_unlock|fastboot_reboot|heimdall_flash_recovery" ), schema.Optional("allow_skip"): bool, schema.Optional("img"): str, diff --git a/openandroidinstaller/openandroidinstaller.py b/openandroidinstaller/openandroidinstaller.py index 06976d21..74119bc1 100644 --- a/openandroidinstaller/openandroidinstaller.py +++ b/openandroidinstaller/openandroidinstaller.py @@ -124,23 +124,11 @@ def __init__(self, state: AppState): # initialize the addon view self.select_addon_view = AddonsView(on_confirm=self.to_next_view, state=self.state) - self.flash_recovery_view = StepView( - step=Step( - title="Flash custom recovery", - type="call_button", - content="Flash a custom recovery (temporarily) by pressing 'Confirm and run'. Once your phone screen looks like the picture on the left, continue.", - command="fastboot_flash_recovery", - img="twrp-start.jpeg", - ), - state=self.state, - on_confirm=self.to_next_view, - ) self.install_addons_view = InstallAddonsView( on_confirm=self.to_next_view, state=self.state ) self.state.addon_views = [ self.install_addons_view, - self.flash_recovery_view, self.select_addon_view, ] diff --git a/openandroidinstaller/tooling.py b/openandroidinstaller/tooling.py index aa96c810..195e80fa 100644 --- a/openandroidinstaller/tooling.py +++ b/openandroidinstaller/tooling.py @@ -155,7 +155,7 @@ def adb_twrp_copy_partitions(bin_path: Path, config_path: Path): def adb_twrp_wipe_and_install( - bin_path: Path, target: str, config_path: Path, install_addons=True + bin_path: Path, target: str, config_path: Path, is_ab: bool, install_addons=True, recovery: str=None, ) -> bool: """Wipe and format data with twrp, then flash os image with adb. @@ -221,19 +221,31 @@ def adb_twrp_wipe_and_install( # finally reboot into os or to fastboot for flashing addons sleep(7) if install_addons: - # TODO: Fix the process for samsung devices - # reboot into the bootloader again - logger.info("Rebooting device into bootloader with adb.") - for line in run_command("adb", ["reboot", "bootloader"], bin_path): - yield line - if (type(line) == bool) and not line: - logger.error("Reboot into bootloader failed.") - yield False - return - sleep(3) + if is_ab: + # reboot into the bootloader again + logger.info("Rebooting device into bootloader with adb.") + for line in run_command("adb", ["reboot", "bootloader"], bin_path): + yield line + if (type(line) == bool) and not line: + logger.error("Reboot into bootloader failed.") + yield False + return + sleep(3) + # boot to TWRP again + logger.info("Boot custom recovery with fastboot.") + for line in run_command("fastboot", ["boot", f"{recovery}"], bin_path): + yield line + if (type(line) == bool) and not line: + logger.error("Reboot into bootloader failed.") + yield False + return + sleep(7) + else: + # if not an a/b-device just stay in twrp + pass else: logger.info("Reboot into OS.") - for line in run_command("adb", ["reboot"], bin_path): # "shell", "twrp", + for line in run_command("adb", ["reboot"], bin_path): yield line if (type(line) == bool) and not line: logger.error("Rebooting failed.") @@ -243,21 +255,23 @@ def adb_twrp_wipe_and_install( yield True -def adb_twrp_install_addons(bin_path: Path, config_path: Path, addons: List[str]) -> bool: +def adb_twrp_install_addons(bin_path: Path, addons: List[str], is_ab: bool) -> bool: """Flash addons through adb and twrp. Only works for twrp recovery. """ logger.info("Install addons with twrp.") - sleep(7) - # activate sideload - logger.info("Activate sideload.") - for line in run_command("adb", ["shell", "twrp", "sideload"], bin_path): - yield line - if (type(line) == bool) and not line: - logger.error("Activating sideload failed.") - yield False - return + if is_ab: + sleep(7) + # activate sideload + logger.info("Activate sideload.") + for line in run_command("adb", ["shell", "twrp", "sideload"], bin_path): + yield line + if (type(line) == bool) and not line: + logger.error("Activating sideload failed.") + yield False + return + # now flash os image sleep(5) logger.info("Sideload and install addons.") @@ -271,41 +285,53 @@ def adb_twrp_install_addons(bin_path: Path, config_path: Path, addons: List[str] # return # finally reboot into os sleep(7) - # reboot into the bootloader again - logger.info("Rebooting device into bootloader with adb.") - for line in run_command("adb", ["reboot", "bootloader"], bin_path): - yield line - if (type(line) == bool) and not line: - logger.error("Reboot into bootloader failed.") - yield False - return - sleep(3) - # switch active boot partition - logger.info("Switch active boot partition") - for line in run_command("fastboot", ["set_active", "other"], bin_path): - yield line - if (type(line) == bool) and not line: - logger.error("Switching boot partition failed.") - yield False - return - sleep(1) - for line in run_command("fastboot", ["set_active", "other"], bin_path): - yield line - if (type(line) == bool) and not line: - logger.error("Switching boot partition failed.") - yield False - return - sleep(1) - # reboot with fastboot - logger.info("Reboot into OS.") - for line in run_command("fastboot", ["reboot"], bin_path): - yield line - if (type(line) == bool) and not line: - logger.error("Rebooting failed.") - yield False - return + if is_ab: + # reboot into the bootloader again + logger.info("Rebooting device into bootloader with adb.") + for line in run_command("adb", ["reboot", "bootloader"], bin_path): + yield line + if (type(line) == bool) and not line: + logger.error("Reboot into bootloader failed.") + yield False + return + sleep(3) + # switch active boot partition + logger.info("Switch active boot partition") + for line in run_command("fastboot", ["set_active", "other"], bin_path): + yield line + if (type(line) == bool) and not line: + logger.error("Switching boot partition failed.") + yield False + return + sleep(1) + for line in run_command("fastboot", ["set_active", "other"], bin_path): + yield line + if (type(line) == bool) and not line: + logger.error("Switching boot partition failed.") + yield False + return + sleep(1) + # reboot with fastboot + logger.info("Reboot into OS.") + for line in run_command("fastboot", ["reboot"], bin_path): + yield line + if (type(line) == bool) and not line: + logger.error("Rebooting failed.") + yield False + return + else: + yield True else: - yield True + # reboot with adb + logger.info("Reboot into OS.") + for line in run_command("adb", ["reboot"], bin_path): + yield line + if (type(line) == bool) and not line: + logger.error("Rebooting failed.") + yield False + return + else: + yield True def fastboot_unlock_with_code(bin_path: Path, unlock_code: str) -> bool: @@ -368,17 +394,51 @@ def fastboot_reboot(bin_path: Path) -> bool: yield True -def fastboot_flash_recovery(bin_path: Path, recovery: str) -> bool: +def fastboot_flash_recovery(bin_path: Path, recovery: str, is_ab: bool=True) -> bool: """Temporarily, flash custom recovery with fastboot.""" + if is_ab: + logger.info("Boot custom recovery with fastboot.") + for line in run_command("fastboot", ["boot", f"{recovery}"], bin_path): + yield line + else: + logger.info("Flash custom recovery with fastboot.") + for line in run_command("fastboot", ["flash", "recovery", f"{recovery}"], bin_path): + yield line + if (type(line) == bool) and not line: + logger.error("Flashing recovery failed.") + yield False + else: + yield True + # reboot + logger.info("Boot into TWRP with fastboot.") + for line in run_command("fastboot", ["reboot", "recovery"], bin_path): + yield line + + if (type(line) == bool) and not line: + logger.error("Booting recovery failed.") + yield False + else: + yield True + +def fastboot_flash_boot(bin_path: Path, recovery: str) -> bool: + """Temporarily, flash custom recovery with fastboot to boot partition.""" logger.info("Flash custom recovery with fastboot.") - for line in run_command("fastboot", ["boot", f"{recovery}"], bin_path): + for line in run_command("fastboot", ["flash", "boot", f"{recovery}"], bin_path): yield line if (type(line) == bool) and not line: logger.error("Flashing recovery failed.") yield False else: yield True - + # reboot + logger.info("Boot into TWRP with fastboot.") + for line in run_command("fastboot", ["reboot"], bin_path): + yield line + if (type(line) == bool) and not line: + logger.error("Booting recovery failed.") + yield False + else: + yield True def heimdall_flash_recovery(bin_path: Path, recovery: str) -> bool: """Temporarily, flash custom recovery with heimdall.""" @@ -398,7 +458,7 @@ def search_device(platform: str, bin_path: Path) -> Optional[str]: """Search for a connected device.""" logger.info(f"Search devices on {platform} with {bin_path}...") try: - # read device properties + # read device code if platform in ("linux", "darwin"): output = check_output( [ @@ -431,4 +491,43 @@ def search_device(platform: str, bin_path: Path) -> Optional[str]: return device_code except CalledProcessError: logger.error("Failed to detect a device.") - return None + return None + +def check_ab_partition(platform: str, bin_path: Path) -> Optional[str]: + """Figure out, if its an a/b-partitioned device.""" + logger.info(f"Run on {platform} with {bin_path}...") + try: + # check if ab device + if platform in ("linux", "darwin"): + output = check_output( + [ + str(bin_path.joinpath(Path("adb"))), + "shell", + "getprop", + "|", + "grep", + "ro.boot.slot_suffix", + ], + stderr=STDOUT, + ).decode() + elif platform in ("windows", "win32"): + output = check_output( + [ + str(bin_path.joinpath(Path("adb.exe"))), + "shell", + "getprop", + "|", + "findstr", + "ro.boot.slot_suffix", + ], + stderr=STDOUT, + shell=True, + ).decode() + else: + raise Exception(f"Unknown platform {platform}.") + logger.info(output) + logger.info("This is an a/b-partitioned device.") + return True + except CalledProcessError: + logger.info("This is not an a/b-partitioned device.") + return False diff --git a/openandroidinstaller/views/addon_view.py b/openandroidinstaller/views/addon_view.py index 638b2fb8..f9952958 100644 --- a/openandroidinstaller/views/addon_view.py +++ b/openandroidinstaller/views/addon_view.py @@ -88,6 +88,8 @@ def build(self): self.selected_addons = Text("Selected addons: ") # initialize and manage button state. + # wrap the call to the next step in a call to boot fastboot + self.confirm_button = confirm_button(self.on_confirm) # self.confirm_button.disabled = True # self.pick_addons_dialog.on_result = self.enable_button_if_ready diff --git a/openandroidinstaller/views/install_addons_view.py b/openandroidinstaller/views/install_addons_view.py index 78ba5b86..7afd92fd 100644 --- a/openandroidinstaller/views/install_addons_view.py +++ b/openandroidinstaller/views/install_addons_view.py @@ -156,7 +156,7 @@ def run_install_addons(self, e): for line in adb_twrp_install_addons( addons=self.state.addon_paths, bin_path=self.state.bin_path, - config_path=self.state.config_path, + is_ab=self.state.is_ab, ): # write the line to advanced output terminal self.terminal_box.write_line(line) diff --git a/openandroidinstaller/views/install_view.py b/openandroidinstaller/views/install_view.py index 6a31a0fa..67bd1c1d 100644 --- a/openandroidinstaller/views/install_view.py +++ b/openandroidinstaller/views/install_view.py @@ -182,6 +182,8 @@ def run_install(self, e): config_path=self.state.config_path, bin_path=self.state.bin_path, install_addons=self.state.install_addons, + is_ab=self.state.is_ab, + recovery=self.state.recovery_path, ): # write the line to advanced output terminal self.terminal_box.write_line(line) diff --git a/openandroidinstaller/views/start_view.py b/openandroidinstaller/views/start_view.py index de461dfc..622e152c 100644 --- a/openandroidinstaller/views/start_view.py +++ b/openandroidinstaller/views/start_view.py @@ -37,7 +37,7 @@ from views import BaseView from app_state import AppState from widgets import get_title -from tooling import search_device +from tooling import search_device, check_ab_partition from installer_config import InstallerConfig @@ -204,7 +204,7 @@ def search_devices(self, e): # search the device if self.state.test: # this only happens for testing - device_code = self.state.test_config + device_code, is_ab = self.state.test_config, True logger.info( f"Running search in development mode and loading config {device_code}.yaml." ) @@ -212,6 +212,9 @@ def search_devices(self, e): device_code = search_device( platform=self.state.platform, bin_path=self.state.bin_path ) + is_ab = check_ab_partition( + platform=self.state.platform, bin_path=self.state.bin_path + ) if device_code: self.device_name.value = device_code self.device_name.color = colors.BLACK @@ -227,6 +230,8 @@ def search_devices(self, e): self.device_name.value = device_code # load config from file self.state.load_config(device_code) + # write ab-info to state + self.state.is_ab = is_ab if self.state.config: device_name = self.state.config.metadata.get( "devicename", "No device name in config." diff --git a/openandroidinstaller/views/step_view.py b/openandroidinstaller/views/step_view.py index 2c8a8060..adbc7bb2 100644 --- a/openandroidinstaller/views/step_view.py +++ b/openandroidinstaller/views/step_view.py @@ -45,6 +45,7 @@ adb_sideload, adb_twrp_copy_partitions, fastboot_flash_recovery, + fastboot_flash_boot, fastboot_oem_unlock, fastboot_reboot, fastboot_unlock, @@ -220,7 +221,10 @@ def call_to_phone(self, e, command: str): "fastboot_oem_unlock": fastboot_oem_unlock, "fastboot_get_unlock_data": fastboot_get_unlock_data, "fastboot_flash_recovery": partial( - fastboot_flash_recovery, recovery=self.state.recovery_path + fastboot_flash_recovery, recovery=self.state.recovery_path, is_ab=self.state.is_ab, + ), + "fastboot_flash_boot": partial( + fastboot_flash_boot, recovery=self.state.recovery_path, ), "fastboot_reboot": fastboot_reboot, "heimdall_flash_recovery": partial( @@ -328,7 +332,7 @@ def display_progress_bar(self, line: str): result = None # get the progress numbers from the output lines if (type(line) == str) and line.strip(): - result = re.search(r"\(\~(\d{1,3})\%\)|(Total xfer: 1\.)", line.strip()) + result = re.search(r"\(\~(\d{1,3})\%\)|(Total xfer:)", line.strip()) if result: if result.group(1): percentage_done = int(result.group(1))