Add estimated time remaining to progress indicator

Estimates the time remaining for each download and displays it in the
progress bar. Waits for 10 seconds before showing the progress bar to
allow the download rate to stabilize, which prevents the estimated time
remaining from jumping all over the place at the start of the download
(a.k.a the "Windows copy dialog experience"). If your download takes
less than 10 seconds, you don't really need to see an ETA anyway.

This commit also refactors the Downloads class, splitting out the
download-specific functionality into a new Download class, providing
better encapsulation. As a result, I was able to simplify the call to
`update_download` because it was no longer necessary to pass the
`total_bytes` (which don't change after the download has begun).

Tested on Mac OS 10.9.
This commit is contained in:
Garrett Robinson 2015-12-23 00:57:46 -05:00
parent 41a30dd4e4
commit ea47e80f14
3 changed files with 103 additions and 29 deletions

View File

@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import os, inspect, hashlib, base64, hmac, platform, zipfile, tempfile
from itertools import izip
import math
import time
# hack to make unicode filenames work (#141)
import sys
@ -112,6 +114,44 @@ def human_readable_filesize(b):
return '{0:.1f} {1:s}'.format(round(b, 1), units[u])
def format_seconds(seconds):
"""Return a human-readable string of the format 1d2h3m4s"""
seconds_in_a_minute = 60
seconds_in_an_hour = seconds_in_a_minute * 60
seconds_in_a_day = seconds_in_an_hour * 24
days = math.floor(seconds / seconds_in_a_day)
hour_seconds = seconds % seconds_in_a_day
hours = math.floor(hour_seconds / seconds_in_an_hour)
minute_seconds = hour_seconds % seconds_in_an_hour
minutes = math.floor(minute_seconds / seconds_in_a_minute)
remaining_seconds = minute_seconds % seconds_in_a_minute
seconds = math.ceil(remaining_seconds)
human_readable = []
if days > 0:
human_readable.append("{}d".format(int(days)))
if hours > 0:
human_readable.append("{}h".format(int(hours)))
if minutes > 0:
human_readable.append("{}m".format(int(minutes)))
if seconds > 0:
human_readable.append("{}s".format(int(seconds)))
return ''.join(human_readable)
def estimated_time_remaining(bytes_downloaded, total_bytes, started):
now = time.time()
time_elapsed = now - started # in seconds
download_rate = bytes_downloaded / time_elapsed
remaining_bytes = total_bytes - bytes_downloaded
eta = remaining_bytes / download_rate
return format_seconds(eta)
def is_root():
"""
Returns if user is root.

View File

@ -17,12 +17,67 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import time
from PyQt4 import QtCore, QtGui
import common
from onionshare import strings, helpers
class Download(object):
def __init__(self, download_id, total_bytes):
self.download_id = download_id
self.started = time.time()
self.total_bytes = total_bytes
self.downloaded_bytes = 0
# make a new progress bar
self.progress_bar = QtGui.QProgressBar()
self.progress_bar.setTextVisible(True)
self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter)
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(total_bytes)
self.progress_bar.setValue(0)
self.progress_bar.setStyleSheet(
"QProgressBar::chunk { background-color: #05B8CC; }")
self.progress_bar.total_bytes = total_bytes
# start at 0
self.update(0)
def update(self, downloaded_bytes):
self.downloaded_bytes = downloaded_bytes
self.progress_bar.setValue(downloaded_bytes)
if downloaded_bytes == self.progress_bar.total_bytes:
pb_fmt = "%p%"
else:
elapsed = time.time() - self.started
if elapsed < 10:
# Wait a couple of seconds for the download rate to stabilize.
# This prevents an "Windows copy dialog"-esque experience at
# the beginning of the download.
pb_fmt = "{0:s}, %p% (Computing ETA)".format(
helpers.human_readable_filesize(downloaded_bytes))
else:
pb_fmt = "{0:s}, ETA: {1:s}, %p%".format(
helpers.human_readable_filesize(downloaded_bytes),
self.estimated_time_remaining)
self.progress_bar.setFormat(pb_fmt)
def cancel(self):
self.progress_bar.setFormat(strings._('gui_canceled'))
@property
def estimated_time_remaining(self):
return helpers.estimated_time_remaining(self.downloaded_bytes,
self.total_bytes,
self.started)
class Downloads(QtGui.QVBoxLayout):
"""
The downloads chunk of the GUI. This lists all of the active download
@ -31,7 +86,7 @@ class Downloads(QtGui.QVBoxLayout):
def __init__(self):
super(Downloads, self).__init__()
self.progress_bars = {}
self.downloads = {}
# downloads label
self.downloads_label = QtGui.QLabel(strings._('gui_downloads', True))
@ -46,40 +101,19 @@ class Downloads(QtGui.QVBoxLayout):
"""
self.downloads_label.show()
# make a new progress bar
pb = QtGui.QProgressBar()
pb.setTextVisible(True)
pb.setAlignment(QtCore.Qt.AlignHCenter)
pb.setMinimum(0)
pb.setMaximum(total_bytes)
pb.setValue(0)
pb.setStyleSheet("QProgressBar::chunk { background-color: #05B8CC; }")
pb.total_bytes = total_bytes
# add it to the list
self.progress_bars[download_id] = pb
self.addWidget(pb)
download = Download(download_id, total_bytes)
self.downloads[download_id] = download
self.addWidget(download.progress_bar)
# start at 0
self.update_download(download_id, total_bytes, 0)
def update_download(self, download_id, total_bytes, downloaded_bytes):
def update_download(self, download_id, downloaded_bytes):
"""
Update the progress of a download progress bar.
"""
if download_id not in self.progress_bars:
self.add_download(download_id, total_bytes)
pb = self.progress_bars[download_id]
pb.setValue(downloaded_bytes)
if downloaded_bytes == pb.total_bytes:
pb.setFormat("%p%")
else:
pb.setFormat("{0:s}, %p%".format(helpers.human_readable_filesize(downloaded_bytes)))
self.downloads[download_id].update(downloaded_bytes)
def cancel_download(self, download_id):
"""
Update a download progress bar to show that it has been canceled.
"""
pb = self.progress_bars[download_id]
pb.setFormat(strings._('gui_canceled'))
self.downloads[download_id].cancel()

View File

@ -218,7 +218,7 @@ class OnionShareGui(QtGui.QWidget):
self.downloads.add_download(event["data"]["id"], web.zip_filesize)
elif event["type"] == web.REQUEST_PROGRESS:
self.downloads.update_download(event["data"]["id"], web.zip_filesize, event["data"]["bytes"])
self.downloads.update_download(event["data"]["id"], event["data"]["bytes"])
# is the download complete?
if event["data"]["bytes"] == web.zip_filesize: