Sampled Instrument Player with static and monolithic design. All instruments are built-in.
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

#! /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")