Provide realistic progress and estimated time

This commit is contained in:
Gabriel Ferreira
2021-07-23 16:00:25 -03:00
committed by Philipp Wolfer
parent 5b80c72d40
commit c88273ee24
13 changed files with 19657 additions and 18137 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,8 @@
#
# Copyright (C) 2013, 2018, 2020 Laurent Monin
# Copyright (C) 2016-2017 Sambhav Kothari
# Copyright (C) 2019 Philipp Wolfer
# Copyright (C) 2019, 2021 Philipp Wolfer
# Copyright (C) 2021 Gabriel Ferreira
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -21,6 +22,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import time
from PyQt5 import (
QtCore,
QtGui,
@@ -28,6 +31,7 @@ from PyQt5 import (
)
from picard.util import icontheme
from picard.util.time import get_timestamp
from picard.ui.ui_infostatus import Ui_InfoStatus
@@ -43,25 +47,31 @@ class InfoStatus(QtWidgets.QWidget, Ui_InfoStatus):
self._create_icons()
self._init_labels()
self.reset_counters()
def _init_labels(self):
size = self._size
self.label1.setPixmap(self.icon_file.pixmap(size))
self.label2.setPixmap(self.icon_cd.pixmap(size))
self.label3.setPixmap(self.icon_file_pending.pixmap(size))
self.label4.setPixmap(self.icon_download.pixmap(size, QtGui.QIcon.Disabled))
self.label1.setPixmap(self.icon_eta.pixmap(size))
self.label1.hide()
self.label2.setPixmap(self.icon_file.pixmap(size))
self.label3.setPixmap(self.icon_cd.pixmap(size))
self.label4.setPixmap(self.icon_file_pending.pixmap(size))
self.label5.setPixmap(self.icon_download.pixmap(size, QtGui.QIcon.Disabled))
self._init_tooltips()
def _create_icons(self):
self.icon_eta = QtGui.QIcon(':/images/22x22/hourglass.png')
self.icon_cd = icontheme.lookup('media-optical')
self.icon_file = QtGui.QIcon(":/images/file.png")
self.icon_file_pending = QtGui.QIcon(":/images/file-pending.png")
self.icon_download = QtGui.QIcon(":/images/16x16/action-go-down-16.png")
def _init_tooltips(self):
t1 = _("Files")
t2 = _("Albums")
t3 = _("Pending files")
t4 = _("Pending requests")
t1 = _("Estimated Time")
t2 = _("Files")
t3 = _("Albums")
t4 = _("Pending files")
t5 = _("Pending requests")
self.val1.setToolTip(t1)
self.label1.setToolTip(t1)
self.val2.setToolTip(t2)
@@ -70,26 +80,98 @@ class InfoStatus(QtWidgets.QWidget, Ui_InfoStatus):
self.label3.setToolTip(t3)
self.val4.setToolTip(t4)
self.label4.setToolTip(t4)
self.val5.setToolTip(t5)
self.label5.setToolTip(t5)
def update(self, files=0, albums=0, pending_files=0, pending_requests=0):
def update(self, files=0, albums=0, pending_files=0, pending_requests=0, progress=0):
self.set_files(files)
self.set_albums(albums)
self.set_pending_files(pending_files)
self.set_pending_requests(pending_requests)
def set_files(self, num):
self.val1.setText(str(num))
# estimate eta
total_pending = pending_files + pending_requests
last_pending = self._last_pending_files + self._last_pending_requests
def set_albums(self, num):
if self._max_pending_files == 0 or (self._max_pending_files > 0
and self._last_pending_files == 0
and pending_files == 0):
# update starting timestamp when pending_files appear in order to discard the idle time
# and when all previous file requests already finished but network requests still ongoing
self.reset_file_counters()
previous_done_files = max(0, self._max_pending_files - self._last_pending_files)
previous_done_requests = max(0, self._max_pending_requests - self._last_pending_requests)
self._max_pending_files = max(self._max_pending_files, previous_done_files + pending_files)
self._max_pending_requests = max(self._max_pending_requests, previous_done_requests + pending_requests)
self._last_pending_files = pending_files
self._last_pending_requests = pending_requests
if total_pending == 0 or (self._max_pending_files + self._max_pending_requests <= 1):
self.reset_counters()
self.hide_eta()
return
if total_pending != last_pending:
current_time = time.time()
# time since we started processing this batch
diff_time = max(0.1, current_time - self._prev_time) # denominator can't be 0
previous_done_files = max(1, previous_done_files) # denominator can't be 0
# we estimate based on the time per file * number of pending files + 1 second per additional request
file_eta_seconds = (diff_time / previous_done_files) * pending_files + pending_requests
# we assume additional network requests based on the ratio of requests/files * pending files
# to estimate an upper bound (e.g. fetch cover, lookup, scan)
network_eta_seconds = pending_requests + (previous_done_requests / previous_done_files) * pending_files
# general eta (biased towards whatever takes longer)
eta_seconds = max(network_eta_seconds, file_eta_seconds)
# estimate progress
self._last_progress = diff_time / (diff_time + eta_seconds)
self.set_eta(eta_seconds)
def reset_counters(self):
self._last_progress = 0
self._max_pending_requests = 0
self._last_pending_requests = 0
self.reset_file_counters()
def reset_file_counters(self):
self._max_pending_files = 0
self._last_pending_files = 0
self._prev_time = time.time()
def get_progress(self):
return self._last_progress
def set_eta(self, eta_seconds):
if eta_seconds > 0:
self.val1.setText(get_timestamp(eta_seconds))
self.val1.show()
self.label1.show()
else:
self.hide_eta()
def hide_eta(self):
self.val1.hide()
self.label1.hide()
def set_files(self, num):
self.val2.setText(str(num))
def set_pending_files(self, num):
def set_albums(self, num):
self.val3.setText(str(num))
def set_pending_files(self, num):
self.val4.setText(str(num))
def set_pending_requests(self, num):
if num <= 0:
enabled = QtGui.QIcon.Disabled
else:
enabled = QtGui.QIcon.Normal
self.label4.setPixmap(self.icon_download.pixmap(self._size, enabled))
self.val4.setText(str(num))
self.label5.setPixmap(self.icon_download.pixmap(self._size, enabled))
self.val5.setText(str(num))

View File

@@ -27,7 +27,7 @@
# Copyright (C) 2018 virusMac
# Copyright (C) 2018, 2021 Bob Swift
# Copyright (C) 2019 Timur Enikeev
# Copyright (C) 2020 Gabriel Ferreira
# Copyright (C) 2020-2021 Gabriel Ferreira
# Copyright (C) 2021 Petit Minion
#
# This program is free software; you can redistribute it and/or
@@ -383,6 +383,7 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry):
"""Creates a new status bar."""
self.statusBar().showMessage(_("Ready"))
infostatus = InfoStatus(self)
self._progress = infostatus.get_progress
self.listening_label = QtWidgets.QLabel()
self.listening_label.setVisible(False)
self.listening_label.setToolTip("<qt/>" + _(
@@ -404,10 +405,9 @@ class MainWindow(QtWidgets.QMainWindow, PreserveGeometry):
total_albums = len(self.tagger.albums)
pending_files = File.num_pending_files
pending_requests = self.tagger.webservice.num_pending_web_requests
for indicator in self.status_indicators:
indicator.update(files=total_files, albums=total_albums,
pending_files=pending_files, pending_requests=pending_requests)
pending_files=pending_files, pending_requests=pending_requests, progress=self._progress())
def update_statusbar_listen_port(self, listen_port):
if listen_port:

View File

@@ -36,13 +36,11 @@ class AbstractProgressStatusIndicator:
self._max_pending = 0
self._last_pending = 0
def update(self, files=0, albums=0, pending_files=0, pending_requests=0):
def update(self, files=0, albums=0, pending_files=0, pending_requests=0, progress=0):
if not self.is_available:
return
# Weight pending network requests higher as they are slower then file loads
total_pending = pending_files + 10 * pending_requests
total_pending = pending_files + pending_requests
if total_pending == self._last_pending:
return # No changes, avoid update
@@ -55,7 +53,6 @@ class AbstractProgressStatusIndicator:
self.hide_progress()
return
progress = 1 - (total_pending / self._max_pending)
self.set_progress(progress)
@property

View File

@@ -3,12 +3,14 @@
# Automatically generated - don't edit.
# Use `python setup.py build_ui` to update it.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_InfoStatus(object):
def setupUi(self, InfoStatus):
InfoStatus.setObjectName("InfoStatus")
InfoStatus.resize(350, 24)
InfoStatus.resize(683, 145)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -31,6 +33,7 @@ class Ui_InfoStatus(object):
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.label1.sizePolicy().hasHeightForWidth())
self.label1.setSizePolicy(sizePolicy)
self.label1.setMinimumSize(QtCore.QSize(0, 0))
self.label1.setFrameShape(QtWidgets.QFrame.NoFrame)
self.label1.setTextFormat(QtCore.Qt.AutoText)
self.label1.setScaledContents(False)
@@ -82,6 +85,18 @@ class Ui_InfoStatus(object):
self.label4.setScaledContents(False)
self.label4.setObjectName("label4")
self.horizontalLayout.addWidget(self.label4)
self.val5 = QtWidgets.QLabel(InfoStatus)
self.val5.setMinimumSize(QtCore.QSize(40, 0))
self.val5.setText("")
self.val5.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.val5.setObjectName("val5")
self.horizontalLayout.addWidget(self.val5)
self.label5 = QtWidgets.QLabel(InfoStatus)
self.label5.setMinimumSize(QtCore.QSize(0, 0))
self.label5.setText("")
self.label5.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.label5.setObjectName("label5")
self.horizontalLayout.addWidget(self.label5)
self.retranslateUi(InfoStatus)
QtCore.QMetaObject.connectSlotsByName(InfoStatus)
@@ -89,4 +104,3 @@ class Ui_InfoStatus(object):
def retranslateUi(self, InfoStatus):
_translate = QtCore.QCoreApplication.translate
InfoStatus.setWindowTitle(_("Form"))

