Skip to content
Snippets Groups Projects
Commit 6477e908 authored by Sybren A. Stüvel's avatar Sybren A. Stüvel
Browse files

Added support for commands used in the blender-video-chunks job type

Adds the following commands:

    - blender_render_audio
    - concat_videos
    - create_video
    - move_with_counter
    - mux_audio
parent 12ff49f1
No related branches found
Tags
No related merge requests found
...@@ -6,6 +6,7 @@ changed functionality, fixed bugs). ...@@ -6,6 +6,7 @@ changed functionality, fixed bugs).
## Version 2.2 (in development) ## Version 2.2 (in development)
- Always log the version of Flamenco Worker. - Always log the version of Flamenco Worker.
- Requires Flamenco Manager 2.2 or newer.
- Include missing merge-exr.blend, required for progressive rendering, in the distribution bundle. - Include missing merge-exr.blend, required for progressive rendering, in the distribution bundle.
- Include `exr-merge` task type in default configuration, which is required for progressive - Include `exr-merge` task type in default configuration, which is required for progressive
rendering. rendering.
...@@ -31,6 +32,12 @@ changed functionality, fixed bugs). ...@@ -31,6 +32,12 @@ changed functionality, fixed bugs).
sequence. It's up to Flamenco Server to include (or not) this command in a render job. sequence. It's up to Flamenco Server to include (or not) this command in a render job.
- Explicitly return tasks to the Manager queue when stopping them (that is, when going asleep or - Explicitly return tasks to the Manager queue when stopping them (that is, when going asleep or
shutting down). Requires Flamenco Manager 2.2 or newer. shutting down). Requires Flamenco Manager 2.2 or newer.
- Added support for commands used in the blender-video-chunks job type:
- blender_render_audio
- concat_videos
- create_video
- move_with_counter
- mux_audio
## Version 2.1.0 (2018-01-04) ## Version 2.1.0 (2018-01-04)
......
...@@ -206,6 +206,18 @@ class AbstractCommand(metaclass=abc.ABCMeta): ...@@ -206,6 +206,18 @@ class AbstractCommand(metaclass=abc.ABCMeta):
return value, None return value, None
async def _mkdir_if_not_exists(self, dirpath: Path):
"""Create a directory if it doesn't exist yet.
Also logs a message to the Worker to indicate the directory was created.
"""
if dirpath.exists():
return
await self.worker.register_log('%s: Directory %s does not exist; creating.',
self.command_name, dirpath)
dirpath.mkdir(parents=True)
@command_executor('echo') @command_executor('echo')
class EchoCommand(AbstractCommand): class EchoCommand(AbstractCommand):
...@@ -294,6 +306,29 @@ def _unique_path(path: Path) -> Path: ...@@ -294,6 +306,29 @@ def _unique_path(path: Path) -> Path:
return path.with_name(path.name + '~%i' % (max_nr + 1)) return path.with_name(path.name + '~%i' % (max_nr + 1))
def _numbered_path(directory: Path, fname_prefix: str, fname_suffix: str) -> Path:
"""Return a unique Path with a number between prefix and suffix.
:return: directory / '{fname_prefix}001{fname_suffix}' where 001 is
replaced by the highest number + 1 if there already is a file with
such a prefix & suffix.
"""
# See which suffixes are in use
max_nr = 0
len_prefix = len(fname_prefix)
len_suffix = len(fname_suffix)
for altpath in directory.glob(f'{fname_prefix}*{fname_suffix}'):
num_str: str = altpath.name[len_prefix:-len_suffix]
try:
num = int(num_str)
except ValueError:
continue
max_nr = max(max_nr, num)
return directory / f'{fname_prefix}{max_nr+1:03}{fname_suffix}'
@command_executor('move_out_of_way') @command_executor('move_out_of_way')
class MoveOutOfWayCommand(AbstractCommand): class MoveOutOfWayCommand(AbstractCommand):
def validate(self, settings: Settings): def validate(self, settings: Settings):
...@@ -386,10 +421,7 @@ class CopyFileCommand(AbstractCommand): ...@@ -386,10 +421,7 @@ class CopyFileCommand(AbstractCommand):
self._log.info('Copying %s to %s', src, dest) self._log.info('Copying %s to %s', src, dest)
await self.worker.register_log('%s: Copying %s to %s', self.command_name, src, dest) await self.worker.register_log('%s: Copying %s to %s', self.command_name, src, dest)
if not dest.parent.exists(): await self._mkdir_if_not_exists(dest.parent)
await self.worker.register_log('%s: Target directory %s does not exist; creating.',
self.command_name, dest.parent)
dest.parent.mkdir(parents=True)
shutil.copy(str(src), str(dest)) shutil.copy(str(src), str(dest))
self.worker.output_produced(dest) self.worker.output_produced(dest)
...@@ -422,6 +454,34 @@ class RemoveTreeCommand(AbstractCommand): ...@@ -422,6 +454,34 @@ class RemoveTreeCommand(AbstractCommand):
path.unlink() path.unlink()
@command_executor('remove_file')
class RemoveFileCommand(AbstractCommand):
def validate(self, settings: Settings):
path, err = self._setting(settings, 'path', True)
if err:
return err
if not path:
return "Parameter 'path' cannot be empty."
async def execute(self, settings: Settings):
path = Path(settings['path'])
if not path.exists():
msg = 'Path %s does not exist, so not removing.' % path
self._log.debug(msg)
await self.worker.register_log(msg)
return
if path.is_dir():
raise CommandExecutionError(f'Path {path} is a directory. Cannot remove with '
'this command; use remove_tree instead.')
msg = 'Removing file %s' % path
self._log.info(msg)
await self.worker.register_log(msg)
path.unlink()
@attr.s @attr.s
class AbstractSubprocessCommand(AbstractCommand, abc.ABC): class AbstractSubprocessCommand(AbstractCommand, abc.ABC):
readline_timeout = attr.ib(default=SUBPROC_READLINE_TIMEOUT) readline_timeout = attr.ib(default=SUBPROC_READLINE_TIMEOUT)
...@@ -856,7 +916,7 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): ...@@ -856,7 +916,7 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
# set up node properties and render settings. # set up node properties and render settings.
output = Path(settings['output']) output = Path(settings['output'])
output.parent.mkdir(parents=True, exist_ok=True) await self._mkdir_if_not_exists(output.parent)
with tempfile.TemporaryDirectory(dir=str(output.parent)) as tmpdir: with tempfile.TemporaryDirectory(dir=str(output.parent)) as tmpdir:
tmppath = Path(tmpdir) tmppath = Path(tmpdir)
...@@ -890,19 +950,57 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand): ...@@ -890,19 +950,57 @@ class MergeProgressiveRendersCommand(AbstractSubprocessCommand):
shutil.move(str(src), str(dst)) shutil.move(str(src), str(dst))
@command_executor('create_video') @command_executor('blender_render_audio')
class CreateVideoCommand(AbstractSubprocessCommand): class BlenderRenderAudioCommand(BlenderRenderCommand):
"""Create a video from individual frames. def validate(self, settings: Settings):
err = super().validate(settings)
if err:
return err
Requires FFmpeg to be installed and available with the 'ffmpeg' command. render_output, err = self._setting(settings, 'render_output', True)
""" if err:
return err
if not render_output:
return "'render_output' is a required setting"
codec_video = 'h264' _, err = self._setting(settings, 'frame_start', False, int)
if err:
return err
_, err = self._setting(settings, 'frame_end', False, int)
if err:
return err
def _build_blender_cmd(self, settings: Settings) -> typing.List[str]:
frame_start = settings.get('frame_start')
frame_end = settings.get('frame_end')
render_output = settings.get('render_output')
py_lines = [
"import bpy"
]
if frame_start is not None:
py_lines.append(f'bpy.context.scene.frame_start = {frame_start}')
if frame_end is not None:
py_lines.append(f'bpy.context.scene.frame_end = {frame_end}')
py_lines.append(f"bpy.ops.sound.mixdown(filepath={render_output!r}, "
f"codec='FLAC', container='FLAC', "
f"accuracy=128)")
py_lines.append('bpy.ops.wm.quit_blender()')
py_script = '\n'.join(py_lines)
return [
*settings['blender_cmd'],
'--enable-autoexec',
'-noaudio',
'--background',
settings['filepath'],
'--python-exit-code', '47',
'--python-expr', py_script
]
# Select some settings that are useful for scrubbing through the video.
constant_rate_factor = 17 # perceptually lossless class AbstractFFmpegCommand(AbstractSubprocessCommand, abc.ABC):
keyframe_interval = 1 # GOP size
max_b_frames: typing.Optional[int] = 0
def validate(self, settings: Settings) -> typing.Optional[str]: def validate(self, settings: Settings) -> typing.Optional[str]:
# Check that FFmpeg can be found and shlex-split the string. # Check that FFmpeg can be found and shlex-split the string.
...@@ -916,6 +1014,49 @@ class CreateVideoCommand(AbstractSubprocessCommand): ...@@ -916,6 +1014,49 @@ class CreateVideoCommand(AbstractSubprocessCommand):
return f'FFmpeg command {ffmpeg_cmd!r} not found on $PATH' return f'FFmpeg command {ffmpeg_cmd!r} not found on $PATH'
settings['ffmpeg_cmd'] = cmd settings['ffmpeg_cmd'] = cmd
self._log.debug('Found FFmpeg command at %r', executable_path) self._log.debug('Found FFmpeg command at %r', executable_path)
return None
async def execute(self, settings: Settings) -> None:
cmd = self._build_ffmpeg_command(settings)
await self.subprocess(cmd)
def _build_ffmpeg_command(self, settings: Settings) -> typing.List[str]:
assert isinstance(settings['ffmpeg_cmd'], list), \
'run validate() before _build_ffmpeg_command'
cmd = [
*settings['ffmpeg_cmd'],
*self.ffmpeg_args(settings),
]
return cmd
@abc.abstractmethod
def ffmpeg_args(self, settings: Settings) -> typing.List[str]:
"""Construct the FFmpeg arguments to execute.
Does not need to include the FFmpeg command itself, just
its arguments.
"""
pass
@command_executor('create_video')
class CreateVideoCommand(AbstractFFmpegCommand):
"""Create a video from individual frames.
Requires FFmpeg to be installed and available with the 'ffmpeg' command.
"""
codec_video = 'h264'
# Select some settings that are useful for scrubbing through the video.
constant_rate_factor = 17 # perceptually lossless
keyframe_interval = 1 # GOP size
max_b_frames: typing.Optional[int] = 0
def validate(self, settings: Settings) -> typing.Optional[str]:
err = super().validate(settings)
if err:
return err
# Check that we know our input and output image files. # Check that we know our input and output image files.
input_files, err = self._setting(settings, 'input_files', is_required=True) input_files, err = self._setting(settings, 'input_files', is_required=True)
...@@ -933,26 +1074,210 @@ class CreateVideoCommand(AbstractSubprocessCommand): ...@@ -933,26 +1074,210 @@ class CreateVideoCommand(AbstractSubprocessCommand):
self._log.debug('Frame rate: %r fps', fps) self._log.debug('Frame rate: %r fps', fps)
return None return None
async def execute(self, settings: Settings) -> None: def ffmpeg_args(self, settings: Settings) -> typing.List[str]:
cmd = self._build_ffmpeg_command(settings) args = [
await self.subprocess(cmd)
def _build_ffmpeg_command(self, settings) -> typing.List[str]:
assert isinstance(settings['ffmpeg_cmd'], list), \
'run validate() before _build_ffmpeg_command'
cmd = [
*settings['ffmpeg_cmd'],
'-pattern_type', 'glob', '-pattern_type', 'glob',
'-r', str(settings['fps']),
'-i', settings['input_files'], '-i', settings['input_files'],
'-c:v', self.codec_video, '-c:v', self.codec_video,
'-crf', str(self.constant_rate_factor), '-crf', str(self.constant_rate_factor),
'-g', str(self.keyframe_interval), '-g', str(self.keyframe_interval),
'-r', str(settings['fps']),
'-y', '-y',
] ]
if self.max_b_frames is not None: if self.max_b_frames is not None:
cmd.extend(['-bf', str(self.max_b_frames)]) args.extend(['-bf', str(self.max_b_frames)])
cmd += [ args += [
settings['output_file'] settings['output_file']
] ]
return cmd return args
@command_executor('concatenate_videos')
class ConcatenateVideosCommand(AbstractFFmpegCommand):
"""Create a video by concatenating other videos.
Requires FFmpeg to be installed and available with the 'ffmpeg' command.
"""
index_file: typing.Optional[Path] = None
def validate(self, settings: Settings) -> typing.Optional[str]:
err = super().validate(settings)
if err:
return err
# Check that we know our input and output image files.
input_files, err = self._setting(settings, 'input_files', is_required=True)
if err:
return err
self._log.debug('Input files: %s', input_files)
output_file, err = self._setting(settings, 'output_file', is_required=True)
if err:
return err
self._log.debug('Output file: %s', output_file)
return None
async def execute(self, settings: Settings) -> None:
await super().execute(settings)
if self.index_file is not None and self.index_file.exists():
try:
self.index_file.unlink()
except IOError:
msg = f'unable to unlink file {self.index_file}, ignoring'
self.worker.register_log(msg)
self._log.warning(msg)
def ffmpeg_args(self, settings: Settings) -> typing.List[str]:
input_files = Path(settings['input_files']).absolute()
self.index_file = input_files.with_name('ffmpeg-input.txt')
# Construct the list of filenames for ffmpeg to process.
# The index file needs to sit next to the input files, as
# ffmpeg checks for 'unsafe paths'.
with self.index_file.open('w') as outfile:
for video_path in sorted(input_files.parent.glob(input_files.name)):
escaped = str(video_path.name).replace("'", "\\'")
print("file '%s'" % escaped, file=outfile)
output_file = Path(settings['output_file'])
self._log.debug('Output file: %s', output_file)
args = [
'-f', 'concat',
'-i', str(self.index_file),
'-c', 'copy',
'-y',
str(output_file),
]
return args
@command_executor('mux_audio')
class MuxAudioCommand(AbstractFFmpegCommand):
def validate(self, settings: Settings) -> typing.Optional[str]:
err = super().validate(settings)
if err:
return err
# Check that we know our input and output image files.
audio_file, err = self._setting(settings, 'audio_file', is_required=True)
if err:
return err
if not Path(audio_file).exists():
return f'Audio file {audio_file} does not exist'
self._log.debug('Audio file: %s', audio_file)
video_file, err = self._setting(settings, 'video_file', is_required=True)
if err:
return err
if not Path(video_file).exists():
return f'Video file {video_file} does not exist'
self._log.debug('Video file: %s', video_file)
output_file, err = self._setting(settings, 'output_file', is_required=True)
if err:
return err
self._log.debug('Output file: %s', output_file)
return None
def ffmpeg_args(self, settings: Settings) -> typing.List[str]:
audio_file = Path(settings['audio_file']).absolute()
video_file = Path(settings['video_file']).absolute()
output_file = Path(settings['output_file']).absolute()
args = [
'-i', str(audio_file),
'-i', str(video_file),
'-c', 'copy',
'-y',
str(output_file),
]
return args
@command_executor('encode_audio')
class EncodeAudioCommand(AbstractFFmpegCommand):
def validate(self, settings: Settings) -> typing.Optional[str]:
err = super().validate(settings)
if err:
return err
# Check that we know our input and output image files.
input_file, err = self._setting(settings, 'input_file', is_required=True)
if err:
return err
if not Path(input_file).exists():
return f'Audio file {input_file} does not exist'
self._log.debug('Audio file: %s', input_file)
output_file, err = self._setting(settings, 'output_file', is_required=True)
if err:
return err
self._log.debug('Output file: %s', output_file)
_, err = self._setting(settings, 'bitrate', is_required=True)
if err:
return err
_, err = self._setting(settings, 'codec', is_required=True)
if err:
return err
return None
def ffmpeg_args(self, settings: Settings) -> typing.List[str]:
input_file = Path(settings['input_file']).absolute()
output_file = Path(settings['output_file']).absolute()
args = [
'-i', str(input_file),
'-c:a', settings['codec'],
'-b:a', settings['bitrate'],
'-y',
str(output_file),
]
return args
@command_executor('move_with_counter')
class MoveWithCounterCommand(AbstractCommand):
# Split '2018_12_06-spring.mkv' into a '2018_12_06' prefix and '-spring.mkv' suffix.
filename_parts = re.compile(r'(?P<prefix>^[0-9_]+)(?P<suffix>.*)$')
def validate(self, settings: Settings):
src, err = self._setting(settings, 'src', True)
if err:
return err
if not src:
return 'src may not be empty'
dest, err = self._setting(settings, 'dest', True)
if err:
return err
if not dest:
return 'dest may not be empty'
async def execute(self, settings: Settings):
src = Path(settings['src'])
if not src.exists():
raise CommandExecutionError('Path %s does not exist, unable to move' % src)
dest = Path(settings['dest'])
fname_parts = self.filename_parts.match(dest.name)
if fname_parts:
prefix = fname_parts.group('prefix') + '_'
suffix = fname_parts.group('suffix')
else:
prefix = dest.stem + '_'
suffix = dest.suffix
self._log.debug('Adding counter to output name between %r and %r', prefix, suffix)
dest = _numbered_path(dest.parent, prefix, suffix)
self._log.info('Moving %s to %s', src, dest)
await self.worker.register_log('%s: Moving %s to %s', self.command_name, src, dest)
await self._mkdir_if_not_exists(dest.parent)
shutil.move(str(src), str(dest))
self.worker.output_produced(dest)
from pathlib import Path
import subprocess
from unittest import mock
from unittest.mock import patch
from tests.test_runner import AbstractCommandTest
expected_script = """
import bpy
bpy.context.scene.frame_start = 1
bpy.context.scene.frame_end = 47
bpy.ops.sound.mixdown(filepath='/tmp/output.flac', codec='FLAC', container='FLAC', accuracy=128)
bpy.ops.wm.quit_blender()
""".strip('\n')
class RenderAudioTest(AbstractCommandTest):
def setUp(self):
super().setUp()
from flamenco_worker.commands import BlenderRenderAudioCommand
self.cmd = BlenderRenderAudioCommand(
worker=self.fworker,
task_id='12345',
command_idx=0,
)
def test_cli_args(self):
from tests.mock_responses import CoroMock
filepath = Path(__file__)
settings = {
# Point blender_cmd to this file so that we're sure it exists.
'blender_cmd': '%s --with --cli="args for CLI"' % __file__,
'frame_start': 1,
'frame_end': 47,
'filepath': str(filepath),
'render_output': '/tmp/output.flac',
}
cse = CoroMock(...)
cse.coro.return_value.wait = CoroMock(return_value=0)
cse.coro.return_value.pid = 47
with patch('asyncio.create_subprocess_exec', new=cse) as mock_cse:
self.loop.run_until_complete(self.cmd.run(settings))
mock_cse.assert_called_once_with(
__file__,
'--with',
'--cli=args for CLI',
'--enable-autoexec',
'-noaudio',
'--background',
str(filepath),
'--python-exit-code', '47',
'--python-expr', expected_script,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
import logging
import shutil
import typing
from pathlib import Path
import shlex
import subprocess
import sys
import tempfile
from tests.test_runner import AbstractCommandTest
log = logging.getLogger(__name__)
frame_dir = Path(__file__).with_name('test_frames')
class ConcatVideosTest(AbstractCommandTest):
settings: typing.Dict[str, typing.Any] = {
'ffmpeg_cmd': f'"{sys.executable}" -hide_banner',
'input_files': str(frame_dir / 'chunk-*.mkv'),
'output_file': '/tmp/merged.mkv',
}
def setUp(self):
super().setUp()
from flamenco_worker.commands import ConcatenateVideosCommand
self.cmd = ConcatenateVideosCommand(
worker=self.fworker,
task_id='12345',
command_idx=0,
)
self.settings = self.settings.copy()
def test_build_ffmpeg_cmd(self):
self.cmd.validate(self.settings)
cliargs = self.cmd._build_ffmpeg_command(self.settings)
self.assertEqual([
sys.executable, '-hide_banner',
'-f', 'concat',
'-i', str(frame_dir / 'ffmpeg-input.txt'),
'-c', 'copy',
'-y',
'/tmp/merged.mkv',
], cliargs)
def test_run_ffmpeg(self):
with tempfile.TemporaryDirectory() as tempdir:
outfile = Path(tempdir) / 'merged.mkv'
settings: typing.Dict[str, typing.Any] = {
**self.settings,
'ffmpeg_cmd': 'ffmpeg', # use the real FFmpeg for this test.
'output_file': str(outfile),
}
self.loop.run_until_complete(self.cmd.run(settings))
self.assertTrue(outfile.exists())
ffprobe_cmd = [shutil.which('ffprobe'), '-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
str(outfile)]
log.debug('Running %s', ' '.join(shlex.quote(arg) for arg in ffprobe_cmd))
probe_out = subprocess.check_output(ffprobe_cmd)
probed_duration = float(probe_out)
# The combined videos are 7 frames @ 24 frames per second.
self.assertAlmostEqual(0.291, probed_duration, places=3)
...@@ -12,7 +12,7 @@ from tests.test_runner import AbstractCommandTest ...@@ -12,7 +12,7 @@ from tests.test_runner import AbstractCommandTest
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class BlenderRenderTest(AbstractCommandTest): class CreateVideoTest(AbstractCommandTest):
settings: typing.Dict[str, typing.Any] = { settings: typing.Dict[str, typing.Any] = {
'ffmpeg_cmd': f'"{sys.executable}" -hide_banner', 'ffmpeg_cmd': f'"{sys.executable}" -hide_banner',
'input_files': '/tmp/*.png', 'input_files': '/tmp/*.png',
...@@ -51,11 +51,11 @@ class BlenderRenderTest(AbstractCommandTest): ...@@ -51,11 +51,11 @@ class BlenderRenderTest(AbstractCommandTest):
self.assertEqual([ self.assertEqual([
sys.executable, '-hide_banner', sys.executable, '-hide_banner',
'-pattern_type', 'glob', '-pattern_type', 'glob',
'-r', '24',
'-i', '/tmp/*.png', '-i', '/tmp/*.png',
'-c:v', 'h264', '-c:v', 'h264',
'-crf', '17', '-crf', '17',
'-g', '1', '-g', '1',
'-r', '24',
'-y', '-y',
'-bf', '0', '-bf', '0',
'/tmp/merged.mkv', '/tmp/merged.mkv',
......
import logging
import shutil
import typing
from pathlib import Path
import shlex
import subprocess
import sys
import tempfile
from tests.test_runner import AbstractCommandTest
log = logging.getLogger(__name__)
frame_dir = Path(__file__).with_name('test_frames')
class EncodeAudioTest(AbstractCommandTest):
settings: typing.Dict[str, typing.Any] = {
'ffmpeg_cmd': f'"{sys.executable}" -hide_banner',
'input_file': f'{frame_dir}/audio.flac',
'codec': 'aac',
'bitrate': '192k',
'output_file': f'{frame_dir}/audio.aac',
}
def setUp(self):
super().setUp()
from flamenco_worker.commands import EncodeAudioCommand
self.cmd = EncodeAudioCommand(
worker=self.fworker,
task_id='12345',
command_idx=0,
)
self.settings = self.__class__.settings.copy()
def test_build_ffmpeg_cmd(self):
self.cmd.validate(self.settings)
cliargs = self.cmd._build_ffmpeg_command(self.settings)
self.assertEqual([
sys.executable, '-hide_banner',
'-i', str(frame_dir / 'audio.flac'),
'-c:a', 'aac',
'-b:a', '192k',
'-y',
str(frame_dir / 'audio.aac'),
], cliargs)
import logging
import shutil
import typing
from pathlib import Path
import shlex
import subprocess
import sys
import tempfile
from tests.test_runner import AbstractCommandTest
log = logging.getLogger(__name__)
frame_dir = Path(__file__).with_name('test_frames')
class MoveWithCounterTest(AbstractCommandTest):
def setUp(self):
super().setUp()
from flamenco_worker.commands import MoveWithCounterCommand
self.cmd = MoveWithCounterCommand(
worker=self.fworker,
task_id='12345',
command_idx=0,
)
self._tempdir = tempfile.TemporaryDirectory()
self.temppath = Path(self._tempdir.name)
self.srcpath = (self.temppath / 'somefile.mkv')
self.srcpath.touch()
(self.temppath / '2018_06_12_001-spring.mkv').touch()
(self.temppath / '2018_06_12_004-spring.mkv').touch()
def tearDown(self):
self._tempdir.cleanup()
super().tearDown()
def test_numbers_with_holes(self):
settings = {
'src': str(self.srcpath),
'dest': str(self.temppath / '2018_06_12-spring.mkv'),
}
task = self.cmd.execute(settings)
self.loop.run_until_complete(task)
self.assertFalse(self.srcpath.exists())
self.assertTrue((self.temppath / '2018_06_12_005-spring.mkv').exists())
def test_no_regexp_match(self):
settings = {
'src': str(self.srcpath),
'dest': str(self.temppath / 'jemoeder.mkv'),
}
task = self.cmd.execute(settings)
self.loop.run_until_complete(task)
self.assertFalse(self.srcpath.exists())
self.assertTrue((self.temppath / 'jemoeder_001.mkv').exists())
import logging
import shutil
import typing
from pathlib import Path
import shlex
import subprocess
import sys
import tempfile
from tests.test_runner import AbstractCommandTest
log = logging.getLogger(__name__)
frame_dir = Path(__file__).with_name('test_frames')
class MuxAudioTest(AbstractCommandTest):
settings: typing.Dict[str, typing.Any] = {
'ffmpeg_cmd': f'"{sys.executable}" -hide_banner',
'audio_file': str(frame_dir / 'audio.mkv'),
'video_file': str(frame_dir / 'video.mkv'),
'output_file': str(frame_dir / 'muxed.mkv'),
}
def setUp(self):
super().setUp()
from flamenco_worker.commands import MuxAudioCommand
self.cmd = MuxAudioCommand(
worker=self.fworker,
task_id='12345',
command_idx=0,
)
self.settings = self.settings.copy()
def test_build_ffmpeg_cmd(self):
self.cmd.validate(self.settings)
cliargs = self.cmd._build_ffmpeg_command(self.settings)
self.assertEqual([
sys.executable, '-hide_banner',
'-i', str(frame_dir / 'audio.mkv'),
'-i', str(frame_dir / 'video.mkv'),
'-c', 'copy',
'-y',
str(frame_dir / 'muxed.mkv'),
], cliargs)
from pathlib import Path
from tests.test_runner import AbstractCommandTest
class RemoveFileTest(AbstractCommandTest):
def setUp(self):
super().setUp()
from flamenco_worker.commands import RemoveFileCommand
import tempfile
self.tmpdir = tempfile.TemporaryDirectory()
self.tmppath = Path(self.tmpdir.name)
self.cmd = RemoveFileCommand(
worker=self.fworker,
task_id='12345',
command_idx=0,
)
def tearDown(self):
super().tearDown()
self.tmpdir.cleanup()
def test_validate_settings(self):
self.assertIn('path', self.cmd.validate({'path': 12}))
self.assertIn('path', self.cmd.validate({'path': ''}))
self.assertIn('path', self.cmd.validate({}))
self.assertFalse(self.cmd.validate({'path': '/some/path'}))
def test_nonexistant_source(self):
path = self.tmppath / 'nonexisting'
task = self.cmd.run({'path': str(path)})
ok = self.loop.run_until_complete(task)
self.assertTrue(ok)
self.assertFalse(path.exists())
def test_source_file(self):
path = self.tmppath / 'existing'
path.touch()
task = self.cmd.run({'path': str(path)})
ok = self.loop.run_until_complete(task)
self.assertTrue(ok)
self.assertFalse(path.exists())
def test_soure_dir_with_files(self):
path = self.tmppath / 'dir'
path.mkdir()
(path / 'a.file').touch()
(path / 'b.file').touch()
(path / 'c.file').touch()
task = self.cmd.run({'path': str(path)})
ok = self.loop.run_until_complete(task)
self.assertFalse(ok)
self.assertTrue(path.exists())
File added
File added
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment