diff --git a/bin/ffx.py b/bin/ffx.py index 082bd87..c109491 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -31,11 +31,12 @@ VERSION='0.1.3' @click.group() @click.pass_context -def ffx(ctx): +@click.option('--database-file', type=str, default='', help='Path to database file') +def ffx(ctx, database_file): """FFX""" ctx.obj = {} - ctx.obj['database'] = databaseContext() + ctx.obj['database'] = databaseContext(databasePath=database_file) # Define a subcommand diff --git a/bin/ffx/database.py b/bin/ffx/database.py index 545074f..5b1783a 100644 --- a/bin/ffx/database.py +++ b/bin/ffx/database.py @@ -11,17 +11,21 @@ from ffx.model.media_tag import MediaTag from ffx.model.track_tag import TrackTag -def databaseContext(): +def databaseContext(databasePath: str = ''): + """sqlite:///:memory:""" databaseContext = {} - # Initialize DB - homeDir = os.path.expanduser("~") - ffxVarDir = os.path.join(homeDir, '.local', 'var', 'ffx') - if not os.path.exists(ffxVarDir): - os.makedirs(ffxVarDir) + if databasePath is None: + databasePath = ':memory:' + elif not databasePath: + homeDir = os.path.expanduser("~") + ffxVarDir = os.path.join(homeDir, '.local', 'var', 'ffx') + if not os.path.exists(ffxVarDir): + os.makedirs(ffxVarDir) + databasePath = os.path.join(ffxVarDir, 'ffx.db') - databaseContext['url'] = f"sqlite:///{os.path.join(ffxVarDir, 'ffx.db')}" + databaseContext['url'] = f"sqlite:///{databasePath}" databaseContext['engine'] = create_engine(databaseContext['url']) databaseContext['session'] = sessionmaker(bind=databaseContext['engine']) diff --git a/bin/ffx/ffx_controller.py b/bin/ffx/ffx_controller.py index cf07443..e4f4fec 100644 --- a/bin/ffx/ffx_controller.py +++ b/bin/ffx/ffx_controller.py @@ -456,3 +456,27 @@ class FfxController(): if rc: raise click.ClickException(f"Command resulted in error: rc={rc} error={err}") + + + def createEmptyFile(self, + path: str = 'output.mp4', + sizeX: int = 1280, + sizeY: int = 720, + rate: int = 25, + length: int = 10): + + commandTokens = FfxController.COMMAND_TOKENS + + commandTokens += ['-f', + 'lavfi', + '-i', + f"color=size={sizeX}x{sizeY}:rate={rate}:color=black", + '-f', + 'lavfi', + '-i', + 'anullsrc=channel_layout=stereo:sample_rate=44100', + '-t', + str(length), + path] + + out, err, rc = executeProcess(commandTokens) diff --git a/bin/ffx/test/helper.py b/bin/ffx/test/helper.py new file mode 100644 index 0000000..757cf7c --- /dev/null +++ b/bin/ffx/test/helper.py @@ -0,0 +1,191 @@ +import os, math, tempfile + +from ffx.process import executeProcess + + +def getTimeString(hours: float = 0.0, + minutes: float = 0.0, + seconds: float = 0.0, + millis: float = 0.0, + format: str = ''): + + duration = (hours * 3600.0 + + minutes * 60.0 + + seconds + + millis / 1000.0) + + hours = math.floor(duration / 3600.0) + remaining = duration - 3600.0 * hours + + minutes = math.floor(remaining / 60.0) + remaining = remaining - 60.0 * minutes + + seconds = math.floor(remaining) + remaining = remaining - seconds + + millis = math.floor(remaining * 1000) + + if format == 'ass': + return f"{hours:01d}:{minutes:02d}:{seconds:02d}.{millis:02d}" + + # srt & vtt + return f"{hours:02d}:{minutes:02d}:{seconds:02d}.{millis:03d}" + + +def createAssFile(entries: dict): + + # [Script Info] + # ; Script generated by FFmpeg/Lavc61.3.100 + # ScriptType: v4.00+ + # PlayResX: 384 + # PlayResY: 288 + # ScaledBorderAndShadow: yes + # YCbCr Matrix: None + # + # [V4+ Styles] + # Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding + # Style: Default,Arial,16,&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,1 + # + # [Events] + # Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text + # Dialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,yolo + # Dialogue: 0,0:00:03.00,0:00:04.00,Default,,0,0,0,,zolo + # Dialogue: 0,0:00:05.00,0:00:06.00,Default,,0,0,0,,golo + tmpFileName = tempfile.mktemp(suffix=".ass") + + with open(tmpFileName, 'w') as tmpFile: + + tmpFile.write("[Script Info]\n") + tmpFile.write("; Script generated by Ffx\n") + tmpFile.write("ScriptType: v4.00+\n") + tmpFile.write("PlayResX: 384\n") + tmpFile.write("PlayResY: 288\n") + tmpFile.write("ScaledBorderAndShadow: yes\n") + tmpFile.write("YCbCr Matrix: None\n") + tmpFile.write("\n") + tmpFile.write("[V4+ Styles]\n") + tmpFile.write("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n") + tmpFile.write("Style: Default,Arial,16,&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,1\n") + tmpFile.write("\n") + tmpFile.write("[Events]\n") + tmpFile.write("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n") + + for entryIndex in range(len(entries)): + tmpFile.write(f"Dialogue: 0,{getTimeString(seconds=entries[entryIndex]['start'], format='ass')},{getTimeString(seconds=entries[entryIndex]['end'], format='ass')},Default,,0,0,0,,{entries[entryIndex]['text']}\n") + + return tmpFileName + +def createSrtFile(entries: dict): + # 1 + # 00:00:00,000 --> 00:00:02,500 + # Welcome to the Example Subtitle File! + # + # 2 + # 00:00:03,000 --> 00:00:06,000 + # This is a demonstration of SRT subtitles. + # + # 3 + # 00:00:07,000 --> 00:00:10,500 + # You can use SRT files to add subtitles to your videos. + + tmpFileName = tempfile.mktemp(suffix=".srt") + + with open(tmpFileName, 'w') as tmpFile: + + for entryIndex in range(len(entries)): + + tmpFile.write(f"{entryIndex}\n") + tmpFile.write(f"{getTimeString(seconds=entries[entryIndex]['start'])} --> {getTimeString(seconds=entries[entryIndex]['end'])}\n") + tmpFile.write(f"{entries[entryIndex]['text']}\n\n") + + return tmpFileName + + +def createVttFile(entries: dict): + # WEBVTT + # + # 01:20:33.050 --> 01:20:35.050 + # Yolo + + tmpFileName = tempfile.mktemp(suffix=".vtt") + + with open(tmpFileName, 'w') as tmpFile: + + tmpFile.write("WEBVTT\n") + + for entryIndex in range(len(entries)): + + tmpFile.write("\n") + tmpFile.write(f"{getTimeString(seconds=entries[entryIndex]['start'])} --> {getTimeString(seconds=entries[entryIndex]['end'])}\n") + tmpFile.write(f"{entries[entryIndex]['text']}\n") + + + return tmpFileName + + +def createMediaFile(directory: str = '', + baseName: str = 'media', + format: str = '', + extension: str = 'mkv', + sizeX: int = 1280, + sizeY: int = 720, + rate: int = 25, + length: int = 10): + + # subtitleContent = [] + # subtitleContent.append({'start': 1, 'end': 2, 'text': 'yolo'}) + # subtitleContent.append({'start': 3, 'end': 4, 'text': 'zolo'}) + # subtitleContent.append({'start': 5, 'end': 6, 'text': 'golo'}) + + # subtitleFilePath = createVttFile(subtitleContent) + + + # commandTokens = FfxController.COMMAND_TOKENS + commandTokens = ['ffmpeg', '-y'] + + commandTokens += ['-f', + 'lavfi', + '-i', + f"color=size={sizeX}x{sizeY}:rate={rate}:color=black"] + + commandTokens += ['-f', + 'lavfi', + '-i', + 'anullsrc=channel_layout=stereo:sample_rate=44100'] + + # '-i', + # subtitleFilePath, + + commandTokens += ['-map', '0:v:0'] + #'-map', '0:v:0', + commandTokens += ['-map', '1:a:0'] + commandTokens += ['-map', '1:a:0'] + + # '-map', '2:s:0', + # '-c:s', 'webvtt', + + commandTokens += ['-t', str(length)] + + if format: + commandTokens += ['-f', format] + + fileName = f"{baseName}.{extension}" + + if directory: + outputPath = os.path.join(directory, fileName) + else: + outputPath = fileName + + commandTokens += [outputPath] + + + print(f"command sequence: {commandTokens}") + out, err, rc = executeProcess(commandTokens) + if rc: + print(f"Creating testfile failed with {rc}: {err}") + +def createEmptyDirectory(): + return tempfile.mkdtemp() + +def createEmptyFile(suffix=None): + return tempfile.mkstemp(suffix=suffix) diff --git a/bin/ffx/test/scenario.py b/bin/ffx/test/scenario.py new file mode 100644 index 0000000..92a9318 --- /dev/null +++ b/bin/ffx/test/scenario.py @@ -0,0 +1,26 @@ +import os, sys, importlib, glob, inspect + +from ffx.test.helper import createEmptyDirectory + + +class Scenario(): + + def __init__(self): + self._testDirectory = createEmptyDirectory() + + @staticmethod + def list(): + + basePath = os.path.dirname(__file__) + + return [int(os.path.basename(p)[8:-3]) + for p + in glob.glob(f"{ basePath }/scenario*.py", recursive = True) + if p != __file__] + + @staticmethod + def getClassReference(identifier): + importlib.import_module(f"ffx.test.scenario{ identifier }") + for name, obj in inspect.getmembers(sys.modules[f"ffx.test.scenario{ identifier }"]): + if inspect.isclass(obj) and name == f"Scenario{identifier}": + return obj diff --git a/bin/ffx/test/scenario1.py b/bin/ffx/test/scenario1.py new file mode 100644 index 0000000..7c4a3bb --- /dev/null +++ b/bin/ffx/test/scenario1.py @@ -0,0 +1,23 @@ +import click + +from .scenario import Scenario + +from ffx.test.helper import createMediaFile + +class Scenario1(Scenario): + """Creating file VAa, h264/aac/aac + Converting to VaA, vp9/opus/opus + No tmdb, default parameters""" + + def __init__(self): + super().__init__() + + def i_am(self): + return 1 + + def run(self): + + click.echo(f"Running scenario 1") + + createMediaFile(directory=self._testDirectory) + diff --git a/bin/ffx/test/scenario2.py b/bin/ffx/test/scenario2.py new file mode 100644 index 0000000..0dac987 --- /dev/null +++ b/bin/ffx/test/scenario2.py @@ -0,0 +1,12 @@ +from .scenario import Scenario + +class Scenario2(Scenario): + + def __init__(self): + super().__init__() + + def run(self): + pass + # self._testDirectory + + #createMediaFile() diff --git a/bin/ffx/test/scenario3.py b/bin/ffx/test/scenario3.py new file mode 100644 index 0000000..46e010b --- /dev/null +++ b/bin/ffx/test/scenario3.py @@ -0,0 +1,12 @@ +from .scenario import Scenario + +class Scenario3(Scenario): + + def __init__(self): + super().__init__() + + def run(self): + pass + # self._testDirectory + + #createMediaFile() diff --git a/bin/ffx_tests.py b/bin/ffx_tests.py new file mode 100755 index 0000000..16f0e6e --- /dev/null +++ b/bin/ffx_tests.py @@ -0,0 +1,80 @@ +#! /usr/bin/python3 + +import os, sys, subprocess, json, click, time, re, tempfile, math +from datetime import datetime, timedelta + +from ffx.file_properties import FileProperties + +from ffx.ffx_app import FfxApp +from ffx.ffx_controller import FfxController +from ffx.show_controller import ShowController +from ffx.tmdb_controller import TmdbController + +from ffx.database import databaseContext + +from ffx.track_descriptor import TrackDescriptor +from ffx.track_type import TrackType +from ffx.video_encoder import VideoEncoder +from ffx.track_disposition import TrackDisposition + +from ffx.process import executeProcess + +from ffx.test.helper import createMediaFile + +from ffx.test.scenario import Scenario + +VERSION='0.1.0' + +# 0.1.1 +# Bugfixes, TMBD identify shows +# 0.1.2 +# Bugfixes +# 0.1.3 +# Subtitle file imports + + +@click.group() +@click.pass_context +def ffx(ctx): + """FFX""" + + ctx.obj = {} + ctx.obj['database'] = databaseContext(databasePath=None) + + +# Define a subcommand +@ffx.command() +def version(): + click.echo(VERSION) + + +# Another subcommand +@ffx.command() +def help(): + click.echo(f"ffx tests {VERSION}\n") + click.echo(f"Usage: ffx_test ...") + + +# @ffx.command() +# def show(): +# for i in Scenario().list(): +# click.echo(i) + + +# Another subcommand +@ffx.command() +def run(): + """Run ffx test sequences""" + + for scenarioIndex in Scenario().list(): + + scenario = Scenario().getClassReference(scenarioIndex)() + + click.echo(f"Running scenario {scenarioIndex}") + + scenario.run() + + + +if __name__ == '__main__': + ffx()