diff --git a/media/-filenamethatstartswithdash-.mp4 b/media/-filenamethatstartswithdash-.mp4 new file mode 100644 index 000000000..ad89c0e66 Binary files /dev/null and b/media/-filenamethatstartswithdash-.mp4 differ diff --git a/moviepy/audio/io/ffmpeg_audiowriter.py b/moviepy/audio/io/ffmpeg_audiowriter.py index 60c2de508..d1d41b24d 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, dash_escape class FFMPEG_AudioWriter: @@ -74,7 +74,7 @@ def __init__( if input_video is None: cmd.extend(["-vn"]) else: - cmd.extend(["-i", input_video, "-vcodec", "copy"]) + cmd.extend(["-i", dash_escape(input_video), "-vcodec", "copy"]) cmd.extend(["-acodec", codec] + ["-ar", "%d" % fps_input]) cmd.extend(["-strict", "-2"]) # needed to support codec 'aac' @@ -82,7 +82,7 @@ def __init__( cmd.extend(["-ab", bitrate]) if ffmpeg_params is not None: cmd.extend(ffmpeg_params) - cmd.extend([filename]) + cmd.extend([dash_escape(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 4f60efb44..42997ee40 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, dash_escape from moviepy.video.io.ffmpeg_reader import ffmpeg_parse_infos @@ -82,13 +82,13 @@ def initialize(self, start_time=0): "-ss", "%.05f" % (start_time - offset), "-i", - self.filename, + dash_escape(self.filename), "-vn", "-ss", "%.05f" % offset, ] else: - i_arg = ["-i", self.filename, "-vn"] + i_arg = ["-i", dash_escape(self.filename), "-vn"] cmd = ( [FFMPEG_BINARY] diff --git a/moviepy/tools.py b/moviepy/tools.py index a260e75bf..1f1770728 100644 --- a/moviepy/tools.py +++ b/moviepy/tools.py @@ -168,3 +168,26 @@ def find_extension(codec): "specify a temp_audiofile with the right extension " "in write_videofile." ) + + +def dash_escape(path): + """Makes paths that start with dash '-' intertreptable as command line arguments + + Returns + ------- + + - ./path if path starts with dash '-' + - path otherwise + + Examples + -------- + >>> dash_escape('-filenamethatstartswithdash-.mp4') + './-filenamethatstartswithdash-.mp4' + >>> dash_escape('-path/that/starts/with/dash.mp4') + './-path/that/starts/with/dash.mp4' + >>> dash_escape('file-name-.mp4') + 'file-name-.mp4' + >>> dash_escape('/absolute/path/to/-file.mp4') + '/absolute/path/to/-file.mp4' + """ + return "./" + path if path.startswith("-") else path diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py index b7be97177..dad2509b6 100644 --- a/moviepy/video/io/ffmpeg_reader.py +++ b/moviepy/video/io/ffmpeg_reader.py @@ -7,7 +7,7 @@ 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, dash_escape class FFMPEG_VideoReader: @@ -790,7 +790,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", dash_escape(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..2a92670d1 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 dash_escape, subprocess_call @convert_path_to_string(("inputfile", "outputfile")) @@ -41,7 +41,7 @@ def ffmpeg_extract_subclip( "-ss", "%0.2f" % start_time, "-i", - inputfile, + dash_escape(inputfile), "-t", "%0.2f" % (end_time - start_time), "-map", @@ -51,7 +51,7 @@ def ffmpeg_extract_subclip( "-acodec", "copy", "-copyts", - outputfile, + dash_escape(outputfile), ] subprocess_call(cmd, logger=logger) @@ -89,14 +89,14 @@ def ffmpeg_merge_video_audio( FFMPEG_BINARY, "-y", "-i", - audiofile, + dash_escape(audiofile), "-i", - videofile, + dash_escape(videofile), "-vcodec", video_codec, "-acodec", audio_codec, - outputfile, + dash_escape(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, + dash_escape(inputfile), "-ab", "%dk" % bitrate, "-ar", "%d" % fps, - outputfile, + dash_escape(outputfile), ] subprocess_call(cmd, logger=logger) @@ -154,10 +154,10 @@ def ffmpeg_resize(inputfile, outputfile, size, logger="bar"): cmd = [ FFMPEG_BINARY, "-i", - inputfile, + dash_escape(inputfile), "-vf", "scale=%d:%d" % (size[0], size[1]), - outputfile, + dash_escape(outputfile), ] subprocess_call(cmd, logger=logger) @@ -194,7 +194,14 @@ 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", + dash_escape(inputfile), + "-vf", + "deshake", + dash_escape(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 9319850cb..9d50ac16c 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, dash_escape class FFMPEG_VideoWriter: @@ -119,7 +119,7 @@ def __init__( "-", ] if audiofile is not None: - cmd.extend(["-i", audiofile, "-acodec", "copy"]) + cmd.extend(["-i", dash_escape(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([dash_escape(filename)]) popen_params = cross_platform_popen_params( {"stdout": sp.DEVNULL, "stderr": logfile, "stdin": sp.PIPE} @@ -307,7 +307,7 @@ def ffmpeg_write_image(filename, image, logfile=False, pixel_format=None): pixel_format, "-i", "-", - filename, + dash_escape(filename), ] if logfile: diff --git a/moviepy/video/io/gif_writers.py b/moviepy/video/io/gif_writers.py index f4240139b..8c7ad2515 100644 --- a/moviepy/video/io/gif_writers.py +++ b/moviepy/video/io/gif_writers.py @@ -8,7 +8,7 @@ from moviepy.config import FFMPEG_BINARY, IMAGEMAGICK_BINARY from moviepy.decorators import requires_duration, use_clip_fps_by_default -from moviepy.tools import cross_platform_popen_params, subprocess_call +from moviepy.tools import cross_platform_popen_params, dash_escape, subprocess_call from moviepy.video.fx.loop import loop as loop_fx @@ -153,7 +153,7 @@ def write_gif_with_tempfiles( "-r", str(fps), "-i", - file_root + "_GIFTEMP%04d.png", + dash_escape(file_root) + "_GIFTEMP%04d.png", "-r", str(fps), filename, @@ -328,7 +328,7 @@ def write_gif( (pixel_format), "-r", "%.02f" % fps, - filename, + dash_escape(filename), ], **popen_params, ) diff --git a/tests/test_issues.py b/tests/test_issues.py index 5a3dbaaf9..4d47475c9 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -9,6 +9,7 @@ from moviepy.video.compositing.concatenate import concatenate_videoclips from moviepy.video.compositing.transitions import crossfadein, crossfadeout from moviepy.video.fx.resize import resize +from moviepy.video.io.ffmpeg_reader import ffmpeg_parse_infos from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.video.VideoClip import ColorClip, ImageClip, VideoClip @@ -401,5 +402,14 @@ def test_issue_655(): assert True +def test_issue_2160(): + os.chdir("media") + try: + d = ffmpeg_parse_infos("-filenamethatstartswithdash-.mp4") + assert d["video_found"] + finally: # change back to working directory even if test fails + os.chdir("..") + + if __name__ == "__main__": pytest.main() diff --git a/tests/test_tools.py b/tests/test_tools.py index 552d29c4e..9f877b660 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -329,5 +329,19 @@ def test_decorators_argument_converters_consistency( assert function_data["function_arguments"] +@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"), + ], +) +def test_dash_escape(given, expected): + """Test the dash_escape function outputs correct paths as per the docstring.""" + assert tools.dash_escape(given) == expected + + if __name__ == "__main__": pytest.main()