#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Lib from pathlib import Path import os.path import os import json from time import sleep from shutil import disk_usage #Third Party, system wide Modules from PyQt5 import QtCore, QtWidgets, QtGui #Template Modules from template.pySmartDL import SmartDL from template.helper import humanReadableFilesize #Client Modules from .designer.chooseDownloadDirectory import Ui_ChooseDownloadDirectory from .resources import * #has the translation import engine.api as api from engine.config import * #imports METADATA from qtgui.resources import * #Has the logo class ChooseDownloadDirectory(QtWidgets.QDialog): """This dialog must only be called when the program is already after the initial init state. Especially on the very first run because we call api.rescanSampleDirectory on accept It gets constructed from init each time. No need to reset values. """ def __init__(self, parentMainWindow, autoStartOnFirstRun=False): super().__init__() #no parent, this is the top level window at this time. self.setModal(True) #block until closed self.ui = Ui_ChooseDownloadDirectory() self.ui.setupUi(self) self.parentMainWindow = parentMainWindow self.autoStartOnFirstRun = autoStartOnFirstRun self.currentSmartDL = None #will be a SmartDL object when a download is in progress self._abortDownloadNOW = False #if set to True during download it will stop the process settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) if settings.contains("sampleDownloadDirectory"): self.ui.pathComboBox.insertItem(0, settings.value("sampleDownloadDirectory", type=str)) else: self.ui.pathComboBox.setCurrentText("") self.ui.buttonBox.accepted.connect(self.accept) self.ui.buttonBox.rejected.connect(self.reject) #For the hidden cancel button self._rescanButtonDefaultText = QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Rescan and Close Dialog") self._rescanButtonPleaseWaitForDownloadText = QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Please wait for download to finish") self.ui.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText(self._rescanButtonDefaultText) #self._cancelDefaultText = QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Don't rescan") #self.ui.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).setText(self._cancelDefaultText) self.ui.abortDownloadButton.hide() self.ui.abortDownloadButton.clicked.connect(self.reject) self.ui.openFileDialogButton.setText("") self.ui.openFileDialogButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogOpenButton"))) self.ui.openFileDialogButton.clicked.connect(self.requestPathFromDialog) self.ui.downloadPushButton.setEnabled(True) self._downloadDefaultText = QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Download and Update Instrument Libraries") self._pauseText = QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Pause Download") self._resumeText = QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Resume Download") self.ui.downloadPushButton.setText(self._downloadDefaultText) self.ui.downloadPushButton.clicked.connect(self.startDownload) self.ui.progressLabel.setVisible(False) self.ui.labelSpeed.setVisible(False) self.ui.progressBar.setValue(0) self.ui.progressBar.setEnabled(False) self.ui.progressBar.setVisible(False) self.exec() def requestPathFromDialog(self): if self.ui.pathComboBox.currentText() == "": startPath = str(Path.home()) else: startPath = self.ui.pathComboBox.currentText() dirname = QtWidgets.QFileDialog.getExistingDirectory(self, QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Choose Download Directory"), startPath, QtWidgets.QFileDialog.ShowDirsOnly|QtWidgets.QFileDialog.DontResolveSymlinks) if dirname: self.ui.pathComboBox.setCurrentText(dirname) def accept(self): self.path = self.ui.pathComboBox.currentText() #easy abstraction so that the caller does not need to know our widget name settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) sampleDir = Path(self.path) if sampleDir.exists() and sampleDir.is_dir() and os.access(self.path, os.R_OK): #readable? logger.info(f"New sample dir path {self.path} accepted. Remembering for later.") settings.setValue("sampleDownloadDirectory", self.path) if not self.autoStartOnFirstRun: api.rescanSampleDirectory(self.path) else: logger.info(f"Attempted to rescan sample dir with path {self.path} that does not exist or is not readable. Ignoring.") super().accept() def reject(self): #We make sure all downloads are actually stopped and then exist as normal. self._abortDownloadNOW = True #just to be safe if self.currentSmartDL: self.currentSmartDL.unpause() #Just stopping here while paused will freeze Qt. With setting the abort switch above we can let it play out. self.path = None super().reject() def closeEvent(self, event): """Window manager close. We tried to stop downloading here in the past, but that was unreliable. We now intentionally prevent closing while the download is running. User can always press the "Abort Download" button explictely. """ if self.currentSmartDL: event.ignore() else: self._abortDownloadNOW = True #just to be safe event.accept() super().closeEvent(event) def startDownload(self): """First we download the index file from our own server. That contains a list of mirror servers and a list of libraries with versions and sha256sums http://itaybb.github.io/pySmartDL/examples.html#example-6-use-the-nonblocking-flag-and-get-information-during-the-download-process """ def _resetDialog(message): self.ui.pathComboBox.setEnabled(True) self.ui.downloadPushButton.setEnabled(True) try: self.ui.downloadPushButton.clicked.disconnect() except TypeError: pass #already disconnect self.ui.downloadPushButton.clicked.connect(self.startDownload) self.ui.downloadPushButton.setText(self._downloadDefaultText) self.ui.progressLabel.setVisible(True) self.ui.progressLabel.setText(message) self.ui.buttonBox.setEnabled(True) self.ui.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(True) self.ui.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText(self._rescanButtonDefaultText) self.ui.abortDownloadButton.hide() #self.ui.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).setText(self._cancelDefaultText) self.currentSmartDL = None #TODO: Make sure all downloads are stopped self.parentMainWindow.qtApp.processEvents() def _pauseUnpause(): if not self.currentSmartDL: raise RuntimeError("Reached the pause/unpause function without a running download. This should not have been possible and needs to be bug-fixed") st = self.currentSmartDL.get_status() if st == "downloading": self.currentSmartDL.pause() self.ui.downloadPushButton.setText(self._resumeText) elif st == "paused": self.currentSmartDL.unpause() self.ui.downloadPushButton.setText(self._pauseText) else: logger.warning(f"Reached download state {st} from pause/unpause the button. This was not intended but is not a problem either.") self.parentMainWindow.qtApp.processEvents() if self.currentSmartDL: return if not self.ui.pathComboBox.currentText(): logger.warning("Tried to download without giving a directory. Please try again.") _resetDialog(QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Warning: Tried to download without giving a directory. Please try again.")) return if not Path(self.ui.pathComboBox.currentText()).exists(): os.makedirs(self.ui.pathComboBox.currentText()) if not Path(self.ui.pathComboBox.currentText()).exists() or not Path(self.ui.pathComboBox.currentText()).is_dir() or not os.access(self.ui.pathComboBox.currentText(), os.W_OK): #writable? logger.warning("Tried to download without giving an existing, writable directory. Please check your filesystem.") _resetDialog(QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Warning: Tried to download without giving an existing, writable directory. Please check your filesystem.")) return logger.info("Downloading index file to temporary directory.") self.ui.progressLabel.setVisible(True) self.ui.progressLabel.setText(QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Fetching instrument list from server laborejo.org")) self.ui.downloadPushButton.setEnabled(False) #self.ui.buttonBox.setEnabled(False) self.ui.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) self.ui.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText(self._rescanButtonPleaseWaitForDownloadText) self.ui.abortDownloadButton.show() #self.ui.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).setText(QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Abort Download")) self.ui.pathComboBox.setEnabled(False) self.parentMainWindow.qtApp.processEvents() indexUrl = "https://www.laborejo.org/downloads/tembro-instruments/downloadindex.json" #indexUrl = "https://download.linuxaudio.org/musical-instrument-libraries/tembro/" indexDL = SmartDL(indexUrl, progress_bar=False) # Because we didn't pass a destination path to the constructor, temporary path was chosen. try: indexDL.start() #Blocking. We wait for the file to finish. except Exception as e: logger.error(e) _resetDialog(QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Error: Unable to download file\n{}\nReason:\n{}".format(indexUrl, e))) return if not Path(indexDL.get_dest()).exists(): #to be extra sure logger.error(f"File {indexUrl} was downloaded, but not found on disk!") _resetDialog(QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Error: File was downloaded, but not found on disk!\n{}".format(indexDL.get_dest()))) return with open(indexDL.get_dest(), "r") as indexf: indexDict = json.loads(indexf.read()) indexDict["mirrors"].append("http://0.0.0.0:8000/") #TODO: Development logger.info(f"Mirror list for downloads: {indexDict['mirrors']}") #Test if there is enough disk space required = indexDict["filesize"] freeSpace = disk_usage(self.ui.pathComboBox.currentText()).free logger.info(f"Download requires {required} bytes ({humanReadableFilesize(required)}). Free: {freeSpace} ({humanReadableFilesize(freeSpace)}) ") if required >= freeSpace: logger.error(f"Download requires {required} bytes ({humanReadableFilesize(required)}). You have free: {freeSpace} ({humanReadableFilesize(freeSpace)})") msg = QtCore.QCoreApplication.translate("ChooseDownloadDirectory", f"Download requires {humanReadableFilesize(required)}. You have only {humanReadableFilesize(freeSpace)} free.") _resetDialog(msg) return #We have the index. We have a download path. All tests ok. Start the actual download. logger.info(f"Downloading instrument libraries to {self.ui.pathComboBox.currentText()}") self.ui.progressBar.setVisible(True) self.ui.progressBar.setValue(0) self.ui.labelSpeed.setVisible(True) self.ui.labelSpeed.setText("") self.ui.downloadPushButton.setEnabled(True) self.ui.downloadPushButton.clicked.disconnect() totalDownloads = len(indexDict["libraries"]) downloadCounter = 0 #Make sure all gui texts and elements are visible self.parentMainWindow.qtApp.processEvents() for libId, entry in indexDict["libraries"].items(): if self._abortDownloadNOW: if self.currentSmartDL: self.currentSmartDL.stop() continue urlMirrorList = (mirror + entry["tar"] for mirror in indexDict["mirrors"]) logger.info(f"Downloading {entry['name']}") obj = SmartDL(urlMirrorList, self.ui.pathComboBox.currentText(), progress_bar=False) self.currentSmartDL = obj #With Hash Verification it will not only test the download but also don't double-download an existing file. obj.add_hash_verification("sha256", entry["sha256"]) obj.start(blocking=False) #Set the progress label text. But keep it international self.ui.progressLabel.setText(f"[{downloadCounter+1}/{totalDownloads}]: {entry['name']}") self.ui.downloadPushButton.setText(self._pauseText) self.ui.downloadPushButton.clicked.connect(_pauseUnpause) self.parentMainWindow.qtApp.processEvents() while not obj.isFinished(): #This loops also runs during download pause if self._abortDownloadNOW: obj.stop() if self.currentSmartDL: self.currentSmartDL.stop() self.currentSmartDL = None continue self.ui.labelSpeed.setText(f"{obj.get_speed(human=True)}") self.ui.progressBar.setValue(int(obj.get_progress()*100)) self.parentMainWindow.qtApp.processEvents() #Keep Qt responsive sleep(0.01) if obj.isSuccessful(): #This is triggered at least when the file already exists in the right version self.ui.labelSpeed.setText("") self.ui.progressBar.setValue(100) self.ui.downloadPushButton.clicked.disconnect() self.parentMainWindow.qtApp.processEvents() #Keep Qt responsive else: for e in obj.get_errors(): logger.error(f"{e}") downloadCounter += 1 #assert downloadCounter == totalDownloads not true in case of Abort. But was true long enough development that I confirmed everything works _resetDialog(f"[{downloadCounter}/{totalDownloads}]") self.ui.downloadPushButton.setEnabled(False) self.parentMainWindow.qtApp.processEvents() logger.info("Download process finished or aborted")