Add basic download info to view

This commit is contained in:
Florian Bruhin 2014-06-12 23:29:34 +02:00
parent 2ffc9bb00a
commit 8c673ee66c
4 changed files with 80 additions and 24 deletions

View File

@ -26,7 +26,7 @@ import qutebrowser.config.config as config
import qutebrowser.utils.message as message import qutebrowser.utils.message as message
from qutebrowser.utils.log import downloads as logger from qutebrowser.utils.log import downloads as logger
from qutebrowser.utils.usertypes import PromptMode from qutebrowser.utils.usertypes import PromptMode
from qutebrowser.utils.misc import interpolate_color from qutebrowser.utils.misc import interpolate_color, format_seconds
class DownloadItem(QObject): class DownloadItem(QObject):
@ -45,21 +45,17 @@ class DownloadItem(QObject):
fileobj: The file object to download the file to. fileobj: The file object to download the file to.
_last_done: The count of bytes which where downloaded when calculating _last_done: The count of bytes which where downloaded when calculating
the speed the last time. the speed the last time.
_last_percentage: The remembered percentage for percentage_changed. _last_percentage: The remembered percentage for data_changed.
Signals: Signals:
speed_changed: The download speed changed. data_changed: The downloads metadata changed.
arg: The speed in bytes/s
percentage_changed: The download percentage changed.
arg: The new percentage, -1 if unknown.
finished: The download was finished. finished: The download was finished.
error: An error with the download occured. error: An error with the download occured.
arg: The error message as string. arg: The error message as string.
""" """
REFRESH_INTERVAL = 200 REFRESH_INTERVAL = 200
speed_changed = pyqtSignal(float) data_changed = pyqtSignal()
percentage_changed = pyqtSignal(int)
finished = pyqtSignal() finished = pyqtSignal()
error = pyqtSignal(str) error = pyqtSignal(str)
@ -74,6 +70,7 @@ class DownloadItem(QObject):
self.bytes_done = None self.bytes_done = None
self.bytes_total = None self.bytes_total = None
self.speed = None self.speed = None
self.basename = '???'
self.fileobj = None self.fileobj = None
self._do_delayed_write = False self._do_delayed_write = False
self._last_done = None self._last_done = None
@ -88,6 +85,19 @@ class DownloadItem(QObject):
self.timer.setInterval(self.REFRESH_INTERVAL) self.timer.setInterval(self.REFRESH_INTERVAL)
self.timer.start() self.timer.start()
def __str__(self):
"""Get the download as a string.
Example: foo.pdf [699.2K/s|0.34|16%|4.253/25.124]
"""
perc = 0 if self.percentage is None else round(self.percentage)
remaining = (format_seconds(self.remaining_time)
if self.remaining_time is not None else
'?')
return '{name} [{speed}|{remaining}|{perc: 2}%|{down}/{total}]'.format(
name=self.basename, speed=self.speed, remaining=remaining,
perc=perc, down=self.bytes_done, total=self.bytes_total)
def _die(self, msg): def _die(self, msg):
"""Abort the download and emit an error.""" """Abort the download and emit an error."""
self.error.emit(msg) self.error.emit(msg)
@ -112,6 +122,15 @@ class DownloadItem(QObject):
else: else:
return 100 * self.bytes_done / self.bytes_total return 100 * self.bytes_done / self.bytes_total
@property
def remaining_time(self):
"""Property to get the remaining download time in seconds."""
if (self.bytes_total is None or self.bytes_total <= 0 or
self.speed is None or self.speed == 0):
return None
return (self.bytes_total - self.bytes_done) / self.speed
def bg_color(self): def bg_color(self):
"""Background color to be shown.""" """Background color to be shown."""
start = config.get('colors', 'download.bg.start') start = config.get('colors', 'download.bg.start')
@ -139,6 +158,7 @@ class DownloadItem(QObject):
if self.fileobj is not None: if self.fileobj is not None:
raise ValueError("Filename was already set! filename: {}, " raise ValueError("Filename was already set! filename: {}, "
"existing: {}".format(filename, self.fileobj)) "existing: {}".format(filename, self.fileobj))
self.basename = os.path.basename(filename)
try: try:
self.fileobj = open(filename, 'wb') self.fileobj = open(filename, 'wb')
if self._do_delayed_write: if self._do_delayed_write:
@ -174,11 +194,7 @@ class DownloadItem(QObject):
""" """
self.bytes_done = bytes_done self.bytes_done = bytes_done
self.bytes_total = bytes_total self.bytes_total = bytes_total
perc = round(self.percentage) self.data_changed.emit()
if perc != self._last_percentage:
logger.debug("{}% downloaded".format(perc))
self.percentage_changed.emit(perc)
self._last_percentage = perc
@pyqtSlot() @pyqtSlot()
def on_reply_finished(self): def on_reply_finished(self):
@ -233,7 +249,7 @@ class DownloadItem(QObject):
self.speed = delta * 1000 / self.REFRESH_INTERVAL self.speed = delta * 1000 / self.REFRESH_INTERVAL
logger.debug("Download speed: {} bytes/sec".format(self.speed)) logger.debug("Download speed: {} bytes/sec".format(self.speed))
self._last_done = self.bytes_done self._last_done = self.bytes_done
self.speed_changed.emit(self.speed) self.data_changed.emit()
class DownloadManager(QObject): class DownloadManager(QObject):
@ -296,21 +312,21 @@ class DownloadManager(QObject):
""" """
suggested_filename = self._get_filename(reply) suggested_filename = self._get_filename(reply)
download_location = config.get('storage', 'download-directory') download_location = config.get('storage', 'download-directory')
suggested_filename = os.path.join(download_location, suggested_filepath = os.path.join(download_location,
suggested_filename) suggested_filename)
logger.debug("fetch: {} -> {}".format(reply.url(), suggested_filename)) logger.debug("fetch: {} -> {}".format(reply.url(), suggested_filepath))
download = DownloadItem(reply) download = DownloadItem(reply)
download.finished.connect(self.on_finished) download.finished.connect(self.on_finished)
download.percentage_changed.connect(self.on_data_changed) download.data_changed.connect(self.on_data_changed)
download.speed_changed.connect(self.on_data_changed)
download.error.connect(self.on_error) download.error.connect(self.on_error)
download.basename = suggested_filename
self.download_about_to_be_added.emit(len(self.downloads) + 1) self.download_about_to_be_added.emit(len(self.downloads) + 1)
self.downloads.append(download) self.downloads.append(download)
self.download_added.emit() self.download_added.emit()
message.question("Save file to:", mode=PromptMode.text, message.question("Save file to:", mode=PromptMode.text,
handler=download.set_filename, handler=download.set_filename,
cancelled_handler=download.cancel, cancelled_handler=download.cancel,
default=suggested_filename) default=suggested_filepath)
@pyqtSlot() @pyqtSlot()
def on_finished(self): def on_finished(self):

