--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/WebBrowser/Download/DownloadItem.py Sat May 04 11:10:44 2019 +0200 @@ -0,0 +1,683 @@ +# -*- 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()