--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/WebBrowser/Download/DownloadItem.py Thu Jul 07 11:23:56 2022 +0200 @@ -0,0 +1,634 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2010 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a widget controlling a download. +""" + +import enum +import os +import pathlib + +from PyQt6.QtCore import ( + pyqtSlot, pyqtSignal, QTime, QUrl, QStandardPaths, QDateTime +) +from PyQt6.QtGui import QDesktopServices +from PyQt6.QtWidgets import QWidget, QStyle, QDialog +from PyQt6.QtWebEngineCore import QWebEngineDownloadRequest + +from EricWidgets import EricFileDialog +from EricWidgets.EricApplication import ericApp + +from .Ui_DownloadItem import Ui_DownloadItem + +from .DownloadUtilities import timeString, dataString, speedString +from WebBrowser.WebBrowserWindow import WebBrowserWindow + +import UI.PixmapCache +import Utilities.MimeTypes + + +class DownloadState(enum.Enum): + """ + Class implementing the various download states. + """ + Downloading = 0 + Successful = 1 + Cancelled = 2 + + +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) + + def __init__(self, downloadRequest=None, pageUrl=None, parent=None): + """ + Constructor + + @param downloadRequest reference to the download object containing the + download data. + @type QWebEngineDownloadRequest + @param pageUrl URL of the calling page + @type QUrl + @param parent reference to the parent widget + @type QWidget + """ + super().__init__(parent) + self.setupUi(self) + + self.fileIcon.setStyleSheet("background-color: transparent") + self.datetimeLabel.setStyleSheet("background-color: transparent") + self.filenameLabel.setStyleSheet("background-color: transparent") + if ericApp().usesDarkPalette(): + self.infoLabel.setStyleSheet( + "color: #c0c0c0; background-color: transparent" + ) # light gray + else: + self.infoLabel.setStyleSheet( + "color: #808080; background-color: transparent" + ) # dark gray + + self.progressBar.setMaximum(0) + + self.pauseButton.setIcon(UI.PixmapCache.getIcon("pause")) + self.stopButton.setIcon(UI.PixmapCache.getIcon("stopLoading")) + self.openButton.setIcon(UI.PixmapCache.getIcon("open")) + self.openButton.setEnabled(False) + self.openButton.setVisible(False) + + self.__state = DownloadState.Downloading + + icon = self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon) + self.fileIcon.setPixmap(icon.pixmap(48, 48)) + + self.__downloadRequest = downloadRequest + 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.__downloadRequest is None: + return + + self.__finishedDownloading = False + self.__bytesReceived = 0 + self.__bytesTotal = -1 + + # start timer for the download estimation + self.__downloadTime = QTime.currentTime() + + # attach to the download item object + self.__url = self.__downloadRequest.url() + self.__downloadRequest.receivedBytesChanged.connect( + self.__downloadProgress) + self.__downloadRequest.isFinishedChanged.connect(self.__finished) + + # reset info + self.datetimeLabel.clear() + self.datetimeLabel.hide() + self.infoLabel.clear() + self.progressBar.setValue(0) + if ( + self.__downloadRequest.state() == + QWebEngineDownloadRequest.DownloadState.DownloadRequested + ): + self.__getFileName() + if not self.__fileName: + self.__downloadRequest.cancel() + else: + self.__downloadRequest.setDownloadFileName(self.__fileName) + self.__downloadRequest.accept() + else: + fileName = self.__downloadRequest.downloadFileName() + self.__setFileName(fileName) + + def __getFileName(self): + """ + Private method to get the file name to save to from the user. + """ + if self.__gettingFileName: + return + + savePage = self.__downloadRequest.isSavePageDownload() + + documentLocation = QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.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.__downloadRequest.url() + mimetype = Utilities.MimeTypes.mimeType(originalFileName) + dlg = DownloadAskActionDialog( + pathlib.Path(originalFileName).name, + mimetype, + "{0}://{1}".format(url.scheme(), url.authority()), + self) + + if ( + dlg.exec() == QDialog.DialogCode.Rejected or + dlg.getAction() == "cancel" + ): + self.progressBar.setVisible(False) + self.on_stopButton_clicked() + self.filenameLabel.setText( + self.tr("Download canceled: {0}").format( + pathlib.Path(defaultFileName).name)) + 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( + pathlib.Path(defaultFileName).name)) + self.__canceledFileSelect = True + return + + self.__autoOpen = dlg.getAction() == "open" + + tempLocation = QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.TempLocation) + fileName = ( + tempLocation + '/' + + pathlib.Path(fileName).stem + ) + + if ask and not self.__autoOpen: + self.__gettingFileName = True + fileName = EricFileDialog.getSaveFileName( + None, + self.tr("Save File"), + defaultFileName, + "") + self.__gettingFileName = False + + if not fileName: + self.progressBar.setVisible(False) + self.on_stopButton_clicked() + self.filenameLabel.setText( + self.tr("Download canceled: {0}") + .format(pathlib.Path(defaultFileName).name)) + 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 + """ + fpath = pathlib.Path(fileName) + WebBrowserWindow.downloadManager().setDownloadDirectory( + fpath.parent.resolve()) + self.filenameLabel.setText(fpath.name) + + self.__fileName = str(fpath) + + # check file path for saving + saveDirPath = pathlib.Path(self.__fileName).parent() + if not saveDirPath.exists(): + saveDirPath.mkdir(parents=True) + + 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) + """ + fpath = pathlib.Path(self.__downloadRequest.downloadFileName()) + origName = fpath.name + name = os.path.join(directory, origName) + 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.__downloadRequest.pause() + else: + self.__downloadRequest.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 = DownloadState.Cancelled + self.__downloadRequest.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. + """ + url = QUrl.fromLocalFile(pathlib.Path(self.__fileName).resolve()) + QDesktopServices.openUrl(url) + + def openFolder(self): + """ + Public slot to open the folder containing the downloaded file. + """ + url = QUrl.fromLocalFile(pathlib.Path(self.__fileName).resolve()) + QDesktopServices.openUrl(url) + + @pyqtSlot() + def __downloadProgress(self): + """ + Private slot to show the download progress. + """ + self.__bytesReceived = self.__downloadRequest.receivedBytes() + self.__bytesTotal = self.__downloadRequest.totalBytes() + currentValue = 0 + totalValue = 0 + if self.__bytesTotal > 0: + currentValue = self.__bytesReceived * 100 // self.__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.__downloadRequest.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() + timeRemaining = ( + (self.bytesTotal() - self.bytesReceived()) / cSpeed + if cSpeed != 0 else + 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.msecsTo(QTime.currentTime()) + ) + + 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 bytesTotal in (self.__bytesReceived, -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 == DownloadState.Downloading + + def downloadedSuccessfully(self): + """ + Public method to check for a successful download. + + @return flag indicating a successful download (boolean) + """ + return self.__state == DownloadState.Successful + + def downloadCanceled(self): + """ + Public method to check, if the download was cancelled. + + @return flag indicating a canceled download (boolean) + """ + return self.__state == DownloadState.Cancelled + + def __finished(self): + """ + Private slot to handle the download finished. + """ + self.__finishedDownloading = True + + noError = (self.__downloadRequest.state() == + QWebEngineDownloadRequest.DownloadState.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 = DownloadState.Successful + 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 pathlib.Path(self.__fileName).resolve() + + 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": self.__fileName, + "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(pathlib.Path(self.__fileName).name) + 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 = DownloadState.Successful + else: + self.__state = DownloadState.Cancelled + 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()