diff --git a/media/-video-with-dash-.mp4 b/media/-video-with-dash-.mp4 new file mode 100644 index 000000000..ad89c0e66 Binary files /dev/null and b/media/-video-with-dash-.mp4 differ diff --git a/moviepy/audio/io/ffmpeg_audiowriter.py b/moviepy/audio/io/ffmpeg_audiowriter.py index 3d7fea660..7b0123a15 100644 --- a/moviepy/audio/io/ffmpeg_audiowriter.py +++ b/moviepy/audio/io/ffmpeg_audiowriter.py @@ -6,7 +6,7 @@ from moviepy.config import FFMPEG_BINARY from moviepy.decorators import requires_duration -from moviepy.tools import cross_platform_popen_params +from moviepy.tools import cross_platform_popen_params, ffmpeg_escape_filename class FFMPEG_AudioWriter: @@ -89,7 +89,7 @@ def __init__( if input_video is None: cmd.extend(["-vn"]) else: - cmd.extend(["-i", input_video, "-vcodec", "copy"]) + cmd.extend(["-i", ffmpeg_escape_filename(input_video), "-vcodec", "copy"]) cmd.extend(["-acodec", codec] + ["-ar", "%d" % fps_input]) cmd.extend(["-strict", "-2"]) # needed to support codec 'aac' @@ -97,7 +97,7 @@ def __init__( cmd.extend(["-ab", bitrate]) if ffmpeg_params is not None: cmd.extend(ffmpeg_params) - cmd.extend([filename]) + cmd.extend([ffmpeg_escape_filename(filename)]) popen_params = cross_platform_popen_params( {"stdout": sp.DEVNULL, "stderr": logfile, "stdin": sp.PIPE} diff --git a/moviepy/audio/io/readers.py b/moviepy/audio/io/readers.py index 80f0f6dea..5d57f5488 100644 --- a/moviepy/audio/io/readers.py +++ b/moviepy/audio/io/readers.py @@ -6,7 +6,7 @@ import numpy as np from moviepy.config import FFMPEG_BINARY -from moviepy.tools import cross_platform_popen_params +from moviepy.tools import cross_platform_popen_params, ffmpeg_escape_filename from moviepy.video.io.ffmpeg_reader import ffmpeg_parse_infos @@ -80,13 +80,13 @@ def initialize(self, start_time=0): "-ss", "%.05f" % (start_time - offset), "-i", - self.filename, + ffmpeg_escape_filename(self.filename), "-vn", "-ss", "%.05f" % offset, ] else: - i_arg = ["-i", self.filename, "-vn"] + i_arg = ["-i", ffmpeg_escape_filename(self.filename), "-vn"] cmd = ( [FFMPEG_BINARY] diff --git a/moviepy/tools.py b/moviepy/tools.py index 52917b40f..fab9791f0 100644 --- a/moviepy/tools.py +++ b/moviepy/tools.py @@ -4,6 +4,7 @@ import platform import subprocess as sp import warnings +import shlex import proglog @@ -50,6 +51,18 @@ def subprocess_call(cmd, logger="bar"): del proc +def ffmpeg_escape_filename(filename): + """Escape a filename that we want to pass to the ffmpeg command line + + That will ensure the filename doesn't start with a '-' (which would raise an error) + and use `shlex.quote` to escape filenames with spaces and special chars. + """ + if filename.startswith('-') : + filename = './' + filename + + return shlex.quote(filename) + + def convert_to_seconds(time): """Will convert any time into seconds. diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py index b22aa2b6d..810842ed3 100644 --- a/moviepy/video/io/ffmpeg_reader.py +++ b/moviepy/video/io/ffmpeg_reader.py @@ -8,7 +8,11 @@ import numpy as np from moviepy.config import FFMPEG_BINARY # ffmpeg, ffmpeg.exe, etc... -from moviepy.tools import convert_to_seconds, cross_platform_popen_params +from moviepy.tools import ( + convert_to_seconds, + cross_platform_popen_params, + ffmpeg_escape_filename +) class FFMPEG_VideoReader: @@ -91,12 +95,12 @@ def initialize(self, start_time=0): "-ss", "%.06f" % (start_time - offset), "-i", - self.filename, + ffmpeg_escape_filename(self.filename), "-ss", "%.06f" % offset, ] else: - i_arg = ["-i", self.filename] + i_arg = ["-i", ffmpeg_escape_filename(self.filename)] cmd = ( [FFMPEG_BINARY] @@ -801,7 +805,7 @@ def ffmpeg_parse_infos( https://github.com/Zulko/moviepy/pull/1222). """ # Open the file in a pipe, read output - cmd = [FFMPEG_BINARY, "-hide_banner", "-i", filename] + cmd = [FFMPEG_BINARY, "-hide_banner", "-i", ffmpeg_escape_filename(filename)] if decode_file: cmd.extend(["-f", "null", "-"]) diff --git a/moviepy/video/io/ffmpeg_tools.py b/moviepy/video/io/ffmpeg_tools.py index 610a8cc9c..926d07317 100644 --- a/moviepy/video/io/ffmpeg_tools.py +++ b/moviepy/video/io/ffmpeg_tools.py @@ -4,7 +4,7 @@ from moviepy.config import FFMPEG_BINARY from moviepy.decorators import convert_parameter_to_seconds, convert_path_to_string -from moviepy.tools import subprocess_call +from moviepy.tools import subprocess_call, ffmpeg_escape_filename @convert_path_to_string(("inputfile", "outputfile")) @@ -41,7 +41,7 @@ def ffmpeg_extract_subclip( "-ss", "%0.2f" % start_time, "-i", - inputfile, + ffmpeg_escape_filename(inputfile), "-t", "%0.2f" % (end_time - start_time), "-map", @@ -51,7 +51,7 @@ def ffmpeg_extract_subclip( "-acodec", "copy", "-copyts", - outputfile, + ffmpeg_escape_filename(outputfile), ] subprocess_call(cmd, logger=logger) @@ -89,14 +89,14 @@ def ffmpeg_merge_video_audio( FFMPEG_BINARY, "-y", "-i", - audiofile, + ffmpeg_escape_filename(audiofile), "-i", - videofile, + ffmpeg_escape_filename(videofile), "-vcodec", video_codec, "-acodec", audio_codec, - outputfile, + ffmpeg_escape_filename(outputfile), ] subprocess_call(cmd, logger=logger) @@ -125,12 +125,12 @@ def ffmpeg_extract_audio(inputfile, outputfile, bitrate=3000, fps=44100, logger= FFMPEG_BINARY, "-y", "-i", - inputfile, + ffmpeg_escape_filename(inputfile), "-ab", "%dk" % bitrate, "-ar", "%d" % fps, - outputfile, + ffmpeg_escape_filename(outputfile), ] subprocess_call(cmd, logger=logger) @@ -154,10 +154,10 @@ def ffmpeg_resize(inputfile, outputfile, size, logger="bar"): cmd = [ FFMPEG_BINARY, "-i", - inputfile, + ffmpeg_escape_filename(inputfile), "-vf", "scale=%d:%d" % (size[0], size[1]), - outputfile, + ffmpeg_escape_filename(outputfile), ] subprocess_call(cmd, logger=logger) @@ -194,7 +194,16 @@ def ffmpeg_stabilize_video( outputfile = f"{name}_stabilized{ext}" outputfile = os.path.join(output_dir, outputfile) - cmd = [FFMPEG_BINARY, "-i", inputfile, "-vf", "deshake", outputfile] + cmd = [ + FFMPEG_BINARY, + "-i", + ffmpeg_escape_filename(inputfile), + "-vf", + "deshake", + ffmpeg_escape_filename(outputfile) + ] + if overwrite_file: cmd.append("-y") + subprocess_call(cmd, logger=logger) diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index cbae415a0..e66cb488f 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -9,7 +9,7 @@ from proglog import proglog from moviepy.config import FFMPEG_BINARY -from moviepy.tools import cross_platform_popen_params +from moviepy.tools import cross_platform_popen_params, ffmpeg_escape_filename class FFMPEG_VideoWriter: @@ -119,7 +119,7 @@ def __init__( "-", ] if audiofile is not None: - cmd.extend(["-i", audiofile, "-acodec", "copy"]) + cmd.extend(["-i", ffmpeg_escape_filename(audiofile), "-acodec", "copy"]) cmd.extend(["-vcodec", codec, "-preset", preset]) if ffmpeg_params is not None: cmd.extend(ffmpeg_params) @@ -131,7 +131,7 @@ def __init__( if (codec == "libx264") and (size[0] % 2 == 0) and (size[1] % 2 == 0): cmd.extend(["-pix_fmt", "yuv420p"]) - cmd.extend([filename]) + cmd.extend([ffmpeg_escape_filename(filename)]) popen_params = cross_platform_popen_params( {"stdout": sp.DEVNULL, "stderr": logfile, "stdin": sp.PIPE} @@ -306,7 +306,7 @@ def ffmpeg_write_image(filename, image, logfile=False, pixel_format=None): pixel_format, "-i", "-", - filename, + ffmpeg_escape_filename(filename), ] if logfile: diff --git a/tests/test_issues.py b/tests/test_issues.py index ca61cc1f8..4bf46bd5e 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -360,5 +360,14 @@ def test_issue_1682_2(util): clip.write_audiofile(output_audio_filepath) +def test_issue_2160(util): + filename = "media/-video-with-dash-.mp4" + clip = VideoFileClip(filename) + output_video_filepath = os.path.join( + util.TMP_DIR, "big_buck_bunny_0_30_cutout.webm" + ) + clip.write_videofile(output_video_filepath) + + if __name__ == "__main__": pytest.main() diff --git a/tests/test_tools.py b/tests/test_tools.py index 0e4e5807c..52cadf32d 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -65,6 +65,23 @@ def test_subprocess_call(command): tools.subprocess_call(command, logger=None) +@pytest.mark.parametrize( + "given, expected", + [ + ("-filenamethatstartswithdash-.mp4", "./-filenamethatstartswithdash-.mp4"), + ("-path/that/starts/with/dash.mp4", "./-path/that/starts/with/dash.mp4"), + ("file-name-.mp4", "file-name-.mp4"), + ("/absolute/path/to/-file.mp4", "/absolute/path/to/-file.mp4"), + ("filename with spaces.mp4", "'filename with spaces.mp4'") + ], +) +def test_ffmpeg_escape_filename(given, expected): + """Test the ffmpeg_escape_filename function outputs correct paths as per + the docstring. + """ + assert tools.ffmpeg_escape_filename(given) == expected + + @pytest.mark.parametrize("os_name", (os.name, "nt")) def test_cross_platform_popen_params(os_name, monkeypatch): tools_module = importlib.import_module("moviepy.tools")