Sat, 04 May 2019 11:10:44 +0200
Merged with default branch to prepare release 19.5.
# -*- coding: utf-8 -*- # Copyright (c) 2010 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a widget controlling a download. """ from __future__ import unicode_literals import os from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QTime, QUrl, \ QStandardPaths, QFileInfo, QDateTime from PyQt5.QtGui import QPalette, QDesktopServices from PyQt5.QtWidgets import QWidget, QStyle, QDialog from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem from E5Gui import E5FileDialog from .Ui_DownloadItem import Ui_DownloadItem from .DownloadUtilities import timeString, dataString, speedString from WebBrowser.WebBrowserWindow import WebBrowserWindow import UI.PixmapCache import Utilities.MimeTypes import Globals from Globals import qVersionTuple class DownloadItem(QWidget, Ui_DownloadItem): """ Class implementing a widget controlling a download. @signal statusChanged() emitted upon a status change of a download @signal downloadFinished(success) emitted when a download finished @signal progress(int, int) emitted to signal the download progress """ statusChanged = pyqtSignal() downloadFinished = pyqtSignal(bool) progress = pyqtSignal(int, int) Downloading = 0 DownloadSuccessful = 1 DownloadCancelled = 2 def __init__(self, downloadItem=None, pageUrl=None, parent=None): """ Constructor @param downloadItem reference to the download object containing the download data. @type QWebEngineDownloadItem @param pageUrl URL of the calling page @type QUrl @param parent reference to the parent widget @type QWidget """ super(DownloadItem, self).__init__(parent) self.setupUi(self) p = self.infoLabel.palette() p.setColor(QPalette.Text, Qt.darkGray) self.infoLabel.setPalette(p) self.progressBar.setMaximum(0) self.pauseButton.setIcon(UI.PixmapCache.getIcon("pause.png")) self.stopButton.setIcon(UI.PixmapCache.getIcon("stopLoading.png")) self.openButton.setIcon(UI.PixmapCache.getIcon("open.png")) self.openButton.setEnabled(False) self.openButton.setVisible(False) if not hasattr(QWebEngineDownloadItem, "pause"): # pause/resume was defined in Qt 5.10.0 / PyQt 5.10.0 self.pauseButton.setEnabled(False) self.pauseButton.setVisible(False) self.__state = DownloadItem.Downloading icon = self.style().standardIcon(QStyle.SP_FileIcon) self.fileIcon.setPixmap(icon.pixmap(48, 48)) self.__downloadItem = downloadItem if pageUrl is None: self.__pageUrl = QUrl() else: self.__pageUrl = pageUrl self.__bytesReceived = 0 self.__bytesTotal = -1 self.__downloadTime = QTime() self.__fileName = "" self.__originalFileName = "" self.__finishedDownloading = False self.__gettingFileName = False self.__canceledFileSelect = False self.__autoOpen = False self.__downloadedDateTime = QDateTime() self.__initialize() def __initialize(self): """ Private method to initialize the widget. """ if self.__downloadItem is None: return self.__finishedDownloading = False self.__bytesReceived = 0 self.__bytesTotal = -1 # start timer for the download estimation self.__downloadTime.start() # attach to the download item object self.__url = self.__downloadItem.url() self.__downloadItem.downloadProgress.connect(self.__downloadProgress) self.__downloadItem.finished.connect(self.__finished) # reset info self.datetimeLabel.clear() self.datetimeLabel.hide() self.infoLabel.clear() self.progressBar.setValue(0) if self.__downloadItem.state() == \ QWebEngineDownloadItem.DownloadRequested: self.__getFileName() if not self.__fileName: self.__downloadItem.cancel() else: self.__downloadItem.setPath(self.__fileName) self.__downloadItem.accept() else: fileName = self.__downloadItem.path() self.__setFileName(fileName) def __getFileName(self): """ Private method to get the file name to save to from the user. """ if self.__gettingFileName: return if qVersionTuple() >= (5, 8, 0) and PYQT_VERSION >= 0x50800: savePage = self.__downloadItem.type() == \ QWebEngineDownloadItem.SavePage elif qVersionTuple() >= (5, 7, 0) and PYQT_VERSION >= 0x50700: savePage = self.__downloadItem.savePageFormat() != \ QWebEngineDownloadItem.UnknownSaveFormat else: savePage = self.__downloadItem.path().lower().endswith( (".mhtml", ".mht")) documentLocation = QStandardPaths.writableLocation( QStandardPaths.DocumentsLocation) downloadDirectory = WebBrowserWindow\ .downloadManager().downloadDirectory() if self.__fileName: fileName = self.__fileName originalFileName = self.__originalFileName self.__toDownload = True ask = False else: defaultFileName, originalFileName = \ self.__saveFileName( documentLocation if savePage else downloadDirectory) fileName = defaultFileName self.__originalFileName = originalFileName ask = True self.__autoOpen = False if not savePage: from .DownloadAskActionDialog import DownloadAskActionDialog url = self.__downloadItem.url() mimetype = Utilities.MimeTypes.mimeType(originalFileName) dlg = DownloadAskActionDialog( QFileInfo(originalFileName).fileName(), mimetype, "{0}://{1}".format(url.scheme(), url.authority()), self) if dlg.exec_() == QDialog.Rejected or dlg.getAction() == "cancel": self.progressBar.setVisible(False) self.on_stopButton_clicked() self.filenameLabel.setText( self.tr("Download canceled: {0}").format( QFileInfo(defaultFileName).fileName())) self.__canceledFileSelect = True self.__setDateTime() return if dlg.getAction() == "scan": self.__mainWindow.requestVirusTotalScan(url) self.progressBar.setVisible(False) self.on_stopButton_clicked() self.filenameLabel.setText( self.tr("VirusTotal scan scheduled: {0}").format( QFileInfo(defaultFileName).fileName())) self.__canceledFileSelect = True return self.__autoOpen = dlg.getAction() == "open" tempLocation = QStandardPaths.writableLocation( QStandardPaths.TempLocation) fileName = tempLocation + '/' + \ QFileInfo(fileName).completeBaseName() if ask and not self.__autoOpen: self.__gettingFileName = True fileName = E5FileDialog.getSaveFileName( None, self.tr("Save File"), defaultFileName, "") self.__gettingFileName = False else: # save page file name and format selection for Qt < 5.8.0 self.__autoOpen = False filterList = [ self.tr("Web Archive (*.mhtml *.mht)"), self.tr("HTML File (*.html *.htm)"), self.tr("HTML File with all resources (*.html *.htm)"), ] extensionsList = [ # tuple of extensions for *nix and Windows # keep in sync with filters list (".mhtml", ".mht"), (".html", ".htm"), (".html", ".htm"), ] self.__gettingFileName = True fileName, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( None, self.tr("Save Web Page"), defaultFileName, ";;".join(filterList), None) self.__gettingFileName = False if fileName: index = filterList.index(selectedFilter) if index == 0: self.__downloadItem.setSavePageFormat( QWebEngineDownloadItem.MimeHtmlSaveFormat) elif index == 1: self.__downloadItem.setSavePageFormat( QWebEngineDownloadItem.SingleHtmlSaveFormat) else: self.__downloadItem.setSavePageFormat( QWebEngineDownloadItem.CompleteHtmlSaveFormat) extension = os.path.splitext(fileName)[1] if not extension: # add the platform specific default extension if Globals.isWindowsPlatform(): extensionsIndex = 1 else: extensionsIndex = 0 extensions = extensionsList[index] fileName += extensions[extensionsIndex] if not fileName: self.progressBar.setVisible(False) self.on_stopButton_clicked() self.filenameLabel.setText( self.tr("Download canceled: {0}") .format(QFileInfo(defaultFileName).fileName())) self.__canceledFileSelect = True self.__setDateTime() return self.__setFileName(fileName) def __setFileName(self, fileName): """ Private method to set the file name to save the download into. @param fileName name of the file to save into @type str """ fileInfo = QFileInfo(fileName) WebBrowserWindow.downloadManager()\ .setDownloadDirectory(fileInfo.absoluteDir().absolutePath()) self.filenameLabel.setText(fileInfo.fileName()) self.__fileName = fileName # check file path for saving saveDirPath = QFileInfo(self.__fileName).dir() if not saveDirPath.exists(): if not saveDirPath.mkpath(saveDirPath.absolutePath()): self.progressBar.setVisible(False) self.on_stopButton_clicked() self.infoLabel.setText(self.tr( "Download directory ({0}) couldn't be created.") .format(saveDirPath.absolutePath())) self.__setDateTime() return self.filenameLabel.setText(QFileInfo(self.__fileName).fileName()) def __saveFileName(self, directory): """ Private method to calculate a name for the file to download. @param directory name of the directory to store the file into (string) @return proposed filename and original filename (string, string) """ path = self.__downloadItem.path() info = QFileInfo(path) baseName = info.completeBaseName() endName = info.suffix() origName = baseName if endName: origName += '.' + endName name = os.path.join(directory, baseName) if endName: name += '.' + endName return name, origName @pyqtSlot(bool) def on_pauseButton_clicked(self, checked): """ Private slot to pause the download. @param checked flag indicating the state of the button @type bool """ if checked: self.__downloadItem.pause() else: self.__downloadItem.resume() @pyqtSlot() def on_stopButton_clicked(self): """ Private slot to stop the download. """ self.cancelDownload() def cancelDownload(self): """ Public slot to stop the download. """ self.setUpdatesEnabled(False) self.stopButton.setEnabled(False) self.stopButton.setVisible(False) self.openButton.setEnabled(False) self.openButton.setVisible(False) self.pauseButton.setEnabled(False) self.pauseButton.setVisible(False) self.setUpdatesEnabled(True) self.__state = DownloadItem.DownloadCancelled self.__downloadItem.cancel() self.__setDateTime() self.downloadFinished.emit(False) @pyqtSlot() def on_openButton_clicked(self): """ Private slot to open the downloaded file. """ self.openFile() def openFile(self): """ Public slot to open the downloaded file. """ info = QFileInfo(self.__fileName) url = QUrl.fromLocalFile(info.absoluteFilePath()) QDesktopServices.openUrl(url) def openFolder(self): """ Public slot to open the folder containing the downloaded file. """ info = QFileInfo(self.__fileName) url = QUrl.fromLocalFile(info.absolutePath()) QDesktopServices.openUrl(url) def __downloadProgress(self, bytesReceived, bytesTotal): """ Private method to show the download progress. @param bytesReceived number of bytes received (integer) @param bytesTotal number of total bytes (integer) """ self.__bytesReceived = bytesReceived self.__bytesTotal = bytesTotal currentValue = 0 totalValue = 0 if bytesTotal > 0: currentValue = bytesReceived * 100 / bytesTotal totalValue = 100 self.progressBar.setValue(currentValue) self.progressBar.setMaximum(totalValue) self.progress.emit(currentValue, totalValue) self.__updateInfoLabel() def downloadProgress(self): """ Public method to get the download progress. @return current download progress @rtype int """ return self.progressBar.value() def bytesTotal(self): """ Public method to get the total number of bytes of the download. @return total number of bytes (integer) """ if self.__bytesTotal == -1: self.__bytesTotal = self.__downloadItem.totalBytes() return self.__bytesTotal def bytesReceived(self): """ Public method to get the number of bytes received. @return number of bytes received (integer) """ return self.__bytesReceived def remainingTime(self): """ Public method to get an estimation for the remaining time. @return estimation for the remaining time (float) """ if not self.downloading(): return -1.0 if self.bytesTotal() == -1: return -1.0 cSpeed = self.currentSpeed() if cSpeed != 0: timeRemaining = (self.bytesTotal() - self.bytesReceived()) / cSpeed else: timeRemaining = 1 # ETA should never be 0 if timeRemaining == 0: timeRemaining = 1 return timeRemaining def currentSpeed(self): """ Public method to get an estimation for the download speed. @return estimation for the download speed (float) """ if not self.downloading(): return -1.0 return self.__bytesReceived * 1000.0 / self.__downloadTime.elapsed() def __updateInfoLabel(self): """ Private method to update the info label. """ bytesTotal = self.bytesTotal() running = not self.downloadedSuccessfully() speed = self.currentSpeed() timeRemaining = self.remainingTime() info = "" if running: remaining = "" if bytesTotal > 0: remaining = timeString(timeRemaining) info = self.tr("{0} of {1} ({2}/sec) {3}")\ .format( dataString(self.__bytesReceived), bytesTotal == -1 and self.tr("?") or dataString(bytesTotal), speedString(speed), remaining) else: if self.__bytesReceived == bytesTotal or bytesTotal == -1: info = self.tr("{0} downloaded")\ .format(dataString(self.__bytesReceived)) else: info = self.tr("{0} of {1} - Stopped")\ .format(dataString(self.__bytesReceived), dataString(bytesTotal)) self.infoLabel.setText(info) def downloading(self): """ Public method to determine, if a download is in progress. @return flag indicating a download is in progress (boolean) """ return self.__state == DownloadItem.Downloading def downloadedSuccessfully(self): """ Public method to check for a successful download. @return flag indicating a successful download (boolean) """ return self.__state == DownloadItem.DownloadSuccessful def downloadCanceled(self): """ Public method to check, if the download was cancelled. @return flag indicating a canceled download (boolean) """ return self.__state == DownloadItem.DownloadCancelled def __finished(self): """ Private slot to handle the download finished. """ self.__finishedDownloading = True noError = (self.__downloadItem.state() == QWebEngineDownloadItem.DownloadCompleted) self.progressBar.setVisible(False) self.pauseButton.setEnabled(False) self.pauseButton.setVisible(False) self.stopButton.setEnabled(False) self.stopButton.setVisible(False) self.openButton.setEnabled(noError) self.openButton.setVisible(noError) self.__state = DownloadItem.DownloadSuccessful self.__updateInfoLabel() self.__setDateTime() self.__adjustSize() self.statusChanged.emit() self.downloadFinished.emit(True) if self.__autoOpen: self.openFile() def canceledFileSelect(self): """ Public method to check, if the user canceled the file selection. @return flag indicating cancellation (boolean) """ return self.__canceledFileSelect def setIcon(self, icon): """ Public method to set the download icon. @param icon reference to the icon to be set (QIcon) """ self.fileIcon.setPixmap(icon.pixmap(48, 48)) def fileName(self): """ Public method to get the name of the output file. @return name of the output file (string) """ return self.__fileName def absoluteFilePath(self): """ Public method to get the absolute path of the output file. @return absolute path of the output file (string) """ return QFileInfo(self.__fileName).absoluteFilePath() def getData(self): """ Public method to get the relevant download data. @return dictionary containing the URL, save location, done flag, the URL of the related web page and the date and time of the download @rtype dict of {"URL": QUrl, "Location": str, "Done": bool, "PageURL": QUrl, "Downloaded": QDateTime} """ return { "URL": self.__url, "Location": QFileInfo(self.__fileName).filePath(), "Done": self.downloadedSuccessfully(), "PageURL": self.__pageUrl, "Downloaded": self.__downloadedDateTime } def setData(self, data): """ Public method to set the relevant download data. @param data dictionary containing the URL, save location, done flag, the URL of the related web page and the date and time of the download @type dict of {"URL": QUrl, "Location": str, "Done": bool, "PageURL": QUrl, "Downloaded": QDateTime} """ self.__url = data["URL"] self.__fileName = data["Location"] self.__pageUrl = data["PageURL"] self.filenameLabel.setText(QFileInfo(self.__fileName).fileName()) self.infoLabel.setText(self.__fileName) try: self.__setDateTime(data["Downloaded"]) except KeyError: self.__setDateTime(QDateTime()) self.pauseButton.setEnabled(False) self.pauseButton.setVisible(False) self.stopButton.setEnabled(False) self.stopButton.setVisible(False) self.openButton.setEnabled(data["Done"]) self.openButton.setVisible(data["Done"]) if data["Done"]: self.__state = DownloadItem.DownloadSuccessful else: self.__state = DownloadItem.DownloadCancelled self.progressBar.setVisible(False) self.__adjustSize() def getInfoData(self): """ Public method to get the text of the info label. @return text of the info label (string) """ return self.infoLabel.text() def getPageUrl(self): """ Public method to get the URL of the download page. @return URL of the download page (QUrl) """ return self.__pageUrl def __adjustSize(self): """ Private method to adjust the size of the download item. """ self.ensurePolished() msh = self.minimumSizeHint() self.resize(max(self.width(), msh.width()), msh.height()) def __setDateTime(self, dateTime=None): """ Private method to set the download date and time. @param dateTime date and time to be set @type QDateTime """ if dateTime is None: self.__downloadedDateTime = QDateTime.currentDateTime() else: self.__downloadedDateTime = dateTime if self.__downloadedDateTime.isValid(): labelText = self.__downloadedDateTime.toString("yyyy-MM-dd hh:mm") self.datetimeLabel.setText(labelText) self.datetimeLabel.show() else: self.datetimeLabel.clear() self.datetimeLabel.hide()