#! /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 {} \n Reason: \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 " )