49
picard/util/time.py Normal file
View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2021 Laurent Monin
# Copyright (C) 2021 Gabriel Ferreira
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
SECS_IN_DAY = 86400
SECS_IN_HOUR = 3600
SECS_IN_MINUTE = 60
def euclidian_div(a, b):
return a // b, a % b
def seconds_to_dhms(seconds):
days, seconds = euclidian_div(seconds, SECS_IN_DAY)
hours, seconds = euclidian_div(seconds, SECS_IN_HOUR)
minutes, seconds = euclidian_div(seconds, SECS_IN_MINUTE)
return days, hours, minutes, seconds
def get_timestamp(seconds):
d, h, m, s = seconds_to_dhms(seconds)
if d > 0:
return _("%.2dd %.2dh") % (d, h)
if h > 0:
return _("%.2dh %.2dm") % (h, m)
if m > 0:
return _("%.2dm %.2ds") % (m, s)
if s > 0:
return _("%.2ds") % s
return ''

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -36,6 +36,8 @@
<file>images/16x16/go-previous@2x.png</file>
<file>images/16x16/go-up.png</file>
<file>images/16x16/go-up@2x.png</file>
<file>images/16x16/hourglass.png</file>
<file>images/16x16/hourglass@2x.png</file>
<file>images/16x16/list-remove.png</file>
<file>images/16x16/list-remove@2x.png</file>
<file>images/16x16/lookup-musicbrainz.png</file>
@@ -97,6 +99,8 @@
<file>images/22x22/fingerprint@2x.png</file>
<file>images/22x22/folder.png</file>
<file>images/22x22/folder@2x.png</file>
<file>images/22x22/hourglass.png</file>
<file>images/22x22/hourglass@2x.png</file>
<file>images/22x22/list-remove.png</file>
<file>images/22x22/list-remove@2x.png</file>
<file>images/22x22/lookup-musicbrainz.png</file>

