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)
|
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:
|
Class attributes:
|
||||||
SPEED_REFRESH_INTERVAL: How often to refresh the speed, in msec.
|
SPEED_REFRESH_INTERVAL: How often to refresh the speed, in msec.
|
||||||
SPEED_AVG_WINDOW: How many seconds of speed data to average to
|
SPEED_AVG_WINDOW: How many seconds of speed data to average to
|
||||||
estimate the remaining time.
|
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.
|
MAX_REDIRECTS: The maximum redirection count.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
stats: A DownloadItemStats object.
|
||||||
successful: Whether the download has completed sucessfully.
|
successful: Whether the download has completed sucessfully.
|
||||||
error_msg: The current error message, or None
|
error_msg: The current error message, or None
|
||||||
autoclose: Whether to close the associated file if the download is
|
autoclose: Whether to close the associated file if the download is
|
||||||
done.
|
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.
|
fileobj: The file object to download the file to.
|
||||||
_filename: The filename of the download.
|
_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.
|
_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.
|
_redirects: How many time we were redirected already.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
@ -81,9 +158,6 @@ class DownloadItem(QObject):
|
|||||||
arg 1: The old QNetworkReply.
|
arg 1: The old QNetworkReply.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MAX_REDIRECTS = 10
|
|
||||||
SPEED_REFRESH_INTERVAL = 500
|
|
||||||
SPEED_AVG_WINDOW = 30
|
|
||||||
data_changed = pyqtSignal()
|
data_changed = pyqtSignal()
|
||||||
finished = pyqtSignal()
|
finished = pyqtSignal()
|
||||||
error = pyqtSignal(str)
|
error = pyqtSignal(str)
|
||||||
@ -97,28 +171,17 @@ class DownloadItem(QObject):
|
|||||||
reply: The QNetworkReply to download.
|
reply: The QNetworkReply to download.
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self.stats = DownloadItemStats(self)
|
||||||
|
self.stats.updated.connect(self.data_changed)
|
||||||
self.autoclose = True
|
self.autoclose = True
|
||||||
self._reply = None
|
self._reply = None
|
||||||
self._redirects = 0
|
self._redirects = 0
|
||||||
self._bytes_total = None
|
|
||||||
self._speed = 0
|
|
||||||
self.error_msg = None
|
self.error_msg = None
|
||||||
self.basename = '???'
|
self.basename = '???'
|
||||||
self.successful = False
|
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.fileobj = None
|
||||||
self._filename = 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.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):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, basename=self.basename)
|
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]
|
Example: foo.pdf [699.2kB/s|0.34|16%|4.253/25.124]
|
||||||
"""
|
"""
|
||||||
speed = utils.format_size(self._speed, suffix='B/s')
|
speed = utils.format_size(self.stats.speed, suffix='B/s')
|
||||||
down = utils.format_size(self._bytes_done, suffix='B')
|
down = utils.format_size(self.stats.done, suffix='B')
|
||||||
perc = self._percentage()
|
perc = self.stats.percentage()
|
||||||
remaining = self._remaining_time()
|
remaining = self.stats.remaining_time()
|
||||||
if self.error_msg is None:
|
if self.error_msg is None:
|
||||||
errmsg = ""
|
errmsg = ""
|
||||||
else:
|
else:
|
||||||
errmsg = " - {}".format(self.error_msg)
|
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(
|
return ('{name} [{speed:>10}|{down}]{errmsg}'.format(
|
||||||
name=self.basename, speed=speed, down=down, errmsg=errmsg))
|
name=self.basename, speed=speed, down=down, errmsg=errmsg))
|
||||||
if perc is None:
|
if perc is None:
|
||||||
@ -147,7 +210,7 @@ class DownloadItem(QObject):
|
|||||||
remaining = '?'
|
remaining = '?'
|
||||||
else:
|
else:
|
||||||
remaining = utils.format_seconds(remaining)
|
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}%|'
|
return ('{name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
|
||||||
'{down}/{total}]{errmsg}'.format(
|
'{down}/{total}]{errmsg}'.format(
|
||||||
name=self.basename, speed=speed, remaining=remaining,
|
name=self.basename, speed=speed, remaining=remaining,
|
||||||
@ -161,7 +224,7 @@ class DownloadItem(QObject):
|
|||||||
self._reply.error.disconnect()
|
self._reply.error.disconnect()
|
||||||
self._reply.readyRead.disconnect()
|
self._reply.readyRead.disconnect()
|
||||||
self.error_msg = msg
|
self.error_msg = msg
|
||||||
self._bytes_done = self._bytes_total
|
self.stats.finish()
|
||||||
self.timer.stop()
|
self.timer.stop()
|
||||||
self.error.emit(msg)
|
self.error.emit(msg)
|
||||||
self._reply.abort()
|
self._reply.abort()
|
||||||
@ -174,26 +237,6 @@ class DownloadItem(QObject):
|
|||||||
self.error.emit(e.strerror)
|
self.error.emit(e.strerror)
|
||||||
self.data_changed.emit()
|
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):
|
def init_reply(self, reply):
|
||||||
"""Set a new reply and connect its signals.
|
"""Set a new reply and connect its signals.
|
||||||
|
|
||||||
@ -202,7 +245,7 @@ class DownloadItem(QObject):
|
|||||||
"""
|
"""
|
||||||
self._reply = reply
|
self._reply = reply
|
||||||
reply.setReadBufferSize(16 * 1024 * 1024)
|
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.finished.connect(self.on_reply_finished)
|
||||||
reply.error.connect(self.on_reply_error)
|
reply.error.connect(self.on_reply_error)
|
||||||
reply.readyRead.connect(self.on_ready_read)
|
reply.readyRead.connect(self.on_ready_read)
|
||||||
@ -224,20 +267,21 @@ class DownloadItem(QObject):
|
|||||||
if self.error_msg is not None:
|
if self.error_msg is not None:
|
||||||
assert not self.successful
|
assert not self.successful
|
||||||
return error
|
return error
|
||||||
elif self._percentage() is None:
|
elif self.stats.percentage() is None:
|
||||||
return start
|
return start
|
||||||
else:
|
else:
|
||||||
return utils.interpolate_color(start, stop, self._percentage(),
|
return utils.interpolate_color(
|
||||||
system)
|
start, stop, self.stats.percentage(), system)
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
"""Cancel the download."""
|
"""Cancel the download."""
|
||||||
log.downloads.debug("cancelled")
|
log.downloads.debug("cancelled")
|
||||||
self.cancelled.emit()
|
self.cancelled.emit()
|
||||||
self._is_cancelled = True
|
|
||||||
if self._reply is not None:
|
if self._reply is not None:
|
||||||
|
self._reply.finished.disconnect(self.on_reply_finished)
|
||||||
self._reply.abort()
|
self._reply.abort()
|
||||||
self._reply.deleteLater()
|
self._reply.deleteLater()
|
||||||
|
self._reply = None
|
||||||
if self.fileobj is not None:
|
if self.fileobj is not None:
|
||||||
self.fileobj.close()
|
self.fileobj.close()
|
||||||
if self._filename is not None and os.path.exists(self._filename):
|
if self._filename is not None and os.path.exists(self._filename):
|
||||||
@ -293,10 +337,10 @@ class DownloadItem(QObject):
|
|||||||
"{}".format(self.fileobj, fileobj))
|
"{}".format(self.fileobj, fileobj))
|
||||||
self.fileobj = fileobj
|
self.fileobj = fileobj
|
||||||
try:
|
try:
|
||||||
if self._do_delayed_write:
|
if self._reply.isFinished():
|
||||||
# Downloading to the buffer in RAM has already finished so we
|
# Downloading to the buffer in RAM has already finished so we
|
||||||
# write out the data and clean up now.
|
# write out the data and clean up now.
|
||||||
self.delayed_write()
|
self.finish_download()
|
||||||
else:
|
else:
|
||||||
# Since the buffer already might be full, on_ready_read might
|
# Since the buffer already might be full, on_ready_read might
|
||||||
# not be called at all anymore, so we force it here to flush
|
# not be called at all anymore, so we force it here to flush
|
||||||
@ -305,10 +349,9 @@ class DownloadItem(QObject):
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
self._die(e.strerror)
|
self._die(e.strerror)
|
||||||
|
|
||||||
def delayed_write(self):
|
def finish_download(self):
|
||||||
"""Write buffered data to disk and finish the QNetworkReply."""
|
"""Write buffered data to disk and finish the QNetworkReply."""
|
||||||
log.downloads.debug("Doing delayed write...")
|
log.downloads.debug("Finishing download...")
|
||||||
self._do_delayed_write = False
|
|
||||||
if self._reply.isOpen():
|
if self._reply.isOpen():
|
||||||
self.fileobj.write(self._reply.readAll())
|
self.fileobj.write(self._reply.readAll())
|
||||||
if self.autoclose:
|
if self.autoclose:
|
||||||
@ -319,20 +362,6 @@ class DownloadItem(QObject):
|
|||||||
self.finished.emit()
|
self.finished.emit()
|
||||||
log.downloads.debug("Download finished")
|
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()
|
@pyqtSlot()
|
||||||
def on_reply_finished(self):
|
def on_reply_finished(self):
|
||||||
"""Clean up when the download was finished.
|
"""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
|
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.
|
as well. Therefore, we can't close() the QNetworkReply in here yet.
|
||||||
"""
|
"""
|
||||||
self._bytes_done = self._bytes_total
|
self.stats.finish()
|
||||||
self.timer.stop()
|
|
||||||
is_redirected = self._handle_redirect()
|
is_redirected = self._handle_redirect()
|
||||||
if is_redirected:
|
if is_redirected:
|
||||||
return
|
return
|
||||||
if self._is_cancelled:
|
if self._reply is None:
|
||||||
return
|
return
|
||||||
log.downloads.debug("Reply finished, fileobj {}".format(self.fileobj))
|
log.downloads.debug("Reply finished, fileobj {}".format(self.fileobj))
|
||||||
if self.fileobj is None:
|
if self.fileobj is not None:
|
||||||
# We'll handle emptying the buffer and cleaning up as soon as the
|
|
||||||
# filename is set.
|
|
||||||
self._do_delayed_write = True
|
|
||||||
else:
|
|
||||||
# We can do a "delayed" write immediately to empty the buffer and
|
# We can do a "delayed" write immediately to empty the buffer and
|
||||||
# clean up.
|
# clean up.
|
||||||
self.delayed_write()
|
self.finish_download()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def on_ready_read(self):
|
def on_ready_read(self):
|
||||||
@ -408,15 +432,6 @@ class DownloadItem(QObject):
|
|||||||
reply.deleteLater() # the old one
|
reply.deleteLater() # the old one
|
||||||
return True
|
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):
|
class DownloadManager(QAbstractListModel):
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user