Repository: Zulko/moviepy

Issue:
"KeyError: 'video_fps'" when trying to convert youtube webm to mp3
I'm downloading audio in mp4/web containers using Pytube, but I'm getting "KeyError: 'video_fps'" when trying to convert them to mp3 with moviepy. 

Code:
`import moviepy.editor as mp`
`from pytube import YouTube`


`#download video`
`youtubeurl = "https://www.youtube.com/watch?v=jYypQueS3ew&index=3&list=PL1nNuwHORUCPajRTocTSCJheaMzbWRu23&t=0s"`
`yt = YouTube(youtubeurl)`
`stream = yt.streams.filter(subtype='webm',only_audio=True).last().download("/home/jaws/Desktop/Stuff/Moosepy/temp/",` `filename="temp_audio")`
`print` `stream`
`print` `"Done!"`

`#convert to mp3`
`clip = mp.VideoFileClip("/home/jaws/Desktop/Stuff/Moosepy/temp/temp_audio.webm").subclip()`
`clip.audio.write_audiofile("audio.mp3")`
`print "Done!"`

Output:

Traceback (most recent call last):
  File "/home/jaws/Desktop/Stuff/Moosepy/test.py", line 13, in <module>
    clip = mp.VideoFileClip("/home/jaws/Desktop/Stuff/Moosepy/temp/temp_audio.webm").subclip()
  File "/home/jaws/.local/lib/python2.7/site-packages/moviepy/video/io/VideoFileClip.py", line 91, in __init__
    fps_source=fps_source)
  File "/home/jaws/.local/lib/python2.7/site-packages/moviepy/video/io/ffmpeg_reader.py", line 34, in __init__
    self.fps = infos['video_fps']
KeyError: 'video_fps'

Am I missing the codec? It converts mp4 and Webm video files to mp3 fine. It's just these weird audio files in webm/mp4 containers that won't convert. I guess since the FPS is 0, that's throwing up the error? How can I get around this? 
`
Moviepy 0.2.3.5
Python 2.7.12

Hints from issue comments:
@adalbertopc: I have the same issue. But im trying to convert mp4(only audio) to mp3 @lyraxvincent: @adhoc92 provide the full path --> `clip.audio.write_audiofile("/home/jaws/Desktop/Stuff/Moosepy/temp/audio.mp3")`@utkarshmttl: @adalbertopc I have the same issue, I am trying to convert an audio-only mp4 to mp3. Were you ever able to solve your problem?@tburrows13: Linking #869@tburrows13: @utkarshmttl it's a bit of a hack, but you could go to `moviepy/video/io/ffmpeg_reader.py", line 34` and remove the line, or put a bit of logic around it to ignore it if "video_fps" is not a key in `infos`. I'll try and get it properly fixed for v2.0.@lyraxvincent: > @adhoc92 provide the full path --> `clip.audio.write_audiofile("/home/jaws/Desktop/Stuff/Moosepy/temp/audio.mp3")`

have you tried this @utkarshmttl @jangrd: I got this problem when trying to do this for my personal mp3 yt downloader. I got the solution.
Instead of using `VideoFileClip` and its `VideoFileClip.audio`, just use `AudioFileClip`.
This worked for me. If you have trouble with this, use the full path instead of the relative path shown here.

    # mp4 to mp3
    clip = AudioFileClip('testfile.mp4')
    clip.write_audiofile('testfile.mp3')
    clip.close()@tburrows13: Thanks for the help, @sh0tzz. Presumably that’s because the video didn’t actually have any video, even though it was a mp4. It’s definitely something that could be fixed in MoviePy (or at least it could raise a better error), but that’s a good workaround for now.

Would you be able to share a file (or instructions on how to create one) that demonstrates the error?@rajusurya39: The error occurs when the video doesn't contain any visual content, ensure you are uploading a video which is not blank.

@2879786424: > The error occurs when the video doesn't contain any visual content, ensure you are uploading a video which is not blank.

some videos doesn`t have visual content but have audio,how can I get the audio with mp3
@tburrows13: Just import it as an AudioFileClip and it should work fine.@2879786424: it`s worked, thank you very much.@OsaAjani: Thank you for your contributions and for reporting issues in this repository. With the release of v2, which introduces significant changes to the codebase and API, we’ve reviewed the backlog of open PRs and issues. Due to the length of the backlog and the likelihood that many of these are either fixed or no longer applicable, we’ve made the decision to close all previous PRs and issues.

