aboutsummaryrefslogtreecommitdiff
path: root/src/core.py
diff options
context:
space:
mode:
authortassaron2026-01-11 14:29:58 -0500
committertassaron2026-01-11 14:29:58 -0500
commit669756b391d26661cf2e2a97a304e73343ef6655 (patch)
tree9cf2d4858c209bdab9f44d5c7f95c2a30b37f7a6 /src/core.py
parent9d45f7f1a986aaa5d3c084c7ae747442b94a61b1 (diff)
update to Qt 6 and Pillow 12
and yeah, I accidentally ran black on the codebase. I don't want to spend more free time fixing that. All of these changes are simple renames or removals, nothing too major.
Diffstat (limited to 'src/core.py')
-rw-r--r--src/core.py420
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()