You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
340 lines
16 KiB
340 lines
16 KiB
#! /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 <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
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")
|
|
|