If you believe that any of these are still relevant to the current version or if you'd like to reopen a related discussion, please feel free to create a new issue or pull request, referencing the old one.

Thank you for your understanding and continued support!


Solution patch:
diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py
index 31645efc9..b43f97f36 100644
--- a/moviepy/video/io/ffmpeg_reader.py
+++ b/moviepy/video/io/ffmpeg_reader.py
@@ -384,8 +384,9 @@ def _reset_state(self):
         # should be ignored
         self._inside_output = False
 
-        # flag which indicates that a default stream has not been found yet
-        self._default_stream_found = False
+        # map from stream type to default stream
+        # if a default stream is not indicated, pick the first one available
+        self._default_streams = {}
 
         # current input file, stream and chapter, which will be built at runtime
         self._current_input_file = {"streams": []}
@@ -484,20 +485,14 @@ def parse(self):
                     "stream_number": stream_number,
                     "stream_type": stream_type_lower,
                     "language": language,
-                    "default": not self._default_stream_found
-                    or line.endswith("(default)"),
+                    "default": (stream_type_lower not in self._default_streams)
+                    and line.endswith("(default)"),
                 }
-                self._default_stream_found = True
 
                 # for default streams, set their numbers globally, so it's
                 # easy to get without iterating all
                 if self._current_stream["default"]:
-                    self.result[
-                        f"default_{stream_type_lower}_input_number"
-                    ] = input_number
-                    self.result[
-                        f"default_{stream_type_lower}_stream_number"
-                    ] = stream_number
+                    self._default_streams[stream_type_lower] = self._current_stream
 
                 # exit chapter
                 if self._current_chapter:
@@ -516,21 +511,18 @@ def parse(self):
                             input_number
                         ]
 
-                    # add new input file to self.result
+                    # add new input file to result
                     self.result["inputs"].append(self._current_input_file)
                     self._current_input_file = {"input_number": input_number}
 
                 # parse relevant data by stream type
                 try:
-                    global_data, stream_data = self.parse_data_by_stream_type(
-                        stream_type, line
-                    )
+                    stream_data = self.parse_data_by_stream_type(stream_type, line)
                 except NotImplementedError as exc:
                     warnings.warn(
                         f"{str(exc)}\nffmpeg output:\n\n{self.infos}", UserWarning
                     )
                 else:
-                    self.result.update(global_data)
                     self._current_stream.update(stream_data)
             elif line.startswith("    Metadata:"):
                 # enter group "    Metadata:"
@@ -596,7 +588,7 @@ def parse(self):
                     self._last_metadata_field_added = field
                 self._current_chapter["metadata"][field] = value
 
-        # last input file, must be included in self.result
+        # last input file, must be included in the result
         if self._current_input_file:
             self._current_input_file["streams"].append(self._current_stream)
             # include their chapters, if there are any
@@ -609,6 +601,33 @@ def parse(self):
                 ]
             self.result["inputs"].append(self._current_input_file)
 
+        # set any missing default automatically
+        for stream in self._current_input_file["streams"]:
+            if stream["stream_type"] not in self._default_streams:
+                self._default_streams[stream["stream_type"]] = stream
+                stream["default"] = True
+
+        # set some global info based on the defaults
+        for stream_type_lower, stream_data in self._default_streams.items():
+            self.result[f"default_{stream_type_lower}_input_number"] = stream_data[
+                "input_number"
+            ]
+            self.result[f"default_{stream_type_lower}_stream_number"] = stream_data[
+                "stream_number"
+            ]
+
+            if stream_type_lower == "audio":
+                self.result["audio_found"] = True
+                self.result["audio_fps"] = stream_data["fps"]
+                self.result["audio_bitrate"] = stream_data["bitrate"]
+            elif stream_type_lower == "video":
+                self.result["video_found"] = True
+                self.result["video_size"] = stream_data.get("size", None)
+                self.result["video_bitrate"] = stream_data.get("bitrate", None)
+                self.result["video_fps"] = stream_data["fps"]
+                self.result["video_codec_name"] = stream_data.get("codec_name", None)
+                self.result["video_profile"] = stream_data.get("profile", None)
+
         # some video duration utilities
         if self.result["video_found"] and self.check_duration:
             self.result["video_duration"] = self.result["duration"]
