diff options
Diffstat (limited to 'src/core.py')
| -rw-r--r-- | src/core.py | 420 |
1 files changed, 201 insertions, 219 deletions
diff --git a/src/core.py b/src/core.py index 1ad4a67..df6ff63 100644 --- a/src/core.py +++ b/src/core.py @@ -1,8 +1,9 @@ -''' - Home to the Core class which tracks program state. Used by GUI & commandline - to create a list of components and create a video thread to export. -''' -from PyQt5 import QtCore, QtGui, uic +""" +Home to the Core class which tracks program state. Used by GUI & commandline +to create a list of components and create a video thread to export. +""" + +from PyQt6 import QtCore, QtGui, uic import sys import os import json @@ -12,20 +13,20 @@ import logging from . import toolkit -log = logging.getLogger('AVP.Core') +log = logging.getLogger("AVP.Core") STDOUT_LOGLVL = logging.WARNING FILE_LIBLOGLVL = logging.WARNING FILE_LOGLVL = logging.INFO class Core: - ''' - MainWindow and Command module both use an instance of this class - to store the core program state. This object tracks the components, - talks to the components, handles opening/creating project files - and presets, and creates the video thread to export. - This class also stores constants as class variables. - ''' + """ + MainWindow and Command module both use an instance of this class + to store the core program state. This object tracks the components, + talks to the components, handles opening/creating project files + and presets, and creates the video thread to export. + This class also stores constants as class variables. + """ def __init__(self): self.importComponents() @@ -34,9 +35,7 @@ class Core: self.openingProject = False def __repr__(self): - return "\n=~=~=~=\n".join( - [repr(comp) for comp in self.selectedComponents] - ) + return "\n=~=~=~=\n".join([repr(comp) for comp in self.selectedComponents]) def importComponents(self): def findComponents(): @@ -44,11 +43,12 @@ class Core: name, ext = os.path.splitext(f) if name.startswith("__"): continue - elif ext == '.py': + elif ext == ".py": yield name - log.debug('Importing component modules') + + log.debug("Importing component modules") self.modules = [ - import_module('.components.%s' % name, __package__) + import_module(".components.%s" % name, __package__) for name in findComponents() ] # store canonical module names and indexes @@ -62,7 +62,7 @@ class Core: # store alternative names for modules self.altCompNames = [] for i, mod in enumerate(self.modules): - if hasattr(mod.Component, 'names'): + if hasattr(mod.Component, "names"): for name in mod.Component.names(): self.altCompNames.append((name, i)) @@ -71,10 +71,10 @@ class Core: component.compPos = i def insertComponent(self, compPos, component, loader): - ''' - Creates a new component using these args: - (compPos, component obj or moduleIndex, MWindow/Command/Core obj) - ''' + """ + Creates a new component using these args: + (compPos, component obj or moduleIndex, MWindow/Command/Core obj) + """ if compPos < 0 or compPos > len(self.selectedComponents): compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: @@ -82,25 +82,16 @@ class Core: if type(component) is int: # create component using module index in self.modules moduleIndex = int(component) - log.debug( - 'Creating new component from module #%s', str(moduleIndex)) - component = self.modules[moduleIndex].Component( - moduleIndex, compPos, self - ) + log.debug("Creating new component from module #%s", str(moduleIndex)) + component = self.modules[moduleIndex].Component(moduleIndex, compPos, self) component.widget(loader) else: moduleIndex = -1 - log.debug( - 'Inserting previously-created %s component', component.name) + log.debug("Inserting previously-created %s component", component.name) - component._error.connect( - loader.videoThreadError - ) - self.selectedComponents.insert( - compPos, - component - ) - if hasattr(loader, 'insertComponent'): + component._error.connect(loader.videoThreadError) + self.selectedComponents.insert(compPos, component) + if hasattr(loader, "insertComponent"): loader.insertComponent(compPos) self.componentListChanged() @@ -123,9 +114,7 @@ class Core: self.componentListChanged() def updateComponent(self, i): - log.debug( - 'Auto-updating %s #%s', - self.selectedComponents[i], str(i)) + log.debug("Auto-updating %s #%s", self.selectedComponents[i], str(i)) self.selectedComponents[i].update(auto=True) def moduleIndexFor(self, compName): @@ -141,63 +130,59 @@ class Core: self.selectedComponents[compIndex].currentPreset = None def openPreset(self, filepath, compIndex, presetName): - '''Applies a preset to a specific component''' + """Applies a preset to a specific component""" saveValueStore = self.getPreset(filepath) if not saveValueStore: return False comp = self.selectedComponents[compIndex] - comp.loadPreset( - saveValueStore, - presetName - ) + comp.loadPreset(saveValueStore, presetName) self.savedPresets[presetName] = dict(saveValueStore) return True def getPreset(self, filepath): - '''Returns the preset dict stored at this filepath''' + """Returns the preset dict stored at this filepath""" if not os.path.exists(filepath): return False - with open(filepath, 'r') as f: + with open(filepath, "r") as f: for line in f: saveValueStore = toolkit.presetFromString(line.strip()) break return saveValueStore def getPresetDir(self, comp): - '''Get the preset subdir for a particular version of a component''' + """Get the preset subdir for a particular version of a component""" return os.path.join(Core.presetDir, comp.name, str(comp.version)) def openProject(self, loader, filepath): - ''' loader is the object calling this method which must have + """loader is the object calling this method which must have its own showMessage(**kwargs) method for displaying errors. - ''' + """ if not os.path.exists(filepath): - loader.showMessage(msg='Project file not found.') + loader.showMessage(msg="Project file not found.") return errcode, data = self.parseAvFile(filepath) if errcode == 0: self.openingProject = True try: - if hasattr(loader, 'window'): - for widget, value in data['WindowFields']: - widget = eval('loader.%s' % widget) + if hasattr(loader, "window"): + for widget, value in data["WindowFields"]: + widget = eval("loader.%s" % widget) with toolkit.blockSignals(widget): toolkit.setWidgetValue(widget, value) - for key, value in data['Settings']: + for key, value in data["Settings"]: Core.settings.setValue(key, value) - for tup in data['Components']: + for tup in data["Components"]: name, vers, preset = tup clearThis = False modified = False # add loaded named presets to savedPresets dict - if 'preset' in preset and preset['preset'] is not None: - nam = preset['preset'] - filepath2 = os.path.join( - Core.presetDir, name, str(vers), nam) + if "preset" in preset and preset["preset"] is not None: + nam = preset["preset"] + filepath2 = os.path.join(Core.presetDir, name, str(vers), nam) origSaveValueStore = self.getPreset(filepath2) if origSaveValueStore: self.savedPresets[nam] = dict(origSaveValueStore) @@ -207,33 +192,31 @@ class Core: clearThis = True # create the actual component object & get its index - i = self.insertComponent( - -1, - self.moduleIndexFor(name), - loader - ) + i = self.insertComponent(-1, self.moduleIndexFor(name), loader) + if i is None: + loader.showMessage( + msg=f"Component '{name}' didn't initialize correctly and had to be removed." + ) + continue if i == -1: loader.showMessage(msg="Too many components!") break try: - if 'preset' in preset and preset['preset'] is not None: - self.selectedComponents[i].loadPreset( - preset - ) + if "preset" in preset and preset["preset"] is not None: + self.selectedComponents[i].loadPreset(preset) else: self.selectedComponents[i].loadPreset( - preset, - preset['preset'] + preset, preset["preset"] ) except KeyError as e: - log.warning('%s missing value: %s' % ( - self.selectedComponents[i], e) + log.warning( + "%s missing value: %s" % (self.selectedComponents[i], e) ) if clearThis: self.clearPreset(i) - if hasattr(loader, 'updateComponentTitle'): + if hasattr(loader, "updateComponentTitle"): loader.updateComponentTitle(i, modified) self.openingProject = False return True @@ -243,56 +226,57 @@ class Core: if errcode == 1: typ, value, tb = data - if typ.__name__ == 'KeyError': + if typ.__name__ == "KeyError": # probably just an old version, still loadable - log.warning('Project file missing value: %s' % value) + log.warning("Project file missing value: %s" % value) return - if hasattr(loader, 'createNewProject'): + if hasattr(loader, "createNewProject"): loader.createNewProject(prompt=False) - msg = '%s: %s\n\n' % (typ.__name__, value) + msg = "%s: %s\n\n" % (typ.__name__, value) msg += toolkit.formatTraceback(tb) loader.showMessage( msg="Project file '%s' is corrupted." % filepath, showCancel=False, - icon='Warning', - detail=msg) + icon="Warning", + detail=msg, + ) self.openingProject = False return False def parseAvFile(self, filepath): - ''' - Parses an avp (project) or avl (preset package) file. - Returns dictionary with section names as the keys, each one - contains a list of tuples: (compName, version, compPresetDict) - ''' - log.debug('Parsing av file: %s', filepath) - validSections = ( - 'Components', - 'Settings', - 'WindowFields' - ) + """ + Parses an avp (project) or avl (preset package) file. + Returns dictionary with section names as the keys, each one + contains a list of tuples: (compName, version, compPresetDict) + """ + log.debug("Parsing av file: %s", filepath) + validSections = ("Components", "Settings", "WindowFields") data = {sect: [] for sect in validSections} try: - with open(filepath, 'r') as f: + with open(filepath, "r") as f: + def parseLine(line): - '''Decides if a file line is a section header''' + """Decides if a file line is a section header""" line = line.strip() - newSection = '' + newSection = "" - if line.startswith('[') and line.endswith(']') \ - and line[1:-1] in validSections: + if ( + line.startswith("[") + and line.endswith("]") + and line[1:-1] in validSections + ): newSection = line[1:-1] return line, newSection - section = '' + section = "" i = 0 for line in f: line, newSection = parseLine(line) if newSection: section = str(newSection) continue - if line and section == 'Components': + if line and section == "Components": if i == 0: lastCompName = str(line) i += 1 @@ -301,14 +285,12 @@ class Core: i += 1 elif i == 2: lastCompPreset = toolkit.presetFromString(line) - data[section].append(( - lastCompName, - lastCompVers, - lastCompPreset - )) + data[section].append( + (lastCompName, lastCompVers, lastCompPreset) + ) i = 0 elif line and section: - key, value = line.split('=', 1) + key, value = line.split("=", 1) data[section].append((key, value.strip())) return 0, data @@ -319,51 +301,40 @@ class Core: errcode, data = self.parseAvFile(filepath) returnList = [] if errcode == 0: - name, vers, preset = data['Components'][0] - presetName = preset['preset'] \ - if preset['preset'] else os.path.basename(filepath)[:-4] - newPath = os.path.join( - Core.presetDir, - name, - vers, - presetName + name, vers, preset = data["Components"][0] + presetName = ( + preset["preset"] + if preset["preset"] + else os.path.basename(filepath)[:-4] ) + newPath = os.path.join(Core.presetDir, name, vers, presetName) if os.path.exists(newPath): return False, newPath - preset['preset'] = presetName - self.createPresetFile( - name, vers, presetName, preset - ) + preset["preset"] = presetName + self.createPresetFile(name, vers, presetName, preset) return True, presetName elif errcode == 1: # TODO: an error message - return False, '' + return False, "" def exportPreset(self, exportPath, compName, vers, origName): - internalPath = os.path.join( - Core.presetDir, compName, str(vers), origName - ) + internalPath = os.path.join(Core.presetDir, compName, str(vers), origName) if not os.path.exists(internalPath): return if os.path.exists(exportPath): os.remove(exportPath) - with open(internalPath, 'r') as f: + with open(internalPath, "r") as f: internalData = [line for line in f] try: saveValueStore = toolkit.presetFromString(internalData[0].strip()) - self.createPresetFile( - compName, vers, - origName, saveValueStore, - exportPath - ) + self.createPresetFile(compName, vers, origName, saveValueStore, exportPath) return True except Exception: return False - def createPresetFile( - self, compName, vers, presetName, saveValueStore, filepath=''): - '''Create a preset file (.avl) at filepath using args. - Or if filepath is empty, create an internal preset using args''' + def createPresetFile(self, compName, vers, presetName, saveValueStore, filepath=""): + """Create a preset file (.avl) at filepath using args. + Or if filepath is empty, create an internal preset using args""" if not filepath: dirname = os.path.join(Core.presetDir, compName, str(vers)) if not os.path.exists(dirname): @@ -371,54 +342,55 @@ class Core: filepath = os.path.join(dirname, presetName) internal = True else: - if not filepath.endswith('.avl'): - filepath += '.avl' + if not filepath.endswith(".avl"): + filepath += ".avl" internal = False - with open(filepath, 'w') as f: + with open(filepath, "w") as f: if not internal: - f.write('[Components]\n') - f.write('%s\n' % compName) - f.write('%s\n' % str(vers)) + f.write("[Components]\n") + f.write("%s\n" % compName) + f.write("%s\n" % str(vers)) f.write(toolkit.presetToString(saveValueStore)) def createProjectFile(self, filepath, window=None): - '''Create a project file (.avp) using the current program state''' - log.info('Creating %s', filepath) + """Create a project file (.avp) using the current program state""" + log.info("Creating %s", filepath) settingsKeys = [ - 'componentDir', - 'inputDir', - 'outputDir', - 'presetDir', - 'projectDir', + "componentDir", + "inputDir", + "outputDir", + "presetDir", + "projectDir", ] try: if not filepath.endswith(".avp"): - filepath += '.avp' + filepath += ".avp" if os.path.exists(filepath): os.remove(filepath) - with open(filepath, 'w') as f: - f.write('[Components]\n') + with open(filepath, "w") as f: + f.write("[Components]\n") for comp in self.selectedComponents: saveValueStore = comp.savePreset() - saveValueStore['preset'] = comp.currentPreset - f.write('%s\n' % str(comp)) - f.write('%s\n' % str(comp.version)) - f.write('%s\n' % toolkit.presetToString(saveValueStore)) + saveValueStore["preset"] = comp.currentPreset + f.write("%s\n" % str(comp)) + f.write("%s\n" % str(comp.version)) + f.write("%s\n" % toolkit.presetToString(saveValueStore)) - f.write('\n[Settings]\n') + f.write("\n[Settings]\n") for key in Core.settings.allKeys(): if key in settingsKeys: - f.write('%s=%s\n' % (key, Core.settings.value(key))) + f.write("%s=%s\n" % (key, Core.settings.value(key))) if window: - f.write('\n[WindowFields]\n') + f.write("\n[WindowFields]\n") f.write( - 'lineEdit_audioFile=%s\n' - 'lineEdit_outputFile=%s\n' % ( + "lineEdit_audioFile=%s\n" + "lineEdit_outputFile=%s\n" + % ( window.lineEdit_audioFile.text(), - window.lineEdit_outputFile.text() + window.lineEdit_outputFile.text(), ) ) return True @@ -426,8 +398,9 @@ class Core: return False def newVideoWorker(self, loader, audioFile, outputPath): - '''loader is MainWindow or Command object which must own the thread''' + """loader is MainWindow or Command object which must own the thread""" from . import video_thread + self.videoThread = QtCore.QThread(loader) videoWorker = video_thread.Worker( loader, audioFile, outputPath, self.selectedComponents @@ -450,18 +423,18 @@ class Core: @classmethod def storeSettings(cls): - '''Store settings/paths to directories as class variables''' + """Store settings/paths to directories as class variables""" from .__init__ import wd from .toolkit.ffmpeg import findFfmpeg cls.wd = wd dataDir = QtCore.QStandardPaths.writableLocation( - QtCore.QStandardPaths.AppConfigLocation + QtCore.QStandardPaths.StandardLocation.AppConfigLocation ) # Windows: C:/Users/<USER>/AppData/Local/audio-visualizer # macOS: ~/Library/Preferences/audio-visualizer # Linux: ~/.config/audio-visualizer - with open(os.path.join(wd, 'encoder-options.json')) as json_file: + with open(os.path.join(wd, "encoder-options.json")) as json_file: encoderOptions = json.load(json_file) # Locate FFmpeg @@ -470,53 +443,60 @@ class Core: print("Could not find FFmpeg") settings = { - 'canceled': False, - 'FFMPEG_BIN': ffmpegBin, - 'dataDir': dataDir, - 'settings': QtCore.QSettings( - os.path.join(dataDir, 'settings.ini'), - QtCore.QSettings.IniFormat), - 'presetDir': os.path.join(dataDir, 'presets'), - 'componentsPath': os.path.join(wd, 'components'), - 'junkStream': os.path.join(wd, 'gui', 'background.png'), - 'encoderOptions': encoderOptions, - 'resolutions': [ - '1920x1080', - '1280x720', - '854x480', + "canceled": False, + "FFMPEG_BIN": ffmpegBin, + "dataDir": dataDir, + "settings": QtCore.QSettings( + os.path.join(dataDir, "settings.ini"), + QtCore.QSettings.Format.IniFormat, + ), + "presetDir": os.path.join(dataDir, "presets"), + "componentsPath": os.path.join(wd, "components"), + "junkStream": os.path.join(wd, "gui", "background.png"), + "encoderOptions": encoderOptions, + "resolutions": [ + "1920x1080", + "1280x720", + "854x480", ], - 'logDir': os.path.join(dataDir, 'log'), - 'logEnabled': False, - 'previewEnabled': True, + "logDir": os.path.join(dataDir, "log"), + "logEnabled": False, + "previewEnabled": True, } - settings['videoFormats'] = toolkit.appendUppercase([ - '*.mp4', - '*.mov', - '*.mkv', - '*.avi', - '*.webm', - '*.flv', - ]) - settings['audioFormats'] = toolkit.appendUppercase([ - '*.mp3', - '*.wav', - '*.ogg', - '*.fla', - '*.flac', - '*.aac', - ]) - settings['imageFormats'] = toolkit.appendUppercase([ - '*.png', - '*.jpg', - '*.tif', - '*.tiff', - '*.gif', - '*.bmp', - '*.ico', - '*.xbm', - '*.xpm', - ]) + settings["videoFormats"] = toolkit.appendUppercase( + [ + "*.mp4", + "*.mov", + "*.mkv", + "*.avi", + "*.webm", + "*.flv", + ] + ) + settings["audioFormats"] = toolkit.appendUppercase( + [ + "*.mp3", + "*.wav", + "*.ogg", + "*.fla", + "*.flac", + "*.aac", + ] + ) + settings["imageFormats"] = toolkit.appendUppercase( + [ + "*.png", + "*.jpg", + "*.tif", + "*.tiff", + "*.gif", + "*.bmp", + "*.ico", + "*.xbm", + "*.xpm", + ] + ) # Register all settings as class variables for classvar, val in settings.items(): @@ -526,7 +506,10 @@ class Core: if not os.path.exists(cls.dataDir): os.makedirs(cls.dataDir) for neededDirectory in ( - cls.presetDir, cls.logDir, cls.settings.value("projectDir")): + cls.presetDir, + cls.logDir, + cls.settings.value("projectDir"), + ): if not os.path.exists(neededDirectory): os.mkdir(neededDirectory) cls.makeLogger(deleteOldLogs=True) @@ -546,7 +529,7 @@ class Core: "outputPreset": "medium", "outputFormat": "mp4", "outputContainer": "MP4", - "projectDir": os.path.join(cls.dataDir, 'projects'), + "projectDir": os.path.join(cls.dataDir, "projects"), "pref_insertCompAtTop": True, "pref_genericPreview": True, "pref_undoLimit": 10, @@ -559,15 +542,15 @@ class Core: # Allow manual editing of prefs. (Surprisingly necessary as Qt seems to # store True as 'true' but interprets a manually-added 'true' as str.) for key in cls.settings.allKeys(): - if not key.startswith('pref_'): + if not key.startswith("pref_"): continue val = cls.settings.value(key) try: val = int(val) except ValueError: - if val == 'true': + if val == "true": val = True - elif val == 'false': + elif val == "false": val = False cls.settings.setValue(key, val) @@ -576,18 +559,16 @@ class Core: # send critical log messages to stdout logStream = logging.StreamHandler() logStream.setLevel(STDOUT_LOGLVL) - streamFormatter = logging.Formatter( - '<%(name)s> %(levelname)s: %(message)s' - ) + streamFormatter = logging.Formatter("<%(name)s> %(levelname)s: %(message)s") logStream.setFormatter(streamFormatter) - log = logging.getLogger('AVP') + log = logging.getLogger("AVP") log.addHandler(logStream) if FILE_LOGLVL is not None: # write log files as well! Core.logEnabled = True - logFilename = os.path.join(Core.logDir, 'avp_debug.log') - libLogFilename = os.path.join(Core.logDir, 'global_debug.log') + logFilename = os.path.join(Core.logDir, "avp_debug.log") + libLogFilename = os.path.join(Core.logDir, "global_debug.log") if deleteOldLogs: for log_ in (logFilename, libLogFilename): @@ -599,8 +580,8 @@ class Core: libLogFile = logging.FileHandler(libLogFilename, delay=True) libLogFile.setLevel(FILE_LIBLOGLVL) fileFormatter = logging.Formatter( - '[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: ' - '%(message)s' + "[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: " + "%(message)s" ) logFile.setFormatter(fileFormatter) libLogFile.setFormatter(fileFormatter) @@ -611,5 +592,6 @@ class Core: # lowest level must be explicitly set on the root Logger libLog.setLevel(0) + # always store settings in class variables even if a Core object is not created Core.storeSettings() |