58
test/test_util_time.py Normal file
View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2021 Gabriel Ferreira
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from test.picardtestcase import PicardTestCase
from picard.util.time import (
get_timestamp,
seconds_to_dhms,
)
class UtilTimeTest(PicardTestCase):
def test_seconds_to_dhms(self):
self.assertTupleEqual(seconds_to_dhms(0), (0, 0, 0, 0))
self.assertTupleEqual(seconds_to_dhms(1), (0, 0, 0, 1))
self.assertTupleEqual(seconds_to_dhms(60), (0, 0, 1, 0))
self.assertTupleEqual(seconds_to_dhms(61), (0, 0, 1, 1))
self.assertTupleEqual(seconds_to_dhms(120), (0, 0, 2, 0))
self.assertTupleEqual(seconds_to_dhms(3599), (0, 0, 59, 59))
self.assertTupleEqual(seconds_to_dhms(3600), (0, 1, 0, 0))
self.assertTupleEqual(seconds_to_dhms(3601), (0, 1, 0, 1))
self.assertTupleEqual(seconds_to_dhms(3660), (0, 1, 1, 0))
self.assertTupleEqual(seconds_to_dhms(3661), (0, 1, 1, 1))
self.assertTupleEqual(seconds_to_dhms(86399), (0, 23, 59, 59))
self.assertTupleEqual(seconds_to_dhms(86400), (1, 0, 0, 0))
def test_get_timestamp(self):
self.assertEqual(get_timestamp(0), "")
self.assertEqual(get_timestamp(1), "01s")
self.assertEqual(get_timestamp(60), "01m 00s")
self.assertEqual(get_timestamp(61), "01m 01s")
self.assertEqual(get_timestamp(120), "02m 00s")
self.assertEqual(get_timestamp(3599), "59m 59s")
self.assertEqual(get_timestamp(3600), "01h 00m")
self.assertEqual(get_timestamp(3601), "01h 00m")
self.assertEqual(get_timestamp(3660), "01h 01m")
self.assertEqual(get_timestamp(3661), "01h 01m")
self.assertEqual(get_timestamp(86399), "23h 59m")
self.assertEqual(get_timestamp(86400), "01d 00h")

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>24</height>
<width>683</width>
<height>145</height>
</rect>
</property>
<property name="sizePolicy">
@@ -29,7 +29,16 @@
<property name="spacing">
<number>2</number>
</property>
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@@ -56,6 +65,12 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
@@ -160,6 +175,41 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="val5">
<property name="minimumSize">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label5">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>