View File

@ -69,11 +69,7 @@ class DownloadModel(QAbstractListModel):
except IndexError: except IndexError:
return QVariant() return QVariant()
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
perc = item.percentage data = str(item)
if perc is None:
data = QVariant()
else:
data = str(round(perc)) # FIXME
elif role == Qt.ForegroundRole: elif role == Qt.ForegroundRole:
data = config.get('colors', 'download.fg') data = config.get('colors', 'download.fg')
elif role == Qt.BackgroundRole: elif role == Qt.BackgroundRole:

View File

@ -525,5 +525,33 @@ class InterpolateColorTests(TestCase):
self.assertEqual(color, expected) self.assertEqual(color, expected)
class FormatSecondsTests(TestCase):
"""Tests for format_seconds.
Class attributes:
TESTS: A list of (input, output) tuples.
"""
TESTS = [
(-1, '-0:01'),
(0, '0:00'),
(59, '0:59'),
(60, '1:00'),
(60.4, '1:00'),
(61, '1:01'),
(-61, '-1:01'),
(3599, '59:59'),
(3600, '1:00:00'),
(3601, '1:00:01'),
(36000, '10:00:00'),
]
def test_format_seconds(self):
"""Test format_seconds with several tests."""
for seconds, out in self.TESTS:
self.assertEqual(utils.format_seconds(seconds), out, seconds)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -354,3 +354,19 @@ def interpolate_color(start, end, percent, colorspace=QColor.Rgb):
else: else:
raise ValueError("Invalid colorspace!") raise ValueError("Invalid colorspace!")
return out.convertTo(start.spec()) return out.convertTo(start.spec())
def format_seconds(total_seconds):
"""Format a count of seconds to get a [H:]M:SS string."""
prefix = '-' if total_seconds < 0 else ''
hours, rem = divmod(abs(round(total_seconds)), 3600)
minutes, seconds = divmod(rem, 60)
chunks = []
if hours:
chunks.append(str(hours))
min_format = '{:02}'
else:
min_format = '{}'
chunks.append(min_format.format(minutes))
chunks.append('{:02}'.format(seconds))
return prefix + ':'.join(chunks)