1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2010 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a widget controlling a download.
8"""
9
10import os
11
12from PyQt5.QtCore import (
13    pyqtSlot, pyqtSignal, Qt, QTime, QUrl, QStandardPaths, QFileInfo, QDateTime
14)
15from PyQt5.QtGui import QPalette, QDesktopServices
16from PyQt5.QtWidgets import QWidget, QStyle, QDialog
17from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
18
19from E5Gui import E5FileDialog
20
21from .Ui_DownloadItem import Ui_DownloadItem
22
23from .DownloadUtilities import timeString, dataString, speedString
24from WebBrowser.WebBrowserWindow import WebBrowserWindow
25
26import UI.PixmapCache
27import Utilities.MimeTypes
28
29
30class DownloadItem(QWidget, Ui_DownloadItem):
31    """
32    Class implementing a widget controlling a download.
33
34    @signal statusChanged() emitted upon a status change of a download
35    @signal downloadFinished(success) emitted when a download finished
36    @signal progress(int, int) emitted to signal the download progress
37    """
38    statusChanged = pyqtSignal()
39    downloadFinished = pyqtSignal(bool)
40    progress = pyqtSignal(int, int)
41
42    Downloading = 0
43    DownloadSuccessful = 1
44    DownloadCancelled = 2
45
46    def __init__(self, downloadItem=None, pageUrl=None, parent=None):
47        """
48        Constructor
49
50        @param downloadItem reference to the download object containing the
51        download data.
52        @type QWebEngineDownloadItem
53        @param pageUrl URL of the calling page
54        @type QUrl
55        @param parent reference to the parent widget
56        @type QWidget
57        """
58        super().__init__(parent)
59        self.setupUi(self)
60
61        p = self.infoLabel.palette()
62        p.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.darkGray)
63        self.infoLabel.setPalette(p)
64
65        self.progressBar.setMaximum(0)
66
67        self.pauseButton.setIcon(UI.PixmapCache.getIcon("pause"))
68        self.stopButton.setIcon(UI.PixmapCache.getIcon("stopLoading"))
69        self.openButton.setIcon(UI.PixmapCache.getIcon("open"))
70        self.openButton.setEnabled(False)
71        self.openButton.setVisible(False)
72        if not hasattr(QWebEngineDownloadItem, "pause"):
73            # pause/resume was defined in Qt 5.10.0 / PyQt 5.10.0
74            self.pauseButton.setEnabled(False)
75            self.pauseButton.setVisible(False)
76
77        self.__state = DownloadItem.Downloading
78
79        icon = self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon)
80        self.fileIcon.setPixmap(icon.pixmap(48, 48))
81
82        self.__downloadItem = downloadItem
83        if pageUrl is None:
84            self.__pageUrl = QUrl()
85        else:
86            self.__pageUrl = pageUrl
87        self.__bytesReceived = 0
88        self.__bytesTotal = -1
89        self.__downloadTime = QTime()
90        self.__fileName = ""
91        self.__originalFileName = ""
92        self.__finishedDownloading = False
93        self.__gettingFileName = False
94        self.__canceledFileSelect = False
95        self.__autoOpen = False
96        self.__downloadedDateTime = QDateTime()
97
98        self.__initialize()
99
100    def __initialize(self):
101        """
102        Private method to initialize the widget.
103        """
104        if self.__downloadItem is None:
105            return
106
107        self.__finishedDownloading = False
108        self.__bytesReceived = 0
109        self.__bytesTotal = -1
110
111        # start timer for the download estimation
112        self.__downloadTime.start()
113
114        # attach to the download item object
115        self.__url = self.__downloadItem.url()
116        self.__downloadItem.downloadProgress.connect(self.__downloadProgress)
117        self.__downloadItem.finished.connect(self.__finished)
118
119        # reset info
120        self.datetimeLabel.clear()
121        self.datetimeLabel.hide()
122        self.infoLabel.clear()
123        self.progressBar.setValue(0)
124        if (
125            self.__downloadItem.state() ==
126            QWebEngineDownloadItem.DownloadState.DownloadRequested
127        ):
128            self.__getFileName()
129            if not self.__fileName:
130                self.__downloadItem.cancel()
131            else:
132                self.__downloadItem.setPath(self.__fileName)
133                self.__downloadItem.accept()
134        else:
135            fileName = self.__downloadItem.path()
136            self.__setFileName(fileName)
137
138    def __getFileName(self):
139        """
140        Private method to get the file name to save to from the user.
141        """
142        if self.__gettingFileName:
143            return
144
145        savePage = self.__downloadItem.type() == (
146            QWebEngineDownloadItem.DownloadType.SavePage
147        )
148
149        documentLocation = QStandardPaths.writableLocation(
150            QStandardPaths.StandardLocation.DocumentsLocation)
151        downloadDirectory = (
152            WebBrowserWindow.downloadManager().downloadDirectory()
153        )
154
155        if self.__fileName:
156            fileName = self.__fileName
157            originalFileName = self.__originalFileName
158            self.__toDownload = True
159            ask = False
160        else:
161            defaultFileName, originalFileName = self.__saveFileName(
162                documentLocation if savePage else downloadDirectory)
163            fileName = defaultFileName
164            self.__originalFileName = originalFileName
165            ask = True
166        self.__autoOpen = False
167
168        if not savePage:
169            from .DownloadAskActionDialog import DownloadAskActionDialog
170            url = self.__downloadItem.url()
171            mimetype = Utilities.MimeTypes.mimeType(originalFileName)
172            dlg = DownloadAskActionDialog(
173                QFileInfo(originalFileName).fileName(),
174                mimetype,
175                "{0}://{1}".format(url.scheme(), url.authority()),
176                self)
177
178            if (
179                dlg.exec() == QDialog.DialogCode.Rejected or
180                dlg.getAction() == "cancel"
181            ):
182                self.progressBar.setVisible(False)
183                self.on_stopButton_clicked()
184                self.filenameLabel.setText(
185                    self.tr("Download canceled: {0}").format(
186                        QFileInfo(defaultFileName).fileName()))
187                self.__canceledFileSelect = True
188                self.__setDateTime()
189                return
190
191            if dlg.getAction() == "scan":
192                self.__mainWindow.requestVirusTotalScan(url)
193
194                self.progressBar.setVisible(False)
195                self.on_stopButton_clicked()
196                self.filenameLabel.setText(
197                    self.tr("VirusTotal scan scheduled: {0}").format(
198                        QFileInfo(defaultFileName).fileName()))
199                self.__canceledFileSelect = True
200                return
201
202            self.__autoOpen = dlg.getAction() == "open"
203
204            tempLocation = QStandardPaths.writableLocation(
205                QStandardPaths.StandardLocation.TempLocation)
206            fileName = (
207                tempLocation + '/' +
208                QFileInfo(fileName).completeBaseName()
209            )
210
211            if ask and not self.__autoOpen:
212                self.__gettingFileName = True
213                fileName = E5FileDialog.getSaveFileName(
214                    None,
215                    self.tr("Save File"),
216                    defaultFileName,
217                    "")
218                self.__gettingFileName = False
219
220        if not fileName:
221            self.progressBar.setVisible(False)
222            self.on_stopButton_clicked()
223            self.filenameLabel.setText(
224                self.tr("Download canceled: {0}")
225                    .format(QFileInfo(defaultFileName).fileName()))
226            self.__canceledFileSelect = True
227            self.__setDateTime()
228            return
229
230        self.__setFileName(fileName)
231
232    def __setFileName(self, fileName):
233        """
234        Private method to set the file name to save the download into.
235
236        @param fileName name of the file to save into
237        @type str
238        """
239        fileInfo = QFileInfo(fileName)
240        WebBrowserWindow.downloadManager().setDownloadDirectory(
241            fileInfo.absoluteDir().absolutePath())
242        self.filenameLabel.setText(fileInfo.fileName())
243
244        self.__fileName = fileName
245
246        # check file path for saving
247        saveDirPath = QFileInfo(self.__fileName).dir()
248        if (
249            not saveDirPath.exists() and
250            not saveDirPath.mkpath(saveDirPath.absolutePath())
251        ):
252            self.progressBar.setVisible(False)
253            self.on_stopButton_clicked()
254            self.infoLabel.setText(self.tr(
255                "Download directory ({0}) couldn't be created.")
256                .format(saveDirPath.absolutePath()))
257            self.__setDateTime()
258            return
259
260        self.filenameLabel.setText(QFileInfo(self.__fileName).fileName())
261
262    def __saveFileName(self, directory):
263        """
264        Private method to calculate a name for the file to download.
265
266        @param directory name of the directory to store the file into (string)
267        @return proposed filename and original filename (string, string)
268        """
269        path = self.__downloadItem.path()
270        info = QFileInfo(path)
271        baseName = info.completeBaseName()
272        endName = info.suffix()
273
274        origName = baseName
275        if endName:
276            origName += '.' + endName
277
278        name = os.path.join(directory, baseName)
279        if endName:
280            name += '.' + endName
281        return name, origName
282
283    @pyqtSlot(bool)
284    def on_pauseButton_clicked(self, checked):
285        """
286        Private slot to pause the download.
287
288        @param checked flag indicating the state of the button
289        @type bool
290        """
291        if checked:
292            self.__downloadItem.pause()
293        else:
294            self.__downloadItem.resume()
295
296    @pyqtSlot()
297    def on_stopButton_clicked(self):
298        """
299        Private slot to stop the download.
300        """
301        self.cancelDownload()
302
303    def cancelDownload(self):
304        """
305        Public slot to stop the download.
306        """
307        self.setUpdatesEnabled(False)
308        self.stopButton.setEnabled(False)
309        self.stopButton.setVisible(False)
310        self.openButton.setEnabled(False)
311        self.openButton.setVisible(False)
312        self.pauseButton.setEnabled(False)
313        self.pauseButton.setVisible(False)
314        self.setUpdatesEnabled(True)
315        self.__state = DownloadItem.DownloadCancelled
316        self.__downloadItem.cancel()
317        self.__setDateTime()
318        self.downloadFinished.emit(False)
319
320    @pyqtSlot()
321    def on_openButton_clicked(self):
322        """
323        Private slot to open the downloaded file.
324        """
325        self.openFile()
326
327    def openFile(self):
328        """
329        Public slot to open the downloaded file.
330        """
331        info = QFileInfo(self.__fileName)
332        url = QUrl.fromLocalFile(info.absoluteFilePath())
333        QDesktopServices.openUrl(url)
334
335    def openFolder(self):
336        """
337        Public slot to open the folder containing the downloaded file.
338        """
339        info = QFileInfo(self.__fileName)
340        url = QUrl.fromLocalFile(info.absolutePath())
341        QDesktopServices.openUrl(url)
342
343    def __downloadProgress(self, bytesReceived, bytesTotal):
344        """
345        Private method to show the download progress.
346
347        @param bytesReceived number of bytes received (integer)
348        @param bytesTotal number of total bytes (integer)
349        """
350        self.__bytesReceived = bytesReceived
351        self.__bytesTotal = bytesTotal
352        currentValue = 0
353        totalValue = 0
354        if bytesTotal > 0:
355            currentValue = bytesReceived * 100 / bytesTotal
356            totalValue = 100
357        self.progressBar.setValue(currentValue)
358        self.progressBar.setMaximum(totalValue)
359
360        self.progress.emit(currentValue, totalValue)
361        self.__updateInfoLabel()
362
363    def downloadProgress(self):
364        """
365        Public method to get the download progress.
366
367        @return current download progress
368        @rtype int
369        """
370        return self.progressBar.value()
371
372    def bytesTotal(self):
373        """
374        Public method to get the total number of bytes of the download.
375
376        @return total number of bytes (integer)
377        """
378        if self.__bytesTotal == -1:
379            self.__bytesTotal = self.__downloadItem.totalBytes()
380        return self.__bytesTotal
381
382    def bytesReceived(self):
383        """
384        Public method to get the number of bytes received.
385
386        @return number of bytes received (integer)
387        """
388        return self.__bytesReceived
389
390    def remainingTime(self):
391        """
392        Public method to get an estimation for the remaining time.
393
394        @return estimation for the remaining time (float)
395        """
396        if not self.downloading():
397            return -1.0
398
399        if self.bytesTotal() == -1:
400            return -1.0
401
402        cSpeed = self.currentSpeed()
403        timeRemaining = (
404            (self.bytesTotal() - self.bytesReceived()) / cSpeed
405            if cSpeed != 0 else
406            1
407        )
408
409        # ETA should never be 0
410        if timeRemaining == 0:
411            timeRemaining = 1
412
413        return timeRemaining
414
415    def currentSpeed(self):
416        """
417        Public method to get an estimation for the download speed.
418
419        @return estimation for the download speed (float)
420        """
421        if not self.downloading():
422            return -1.0
423
424        return self.__bytesReceived * 1000.0 / self.__downloadTime.elapsed()
425
426    def __updateInfoLabel(self):
427        """
428        Private method to update the info label.
429        """
430        bytesTotal = self.bytesTotal()
431        running = not self.downloadedSuccessfully()
432
433        speed = self.currentSpeed()
434        timeRemaining = self.remainingTime()
435
436        info = ""
437        if running:
438            remaining = ""
439
440            if bytesTotal > 0:
441                remaining = timeString(timeRemaining)
442
443            info = self.tr(
444                "{0} of {1} ({2}/sec) {3}"
445            ).format(
446                dataString(self.__bytesReceived),
447                bytesTotal == -1 and self.tr("?") or
448                dataString(bytesTotal),
449                speedString(speed),
450                remaining
451            )
452        else:
453            if bytesTotal in (self.__bytesReceived, -1):
454                info = self.tr(
455                    "{0} downloaded"
456                ).format(dataString(self.__bytesReceived))
457            else:
458                info = self.tr(
459                    "{0} of {1} - Stopped"
460                ).format(dataString(self.__bytesReceived),
461                         dataString(bytesTotal))
462        self.infoLabel.setText(info)
463
464    def downloading(self):
465        """
466        Public method to determine, if a download is in progress.
467
468        @return flag indicating a download is in progress (boolean)
469        """
470        return self.__state == DownloadItem.Downloading
471
472    def downloadedSuccessfully(self):
473        """
474        Public method to check for a successful download.
475
476        @return flag indicating a successful download (boolean)
477        """
478        return self.__state == DownloadItem.DownloadSuccessful
479
480    def downloadCanceled(self):
481        """
482        Public method to check, if the download was cancelled.
483
484        @return flag indicating a canceled download (boolean)
485        """
486        return self.__state == DownloadItem.DownloadCancelled
487
488    def __finished(self):
489        """
490        Private slot to handle the download finished.
491        """
492        self.__finishedDownloading = True
493
494        noError = (self.__downloadItem.state() ==
495                   QWebEngineDownloadItem.DownloadState.DownloadCompleted)
496
497        self.progressBar.setVisible(False)
498        self.pauseButton.setEnabled(False)
499        self.pauseButton.setVisible(False)
500        self.stopButton.setEnabled(False)
501        self.stopButton.setVisible(False)
502        self.openButton.setEnabled(noError)
503        self.openButton.setVisible(noError)
504        self.__state = DownloadItem.DownloadSuccessful
505        self.__updateInfoLabel()
506        self.__setDateTime()
507
508        self.__adjustSize()
509
510        self.statusChanged.emit()
511        self.downloadFinished.emit(True)
512
513        if self.__autoOpen:
514            self.openFile()
515
516    def canceledFileSelect(self):
517        """
518        Public method to check, if the user canceled the file selection.
519
520        @return flag indicating cancellation (boolean)
521        """
522        return self.__canceledFileSelect
523
524    def setIcon(self, icon):
525        """
526        Public method to set the download icon.
527
528        @param icon reference to the icon to be set (QIcon)
529        """
530        self.fileIcon.setPixmap(icon.pixmap(48, 48))
531
532    def fileName(self):
533        """
534        Public method to get the name of the output file.
535
536        @return name of the output file (string)
537        """
538        return self.__fileName
539
540    def absoluteFilePath(self):
541        """
542        Public method to get the absolute path of the output file.
543
544        @return absolute path of the output file (string)
545        """
546        return QFileInfo(self.__fileName).absoluteFilePath()
547
548    def getData(self):
549        """
550        Public method to get the relevant download data.
551
552        @return dictionary containing the URL, save location, done flag,
553            the URL of the related web page and the date and time of the
554            download
555        @rtype dict of {"URL": QUrl, "Location": str, "Done": bool,
556            "PageURL": QUrl, "Downloaded": QDateTime}
557        """
558        return {
559            "URL": self.__url,
560            "Location": QFileInfo(self.__fileName).filePath(),
561            "Done": self.downloadedSuccessfully(),
562            "PageURL": self.__pageUrl,
563            "Downloaded": self.__downloadedDateTime
564        }
565
566    def setData(self, data):
567        """
568        Public method to set the relevant download data.
569
570        @param data dictionary containing the URL, save location, done flag,
571            the URL of the related web page and the date and time of the
572            download
573        @type dict of {"URL": QUrl, "Location": str, "Done": bool,
574            "PageURL": QUrl, "Downloaded": QDateTime}
575        """
576        self.__url = data["URL"]
577        self.__fileName = data["Location"]
578        self.__pageUrl = data["PageURL"]
579
580        self.filenameLabel.setText(QFileInfo(self.__fileName).fileName())
581        self.infoLabel.setText(self.__fileName)
582
583        try:
584            self.__setDateTime(data["Downloaded"])
585        except KeyError:
586            self.__setDateTime(QDateTime())
587
588        self.pauseButton.setEnabled(False)
589        self.pauseButton.setVisible(False)
590        self.stopButton.setEnabled(False)
591        self.stopButton.setVisible(False)
592        self.openButton.setEnabled(data["Done"])
593        self.openButton.setVisible(data["Done"])
594        if data["Done"]:
595            self.__state = DownloadItem.DownloadSuccessful
596        else:
597            self.__state = DownloadItem.DownloadCancelled
598        self.progressBar.setVisible(False)
599
600        self.__adjustSize()
601
602    def getInfoData(self):
603        """
604        Public method to get the text of the info label.
605
606        @return text of the info label (string)
607        """
608        return self.infoLabel.text()
609
610    def getPageUrl(self):
611        """
612        Public method to get the URL of the download page.
613
614        @return URL of the download page (QUrl)
615        """
616        return self.__pageUrl
617
618    def __adjustSize(self):
619        """
620        Private method to adjust the size of the download item.
621        """
622        self.ensurePolished()
623
624        msh = self.minimumSizeHint()
625        self.resize(max(self.width(), msh.width()), msh.height())
626
627    def __setDateTime(self, dateTime=None):
628        """
629        Private method to set the download date and time.
630
631        @param dateTime date and time to be set
632        @type QDateTime
633        """
634        if dateTime is None:
635            self.__downloadedDateTime = QDateTime.currentDateTime()
636        else:
637            self.__downloadedDateTime = dateTime
638        if self.__downloadedDateTime.isValid():
639            labelText = self.__downloadedDateTime.toString("yyyy-MM-dd hh:mm")
640            self.datetimeLabel.setText(labelText)
641            self.datetimeLabel.show()
642        else:
643            self.datetimeLabel.clear()
644            self.datetimeLabel.hide()
645