First download cleanup.
This commit is contained in:
parent
aeb6ceb942
commit
ee0cb00428
@ -42,32 +42,109 @@ ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
|
||||
is_int=True)
|
||||
|
||||
|
||||
class DownloadItem(QObject):
|
||||
class DownloadItemStats(QObject):
|
||||
|
||||
"""A single download currently running.
|
||||
"""Statistics (bytes done, total bytes, time, etc.) about a download.
|
||||
|
||||
Class attributes:
|
||||
SPEED_REFRESH_INTERVAL: How often to refresh the speed, in msec.
|
||||
SPEED_AVG_WINDOW: How many seconds of speed data to average to
|
||||
estimate the remaining time.
|
||||
|
||||
Attributes:
|
||||
done: How many bytes there are already downloaded.
|
||||
total: The total count of bytes. None if the total is unknown.
|
||||
speed: The current download speed, in bytes per second.
|
||||
_speed_avg: A rolling average of speeds.
|
||||
_last_done: The count of bytes which where downloaded when calculating
|
||||
the speed the last time.
|
||||
"""
|
||||
|
||||
MAX_REDIRECTS = 10
|
||||
SPEED_REFRESH_INTERVAL = 500
|
||||
SPEED_AVG_WINDOW = 30
|
||||
|
||||
updated = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.total = None
|
||||
self.done = 0
|
||||
self.speed = 0
|
||||
self._last_done = 0
|
||||
samples = int(self.SPEED_AVG_WINDOW *
|
||||
(1000 / self.SPEED_REFRESH_INTERVAL))
|
||||
self._speed_avg = collections.deque(maxlen=samples)
|
||||
self.timer = usertypes.Timer(self, 'speed_refresh')
|
||||
self.timer.timeout.connect(self._update_speed)
|
||||
self.timer.setInterval(self.SPEED_REFRESH_INTERVAL)
|
||||
self.timer.start()
|
||||
|
||||
@pyqtSlot()
|
||||
def _update_speed(self):
|
||||
"""Recalculate the current download speed."""
|
||||
delta = self.done - self._last_done
|
||||
self.speed = delta * 1000 / self.SPEED_REFRESH_INTERVAL
|
||||
self._speed_avg.append(self.speed)
|
||||
self._last_done = self.done
|
||||
self.updated.emit()
|
||||
|
||||
def finish(self):
|
||||
"""Set the download stats as finished."""
|
||||
self.timer.stop()
|
||||
self.done = self.total
|
||||
|
||||
def percentage(self):
|
||||
"""The current download percentage, or None if unknown."""
|
||||
if self.total == 0 or self.total is None:
|
||||
return None
|
||||
else:
|
||||
return 100 * self.done / self.total
|
||||
|
||||
def remaining_time(self):
|
||||
"""The remaining download time in seconds, or None."""
|
||||
if self.total is None or not self._speed_avg:
|
||||
# No average yet or we don't know the total size.
|
||||
return None
|
||||
remaining_bytes = self.total - self.done
|
||||
avg = sum(self._speed_avg) / len(self._speed_avg)
|
||||
if avg == 0:
|
||||
# Download stalled
|
||||
return None
|
||||
else:
|
||||
return remaining_bytes / avg
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
def on_download_progress(self, bytes_done, bytes_total):
|
||||
"""Upload local variables when the download progress changed.
|
||||
|
||||
Args:
|
||||
bytes_done: How many bytes are downloaded.
|
||||
bytes_total: How many bytes there are to download in total.
|
||||
"""
|
||||
if bytes_total == -1:
|
||||
bytes_total = None
|
||||
self.done = bytes_done
|
||||
self.total = bytes_total
|
||||
self.updated.emit()
|
||||
|
||||
|
||||
class DownloadItem(QObject):
|
||||
|
||||
"""A single download currently running.
|
||||
|
||||
Class attributes:
|
||||
MAX_REDIRECTS: The maximum redirection count.
|
||||
|
||||
Attributes:
|
||||
stats: A DownloadItemStats object.
|
||||
successful: Whether the download has completed sucessfully.
|
||||
error_msg: The current error message, or None
|
||||
autoclose: Whether to close the associated file if the download is
|
||||
done.
|
||||
_bytes_done: How many bytes there are already downloaded.
|
||||
_bytes_total: The total count of bytes.
|
||||
None if the total is unknown.
|
||||
_speed: The current download speed, in bytes per second.
|
||||
fileobj: The file object to download the file to.
|
||||
_filename: The filename of the download.
|
||||
_is_cancelled: Whether the download was cancelled.
|
||||
_speed_avg: A rolling average of speeds.
|
||||
_reply: The QNetworkReply associated with this download.
|
||||
_last_done: The count of bytes which where downloaded when calculating
|
||||
the speed the last time.
|
||||
_redirects: How many time we were redirected already.
|
||||
|
||||
Signals:
|
||||
@ -81,9 +158,6 @@ class DownloadItem(QObject):
|
||||
arg 1: The old QNetworkReply.
|
||||
"""
|
||||
|
||||
MAX_REDIRECTS = 10
|
||||
SPEED_REFRESH_INTERVAL = 500
|
||||
SPEED_AVG_WINDOW = 30
|
||||
data_changed = pyqtSignal()
|
||||
finished = pyqtSignal()
|
||||
error = pyqtSignal(str)
|
||||
@ -97,28 +171,17 @@ class DownloadItem(QObject):
|
||||
reply: The QNetworkReply to download.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.stats = DownloadItemStats(self)
|
||||
self.stats.updated.connect(self.data_changed)
|
||||
self.autoclose = True
|
||||
self._reply = None
|
||||
self._redirects = 0
|
||||
self._bytes_total = None
|
||||
self._speed = 0
|
||||
self.error_msg = None
|
||||
self.basename = '???'
|
||||
self.successful = False
|
||||
samples = int(self.SPEED_AVG_WINDOW *
|
||||
(1000 / self.SPEED_REFRESH_INTERVAL))
|
||||
self._speed_avg = collections.deque(maxlen=samples)
|
||||
self.fileobj = None
|
||||
self._filename = None
|
||||
self._is_cancelled = False
|
||||
self._do_delayed_write = False
|
||||
self._bytes_done = 0
|
||||
self._last_done = 0
|
||||
self.init_reply(reply)
|
||||
self.timer = usertypes.Timer(self, 'speed_refresh')
|
||||
self.timer.timeout.connect(self.update_speed)
|
||||
self.timer.setInterval(self.SPEED_REFRESH_INTERVAL)
|
||||
self.timer.start()
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, basename=self.basename)
|
||||
@ -128,15 +191,15 @@ class DownloadItem(QObject):
|
||||
|
||||
Example: foo.pdf [699.2kB/s|0.34|16%|4.253/25.124]
|
||||
"""
|
||||
speed = utils.format_size(self._speed, suffix='B/s')
|
||||
down = utils.format_size(self._bytes_done, suffix='B')
|
||||
perc = self._percentage()
|
||||
remaining = self._remaining_time()
|
||||
speed = utils.format_size(self.stats.speed, suffix='B/s')
|
||||
down = utils.format_size(self.stats.done, suffix='B')
|
||||
perc = self.stats.percentage()
|
||||
remaining = self.stats.remaining_time()
|
||||
if self.error_msg is None:
|
||||
errmsg = ""
|
||||
else:
|
||||
errmsg = " - {}".format(self.error_msg)
|
||||
if all(e is None for e in (perc, remaining, self._bytes_total)):
|
||||
if all(e is None for e in (perc, remaining, self.stats.total)):
|
||||
return ('{name} [{speed:>10}|{down}]{errmsg}'.format(
|
||||
name=self.basename, speed=speed, down=down, errmsg=errmsg))
|
||||
if perc is None:
|
||||
@ -147,7 +210,7 @@ class DownloadItem(QObject):
|
||||
remaining = '?'
|
||||
else:
|
||||
remaining = utils.format_seconds(remaining)
|
||||
total = utils.format_size(self._bytes_total, suffix='B')
|
||||
total = utils.format_size(self.stats.total, suffix='B')
|
||||
return ('{name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
|
||||
'{down}/{total}]{errmsg}'.format(
|
||||
name=self.basename, speed=speed, remaining=remaining,
|
||||
@ -161,7 +224,7 @@ class DownloadItem(QObject):
|
||||
self._reply.error.disconnect()
|
||||
self._reply.readyRead.disconnect()
|
||||
self.error_msg = msg
|
||||
self._bytes_done = self._bytes_total
|
||||
self.stats.finish()
|
||||
self.timer.stop()
|
||||
self.error.emit(msg)
|
||||
self._reply.abort()
|
||||
@ -174,26 +237,6 @@ class DownloadItem(QObject):
|
||||
self.error.emit(e.strerror)
|
||||
self.data_changed.emit()
|
||||
|
||||
def _percentage(self):
|
||||
"""The current download percentage, or None if unknown."""
|
||||
if self._bytes_total == 0 or self._bytes_total is None:
|
||||
return None
|
||||
else:
|
||||
return 100 * self._bytes_done / self._bytes_total
|
||||
|
||||
def _remaining_time(self):
|
||||
"""The remaining download time in seconds, or None."""
|
||||
if self._bytes_total is None or not self._speed_avg:
|
||||
# No average yet or we don't know the total size.
|
||||
return None
|
||||
remaining_bytes = self._bytes_total - self._bytes_done
|
||||
avg = sum(self._speed_avg) / len(self._speed_avg)
|
||||
if avg == 0:
|
||||
# Download stalled
|
||||
return None
|
||||
else:
|
||||
return remaining_bytes / avg
|
||||
|
||||
def init_reply(self, reply):
|
||||
"""Set a new reply and connect its signals.
|
||||
|
||||
@ -202,7 +245,7 @@ class DownloadItem(QObject):
|
||||
"""
|
||||
self._reply = reply
|
||||
reply.setReadBufferSize(16 * 1024 * 1024)
|
||||
reply.downloadProgress.connect(self.on_download_progress)
|
||||
reply.downloadProgress.connect(self.stats.on_download_progress)
|
||||
reply.finished.connect(self.on_reply_finished)
|
||||
reply.error.connect(self.on_reply_error)
|
||||
reply.readyRead.connect(self.on_ready_read)
|
||||
@ -224,20 +267,21 @@ class DownloadItem(QObject):
|
||||
if self.error_msg is not None:
|
||||
assert not self.successful
|
||||
return error
|
||||
elif self._percentage() is None:
|
||||
elif self.stats.percentage() is None:
|
||||
return start
|
||||
else:
|
||||
return utils.interpolate_color(start, stop, self._percentage(),
|
||||
system)
|
||||
return utils.interpolate_color(
|
||||
start, stop, self.stats.percentage(), system)
|
||||
|
||||
def cancel(self):
|
||||
"""Cancel the download."""
|
||||
log.downloads.debug("cancelled")
|
||||
self.cancelled.emit()
|
||||
self._is_cancelled = True
|
||||
if self._reply is not None:
|
||||
self._reply.finished.disconnect(self.on_reply_finished)
|
||||
self._reply.abort()
|
||||
self._reply.deleteLater()
|
||||
self._reply = None
|
||||
if self.fileobj is not None:
|
||||
self.fileobj.close()
|
||||
if self._filename is not None and os.path.exists(self._filename):
|
||||
@ -293,10 +337,10 @@ class DownloadItem(QObject):
|
||||
"{}".format(self.fileobj, fileobj))
|
||||
self.fileobj = fileobj
|
||||
try:
|
||||
if self._do_delayed_write:
|
||||
if self._reply.isFinished():
|
||||
# Downloading to the buffer in RAM has already finished so we
|
||||
# write out the data and clean up now.
|
||||
self.delayed_write()
|
||||
self.finish_download()
|
||||
else:
|
||||
# Since the buffer already might be full, on_ready_read might
|
||||
# not be called at all anymore, so we force it here to flush
|
||||
@ -305,10 +349,9 @@ class DownloadItem(QObject):
|
||||
except OSError as e:
|
||||
self._die(e.strerror)
|
||||
|
||||
def delayed_write(self):
|
||||
def finish_download(self):
|
||||
"""Write buffered data to disk and finish the QNetworkReply."""
|
||||
log.downloads.debug("Doing delayed write...")
|
||||
self._do_delayed_write = False
|
||||
log.downloads.debug("Finishing download...")
|
||||
if self._reply.isOpen():
|
||||
self.fileobj.write(self._reply.readAll())
|
||||
if self.autoclose:
|
||||
@ -319,20 +362,6 @@ class DownloadItem(QObject):
|
||||
self.finished.emit()
|
||||
log.downloads.debug("Download finished")
|
||||
|
||||
@pyqtSlot(int, int)
|
||||
def on_download_progress(self, bytes_done, bytes_total):
|
||||
"""Upload local variables when the download progress changed.
|
||||
|
||||
Args:
|
||||
bytes_done: How many bytes are downloaded.
|
||||
bytes_total: How many bytes there are to download in total.
|
||||
"""
|
||||
if bytes_total == -1:
|
||||
bytes_total = None
|
||||
self._bytes_done = bytes_done
|
||||
self._bytes_total = bytes_total
|
||||
self.data_changed.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_reply_finished(self):
|
||||
"""Clean up when the download was finished.
|
||||
@ -341,22 +370,17 @@ class DownloadItem(QObject):
|
||||
doesn't mean the download (i.e. writing data to the disk) is finished
|
||||
as well. Therefore, we can't close() the QNetworkReply in here yet.
|
||||
"""
|
||||
self._bytes_done = self._bytes_total
|
||||
self.timer.stop()
|
||||
self.stats.finish()
|
||||
is_redirected = self._handle_redirect()
|
||||
if is_redirected:
|
||||
return
|
||||
if self._is_cancelled:
|
||||
if self._reply is None:
|
||||
return
|
||||
log.downloads.debug("Reply finished, fileobj {}".format(self.fileobj))
|
||||
if self.fileobj is None:
|
||||
# We'll handle emptying the buffer and cleaning up as soon as the
|
||||
# filename is set.
|
||||
self._do_delayed_write = True
|
||||
else:
|
||||
if self.fileobj is not None:
|
||||
# We can do a "delayed" write immediately to empty the buffer and
|
||||
# clean up.
|
||||
self.delayed_write()
|
||||
self.finish_download()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_ready_read(self):
|
||||
@ -408,15 +432,6 @@ class DownloadItem(QObject):
|
||||
reply.deleteLater() # the old one
|
||||
return True
|
||||
|
||||
@pyqtSlot()
|
||||
def update_speed(self):
|
||||
"""Recalculate the current download speed."""
|
||||
delta = self._bytes_done - self._last_done
|
||||
self._speed = delta * 1000 / self.SPEED_REFRESH_INTERVAL
|
||||
self._speed_avg.append(self._speed)
|
||||
self._last_done = self._bytes_done
|
||||
self.data_changed.emit()
|
||||
|
||||
|
||||
class DownloadManager(QAbstractListModel):
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user