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