@@ -646,7 +665,7 @@ def parse_data_by_stream_type(self, stream_type, line):
             return {
                 "Audio": self.parse_audio_stream_data,
                 "Video": self.parse_video_stream_data,
-                "Data": lambda _line: ({}, {}),
+                "Data": lambda _line: {},
             }[stream_type](line)
         except KeyError:
             raise NotImplementedError(
@@ -656,7 +675,7 @@ def parse_data_by_stream_type(self, stream_type, line):
 
     def parse_audio_stream_data(self, line):
         """Parses data from "Stream ... Audio" line."""
-        global_data, stream_data = ({"audio_found": True}, {})
+        stream_data = {}
         try:
             stream_data["fps"] = int(re.search(r" (\d+) Hz", line).group(1))
         except (AttributeError, ValueError):
@@ -667,14 +686,11 @@ def parse_audio_stream_data(self, line):
         stream_data["bitrate"] = (
             int(match_audio_bitrate.group(1)) if match_audio_bitrate else None
         )
-        if self._current_stream["default"]:
-            global_data["audio_fps"] = stream_data["fps"]
-            global_data["audio_bitrate"] = stream_data["bitrate"]
-        return (global_data, stream_data)
+        return stream_data
 
     def parse_video_stream_data(self, line):
         """Parses data from "Stream ... Video" line."""
-        global_data, stream_data = ({"video_found": True}, {})
+        stream_data = {}
 
         try:
             match_video_size = re.search(r" (\d+)x(\d+)[,\s]", line)
@@ -736,20 +752,7 @@ def parse_video_stream_data(self, line):
             stream_data["codec_name"] = codec_name
             stream_data["profile"] = profile
 
-        if self._current_stream["default"] or "video_codec_name" not in self.result:
-            global_data["video_codec_name"] = stream_data.get("codec_name", None)
-
-        if self._current_stream["default"] or "video_profile" not in self.result:
-            global_data["video_profile"] = stream_data.get("profile", None)
-
-        if self._current_stream["default"] or "video_size" not in self.result:
-            global_data["video_size"] = stream_data.get("size", None)
-        if self._current_stream["default"] or "video_bitrate" not in self.result:
-            global_data["video_bitrate"] = stream_data.get("bitrate", None)
-        if self._current_stream["default"] or "video_fps" not in self.result:
-            global_data["video_fps"] = stream_data["fps"]
-
-        return (global_data, stream_data)
+        return stream_data
 
     def parse_fps(self, line):
         """Parses number of FPS from a line of the ``ffmpeg -i`` command output."""


Test patch:
diff --git a/tests/test_ffmpeg_reader.py b/tests/test_ffmpeg_reader.py
index 5cf89bc73..df7468dce 100644
--- a/tests/test_ffmpeg_reader.py
+++ b/tests/test_ffmpeg_reader.py
@@ -10,6 +10,7 @@
 
 from moviepy.audio.AudioClip import AudioClip
 from moviepy.config import FFMPEG_BINARY
+from moviepy.tools import ffmpeg_escape_filename
 from moviepy.video.compositing.CompositeVideoClip import clips_array
 from moviepy.video.io.ffmpeg_reader import (
     FFMPEG_VideoReader,
@@ -56,6 +57,38 @@ def test_ffmpeg_parse_infos_video_nframes():
     assert d["video_n_frames"] == 5
 
 
+def test_ffmpeg_parse_infos_no_default_stream(util):
+    """WMV files don't have "default" streams marked in ffmpeg output.
+    Make sure that ffmpeg_parse_infos can handle this case.
+    """
+    mp4_filepath = os.path.abspath("media/smpte-2997.mp4")
+    wmv_filepath = os.path.join(
+        util.TMP_DIR, "ffmpeg_parse_infos_no_default_stream-smpte-2997.wmv"
+    )
+
+    cmd = [
+        FFMPEG_BINARY,
+        "-y",
+        "-i",
+        ffmpeg_escape_filename(mp4_filepath),
+        ffmpeg_escape_filename(wmv_filepath),
+    ]
+    with open(os.devnull, "w") as stderr:
+        subprocess.check_call(cmd, stderr=stderr)
+
+    d = ffmpeg_parse_infos(wmv_filepath)
+
+    for key in (
+        "default_video_stream_number",
+        "default_video_input_number",
+        "default_audio_stream_number",
+        "default_audio_input_number",
+        "video_fps",
+        "audio_fps",
+    ):
+        assert key in d
+
+
 @pytest.mark.parametrize(
     ("decode_file", "expected_duration"),
     (