aboutsummaryrefslogtreecommitdiff
path: root/src/command.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/command.py')
-rw-r--r--src/command.py263
1 files changed, 263 insertions, 0 deletions
diff --git a/src/command.py b/src/command.py
new file mode 100644
index 0000000..bf7941a
--- /dev/null
+++ b/src/command.py
@@ -0,0 +1,263 @@
+'''
+ When using commandline mode, this module's object handles interpreting
+ the arguments and giving them to Core, which tracks the main program state.
+ Then it immediately exports a video.
+'''
+from PyQt5 import QtCore
+import argparse
+import os
+import sys
+import time
+import signal
+import shutil
+import logging
+
+from . import core
+
+
+log = logging.getLogger('AVP.Commandline')
+
+
+class Command(QtCore.QObject):
+ """
+ This replaces the GUI MainWindow when in commandline mode.
+ """
+
+ createVideo = QtCore.pyqtSignal()
+
+ def __init__(self):
+ super().__init__()
+ self.core = core.Core()
+ core.Core.mode = 'commandline'
+ self.dataDir = self.core.dataDir
+ self.canceled = False
+ self.settings = core.Core.settings
+
+ # ctrl-c stops the export thread
+ signal.signal(signal.SIGINT, self.stopVideo)
+
+ def parseArgs(self):
+ self.parser = argparse.ArgumentParser(
+ description='Create a visualization for an audio file',
+ epilog='EXAMPLE COMMAND: main.py myvideotemplate.avp '
+ '-i ~/Music/song.mp3 -o ~/video.mp4 '
+ '-c 0 image path=~/Pictures/thisWeeksPicture.jpg '
+ '-c 1 video "preset=My Logo" -c 2 vis layout=classic'
+ )
+ self.parser.add_argument(
+ '-i', '--input', metavar='SOUND',
+ help='input audio file'
+ )
+ self.parser.add_argument(
+ '-o', '--output', metavar='OUTPUT',
+ help='output video file'
+ )
+ self.parser.add_argument(
+ '--export-project', action='store_true',
+ help='ignore -i and -o, use input and output from project file'
+ )
+ self.parser.add_argument(
+ '--test', action='store_true',
+ help='run tests, generate logfiles, then exit'
+ )
+ self.parser.add_argument(
+ '--debug', action='store_true',
+ help='create bigger logfiles while program is running'
+ )
+
+ # optional arguments
+ self.parser.add_argument(
+ 'projpath', metavar='path-to-project',
+ help='open a project file (.avp)', nargs='?')
+ self.parser.add_argument(
+ '-c', '--comp', metavar=('LAYER', 'ARG'),
+ help='first arg must be component NAME to insert at LAYER.'
+ '"help" for information about possible args for a component.',
+ nargs='*', action='append')
+
+ self.args = self.parser.parse_args()
+
+ if self.args.debug:
+ core.FILE_LOGLVL = logging.DEBUG
+ core.STDOUT_LOGLVL = logging.DEBUG
+ core.Core.makeLogger()
+
+ if self.args.test:
+ self.runTests()
+ quit(0)
+
+ if self.args.projpath:
+ projPath = self.args.projpath
+ if not os.path.dirname(projPath):
+ projPath = os.path.join(
+ self.settings.value("projectDir"),
+ projPath
+ )
+ if not projPath.endswith('.avp'):
+ projPath += '.avp'
+ success = self.core.openProject(self, projPath)
+ if not success:
+ quit(1)
+ self.core.selectedComponents = list(
+ reversed(self.core.selectedComponents))
+ self.core.componentListChanged()
+
+ if self.args.comp:
+ for comp in self.args.comp:
+ pos = comp[0]
+ name = comp[1]
+ args = comp[2:]
+ try:
+ pos = int(pos)
+ except ValueError:
+ print(pos, 'is not a layer number.')
+ quit(1)
+ realName = self.parseCompName(name)
+ if not realName:
+ print(name, 'is not a valid component name.')
+ quit(1)
+ modI = self.core.moduleIndexFor(realName)
+ i = self.core.insertComponent(pos, modI, self)
+ for arg in args:
+ self.core.selectedComponents[i].command(arg)
+
+ if self.args.export_project and self.args.projpath:
+ errcode, data = self.core.parseAvFile(projPath)
+ for key, value in data['WindowFields']:
+ if 'outputFile' in key:
+ output = value
+ if not os.path.dirname(value):
+ output = os.path.join(
+ os.path.expanduser('~'),
+ output
+ )
+ if 'audioFile' in key:
+ input = value
+ self.createAudioVisualisation(input, output)
+ return "commandline"
+
+ elif self.args.input and self.args.output:
+ self.createAudioVisualisation(self.args.input, self.args.output)
+ return "commandline"
+
+ elif 'help' not in sys.argv and self.args.projpath is None and '--debug' not in sys.argv:
+ self.parser.print_help()
+ quit(1)
+
+ return "GUI"
+
+ def createAudioVisualisation(self, input, output):
+ self.core.selectedComponents = list(
+ reversed(self.core.selectedComponents))
+ self.core.componentListChanged()
+ self.worker = self.core.newVideoWorker(
+ self, input, output
+ )
+ # quit(0) after video is created
+ self.worker.videoCreated.connect(self.videoCreated)
+ self.lastProgressUpdate = time.time()
+ self.worker.progressBarSetText.connect(self.progressBarSetText)
+ self.createVideo.emit()
+
+ def stopVideo(self, *args):
+ self.worker.error = True
+ self.worker.cancelExport()
+ self.worker.cancel()
+
+ @QtCore.pyqtSlot(str)
+ def progressBarSetText(self, value):
+ if 'Export ' in value:
+ # Don't duplicate completion/failure messages
+ return
+ if not value.startswith('Exporting') \
+ and time.time() - self.lastProgressUpdate >= 0.05:
+ # Show most messages very often
+ print(value)
+ elif time.time() - self.lastProgressUpdate >= 2.0:
+ # Give user time to read ffmpeg's output during the export
+ print('##### %s' % value)
+ else:
+ return
+ self.lastProgressUpdate = time.time()
+
+ @QtCore.pyqtSlot()
+ def videoCreated(self):
+ quit(0)
+
+ def showMessage(self, **kwargs):
+ print(kwargs['msg'])
+ if 'detail' in kwargs:
+ print(kwargs['detail'])
+
+ @QtCore.pyqtSlot(str, str)
+ def videoThreadError(self, msg, detail):
+ print(msg)
+ print(detail)
+ quit(1)
+
+ def drawPreview(self, *args):
+ pass
+
+ def parseCompName(self, name):
+ '''Deduces a proper component name out of a commandline arg'''
+
+ if name.title() in self.core.compNames:
+ return name.title()
+ for compName in self.core.compNames:
+ if name.capitalize() in compName:
+ return compName
+
+ compFileNames = [
+ os.path.splitext(
+ os.path.basename(mod.__file__)
+ )[0]
+ for mod in self.core.modules
+ ]
+ for i, compFileName in enumerate(compFileNames):
+ if name.lower() in compFileName:
+ return self.core.compNames[i]
+ return
+
+ return None
+
+ def runTests(self):
+ core.FILE_LOGLVL = logging.DEBUG
+ core.Core.makeLogger()
+ from . import tests
+ test_report = os.path.join(core.Core.logDir, "test_report.log")
+ tests.run(test_report)
+
+ # Print test report into terminal
+ with open(test_report, "r") as f:
+ output = f.readlines()
+ test_output = "".join(output)
+ print(test_output)
+
+ # Choose a numbered location to put the output file
+ logNumber = 0
+ def getFilename():
+ """Get a numbered filename for the final test report"""
+ nonlocal logNumber
+ name = os.path.join(os.path.expanduser('~'), "avp_test_report")
+ while True:
+ possibleName = f"{name}{logNumber:0>2}.txt"
+ if os.path.exists(possibleName) and logNumber < 100:
+ logNumber += 1
+ continue
+ break
+ return possibleName
+
+ # Copy latest debug log to chosen test report location
+ filename = getFilename()
+ if logNumber == 100:
+ print("Test Report could not be created.")
+ return
+ try:
+ shutil.copy(os.path.join(core.Core.logDir, "avp_debug.log"), filename)
+ except FileNotFoundError:
+ print("No debug log found.")
+ # Append actual test report to debug log
+ with open(filename, "a") as f:
+ f.write(f"{'='*59} debug log ends {'='*59}\n")
+ f.write(test_output)
+ print(f"Test Report created at {filename}")