Quantcast
Channel: Active questions tagged python - Stack Overflow
Viewing all articles
Browse latest Browse all 14243

how to download file using subprocess and update QProgressBar in PyQt6

$
0
0

I am writing a program to download a bunch of files, the program is very long, I download the files by calling aria2c.exe via subprocess, and I have encountered a problem.

Specifically, when using aria2c + subprocess + QThread to download the file in the background, the GUI hangs and the progressbar and related labels don't update while the download is running, the GUI remains unresponsive until the download is complete.

I used the same method to download the files in console without the GUI using aria2c + subprocess + threading.Thread, the download completed successfully and all stats are updated correctly, and the threads complete without errors.

This is the minimal code required to reproduce the issue, though it is rather long:

import reimport requestsimport subprocessimport sysimport timefrom PyQt6.QtCore import Qt, QThread, pyqtSignal, pyqtSlotfrom PyQt6.QtGui import (    QFont,    QFontMetrics,)from PyQt6.QtWidgets import (    QApplication,    QGridLayout,    QGroupBox,    QHBoxLayout,    QLabel,    QProgressBar,    QPushButton,    QSizePolicy,    QVBoxLayout,    QWidget,)BASE_COMMAND = ["aria2c","--async-dns=false","--connect-timeout=3","--disk-cache=256M","--disable-ipv6=true","--enable-mmap=true","--http-no-cache=true","--max-connection-per-server=16","--min-split-size=1M","--piece-length=1M","--split=32","--timeout=3",]url = "http://ipv4.download.thinkbroadband.com/100MB.zip"UNITS_SIZE = {"B": 1, "KiB": 1 << 10, "MiB": 1 << 20, "GiB": 1 << 30}DOWNLOAD_PROGRESS = re.compile("(?P<downloaded>\d+(\.\d+)?[KMG]iB)/(?P<total>\d+(\.\d+)?[KMG]iB)")UNITS = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB")ALIGNMENT = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTopclass Font(QFont):    def __init__(self, size: int = 10) -> None:        super().__init__()        self.setFamily("Times New Roman")        self.setStyleHint(QFont.StyleHint.Times)        self.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)        self.setPointSize(size)        self.setBold(True)        self.setHintingPreference(QFont.HintingPreference.PreferFullHinting)FONT = Font()FONT_RULER = QFontMetrics(FONT)class Box(QGroupBox):    def __init__(self) -> None:        super().__init__()        self.setAlignment(ALIGNMENT)        self.setContentsMargins(3, 3, 3, 3)        self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)        self.vbox = make_vbox(self)class Button(QPushButton):    def __init__(self, text: str) -> None:        super().__init__()        self.setFont(FONT)        self.setFixedSize(72, 20)        self.setText(text)def make_box(    box_type: type[QHBoxLayout] | type[QVBoxLayout] | type[QGridLayout],    parent: QWidget,    margin: int,) -> QHBoxLayout | QVBoxLayout | QGridLayout:    box = box_type(parent) if parent else box_type()    box.setAlignment(ALIGNMENT)    box.setContentsMargins(*[margin] * 4)    return boxdef make_vbox(parent: QWidget = None, margin: int = 0) -> QVBoxLayout:    return make_box(QVBoxLayout, parent, margin)def make_hbox(parent: QWidget = None, margin: int = 0) -> QHBoxLayout:    return make_box(QHBoxLayout, parent, margin)class Label(QLabel):    def __init__(self, text: str) -> None:        super().__init__()        self.setFont(FONT)        self.set_text(text)    def autoResize(self) -> None:        self.Height = FONT_RULER.size(0, self.text()).height()        self.Width = FONT_RULER.size(0, self.text()).width()        self.setFixedSize(self.Width + 3, self.Height + 8)    def set_text(self, text: str) -> None:        self.setText(text)        self.autoResize()class ProgressBar(QProgressBar):    def __init__(self) -> None:        super().__init__()        self.setFont(FONT)        self.setValue(0)        self.setFixedSize(1000, 25)class DownThread(QThread):    update = pyqtSignal(dict)    def __init__(self, parent: QWidget, url: str, folder: str) -> None:        super().__init__(parent)        self.url = url        self.folder = folder        self.line = ""        self.stats = {}    def run(self) -> None:        self.total = 0        res = requests.head(url)        if res.status_code == 200 and (total := res.headers.get("Content-Length")):            self.total = int(total)        self.process = subprocess.Popen(            BASE_COMMAND + [f"--dir={self.folder}", self.url],            stdout=subprocess.PIPE,            stderr=subprocess.PIPE,        )        self.monitor()        self.quit()    def monitor(self) -> None:        self.start = self.elapsed = time.time_ns()        self.downloaded = 0        output = self.process.stdout        self.buffer = ""        while self.process.poll() is None:            char = output.read(1)            if char in (b"\n", b"\r"):                self.line = self.buffer                self.buffer = ""                self.update_stats()            else:                self.buffer += char.decode()        self.finish()    def update_stats(self) -> None:        if match := DOWNLOAD_PROGRESS.search(self.line):            current = time.time_ns()            new, total = map(self.parse_size, match.groupdict().values())            delta = (current - self.elapsed) / 1e9            speed = (new - self.downloaded) / delta            self.stats = {"downloaded": new,"total": total,"speed": speed,"elapsed": (current - self.start) / 1e9,"eta": ((total - new) / speed) if speed != 0 else 1e309,            }            self.elapsed = current            self.downloaded = new            self.update.emit(self.stats)    @staticmethod    def parse_size(size: str) -> int:        unit = size[-3:]        size = size.replace(unit, "")        return (float if "." in size else int)(size) * UNITS_SIZE[unit]    def finish(self):        self.elapsed = (time.time_ns() - self.start) // 1e9        total = self.total or self.stats["total"]        self.stats["downloaded"] = total        self.stats["total"] = total        self.stats["elapsed"] = self.elapsed        self.stats["eta"] = 0        self.stats["speed"] = total / self.elapsed        self.update.emit(self.stats)class Underbar(Box):    def __init__(self):        super().__init__()        self.setFixedHeight(256)        self.progressbar = ProgressBar()        self.hbox = make_hbox()        self.hbox.addWidget(self.progressbar)        self.displays = {}        for name in ("Downloaded", "Total", "Speed", "Elapsed", "ETA"):            self.hbox.addWidget(Label(name))            widget = Label("0")            self.hbox.addWidget(widget)            self.displays[name] = widget        self.vbox.addLayout(self.hbox)        self.button = Button("Test")        self.vbox.addWidget(self.button)        self.button.clicked.connect(self.test)    def test(self):        self.progressbar.setValue(0)        down = DownThread(self, url, "D:/downloads")        down.update.connect(self.update_displays)        down.run()    def update_displays(self, stats):        self.progressbar.setValue(100 * int(stats["downloaded"] / stats["total"] + 0.5))        for name, suffix in (("Downloaded", ""), ("Total", ""), ("Speed", "/s")):            self.displays[name].setText(                f"{round(stats[name.lower()] / 1048576, 2)}MiB{suffix}"            )        self.displays["Elapsed"].setText(f'{round(stats["elapsed"], 2)}s')        self.displays["ETA"].setText(f'{round(stats["eta"], 2)}s')        for label in self.displays.values():            label.autoResize()if __name__ == "__main__":    app = QApplication([])    app.setStyle("Fusion")    window = Underbar()    window.show()    sys.exit(app.exec())

How to fix this?


Viewing all articles
Browse latest Browse all 14243

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>