Repository: Zulko/moviepy

Issue:
Support multiline metadata values parsing in 'ffmpeg_parse_infos'
From #207, the output reported by the user can't be parsed correctly by `FFMpegInfosParser`:

```
  Metadata:
    major_brand     : qt  
    minor_version   : 537199360
    compatible_brands: qt  
    creation_time   : 2015-09-14 14:57:32
    xmp             : <?xpacket begin="﻿" id="W5M0MpCehiHzreSzNTczkc9d"?>
                    : <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.0-c060 61.136967, 2010/06/15-10:43:27        ">
                    :  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
                    :   <rdf:Description rdf:about=""
                    :     xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
                    :     xmlns:xmpDM="http://ns.adobe.com/xmp/1.0/DynamicMedia/"
                    :     xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
                    :     xmlns:dc="http://purl.org/dc/elements/1.1/"
                    :    xmpMM:DocumentID="xmp.did:39FA818BE85AE511B9009F953BF804AA"
                    :    xmpMM:InstanceID="xmp.iid:39FA818BE85AE511B9009F953BF804AA"
                    :    xmpDM:videoFrameRate="24.000000"
                    :    xmpDM:videoFieldOrder="Progressive"
                    :    xmpDM:videoPixelAspectRatio="1/1"
                    :    xmpDM:audioSampleRate="44100"
                    :    xmpDM:audioSampleType="16Int"
                    :    xmpDM:audioChannelType="Mono"
                    :    dc:format="QuickTime">
                    :    <xmpDM:startTimecode
                    :     xmpDM:timeValue="00:00:00:00"
                    :     xmpDM:timeFormat="24Timecode"/>
                    :    <xmpDM:altTimecode
                    :     xmpDM:timeValue="00:00:00:00"
                    :     xmpDM:timeFormat="24Timecode"/>
                    :    <xmpDM:videoFrameSize
                    :     stDim:w="768"
                    :     stDim:h="576"
                    :     stDim:unit="pixel"/>
                    :   </rdf:Description>
                    :  </rdf:RDF>
                    : </x:xmpmeta>
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                                                                                                     
                    :                            
                    : <?xpacket end="w"?>
  Duration: 00:02:10.67, start: 0.000000, bitrate: 26287 kb/s
    Stream #0:0(eng): Video: mjpeg (jpeg / 0x6765706A), yuvj422p(pc, bt470bg), 768x576 [SAR 72:72 DAR 4:3], 26213 kb/s, 24 fps, 24 tbr, 24 tbn, 24 tbc (default)
    Metadata:
      creation_time   : 2015-09-14 14:57:32
      handler_name    : Gestionnaire d�alias Apple
      encoder         : Photo - JPEG
      timecode        : 00:00:00:00
    Stream #0:1(eng): Audio: aac (mp4a / 0x6134706D), 44100 Hz, mono, fltp, 64 kb/s (default)
    Metadata:
      creation_time   : 2015-09-14 14:57:32
      handler_name    : Gestionnaire d�alias Apple
      timecode        : 00:00:00:00
    Stream #0:2(eng): Data: none (tmcd / 0x64636D74) (default)
    Metadata:
      creation_time   : 2015-09-14 14:58:24
      handler_name    : Gestionnaire d�alias Apple
      timecode        : 00:00:00:00
At least one output file must be specified
```

XMP fields included currently in the result are:

- `xmp` -> `<?xpacket begin="﻿" id="W5M0MpCehiHzreSzNTczkc9d"?>`
- `<empty>` -> `<?xpacket end="w"?>`


Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/36565202/badge)](https://coveralls.io/builds/36565202)

Coverage increased (+0.07%) to 67.542% when pulling **abc8eac1e9381929fe77f9276a2f7b1c5a0b3a07 on mondeja:1491** into **6e996533c75ca12f5b8debd072baf657f750016e on Zulko:master**.


Patch:
diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py
index 9846160..5413ed0 100644
--- a/moviepy/video/io/ffmpeg_reader.py
+++ b/moviepy/video/io/ffmpeg_reader.py
@@ -335,7 +335,7 @@ class FFmpegInfosParser:
 
         # this state is neeeded if `duration_tag_separator == "time="` because
         # execution of ffmpeg decoding the whole file using `-f null -` appends
-        # to the output a the blocks "Stream mapping:" and "Output:", which
+        # to the output the blocks "Stream mapping:" and "Output:", which
         # should be ignored
         self._inside_output = False
 
@@ -355,6 +355,10 @@ class FFmpegInfosParser:
             "inputs": [],
         }
 
+        # keep the value of latest metadata value parsed so we can build
+        # at next lines a multiline metadata value
+        self._last_metadata_field_added = None
+
     def parse(self):
         """Parses the information returned by FFmpeg in stderr executing their binary
         for a file with ``-i`` option and returns a dictionary with all data needed
@@ -398,7 +402,14 @@ class FFmpegInfosParser:
             elif self._inside_file_metadata:
                 # file metadata line
                 field, value = self.parse_metadata_field_value(line)
-                self.result["metadata"].update({field: value})
+
+                # multiline metadata value parsing
+                if field == "":
+                    field = self._last_metadata_field_added
+                    value = self.result["metadata"][field] + "\n" + value
+                else:
+                    self._last_metadata_field_added = field
+                self.result["metadata"][field] = value
             elif line.startswith("    Stream "):
                 # exit stream "    Metadata:"
                 if self._current_stream:
@@ -481,10 +492,18 @@ class FFmpegInfosParser:
                     self._current_stream["metadata"] = {}
 
                 field, value = self.parse_metadata_field_value(line)
+
                 if self._current_stream["stream_type"] == "video":
                     field, value = self.video_metadata_type_casting(field, value)
                     if field == "rotate":
                         self.result["video_rotation"] = value
+
+                # multiline metadata value parsing
+                if field == "":
+                    field = self._last_metadata_field_added
+                    value = self._current_stream["metadata"][field] + "\n" + value
+                else:
+                    self._last_metadata_field_added = field
                 self._current_stream["metadata"][field] = value
             elif line.startswith("    Chapter"):
                 # Chapter data line
@@ -516,6 +535,13 @@ class FFmpegInfosParser:
                 if "metadata" not in self._current_chapter:
                     self._current_chapter["metadata"] = {}
                 field, value = self.parse_metadata_field_value(line)
+
+                # multiline metadata value parsing
+                if field == "":
+                    field = self._last_metadata_field_added
+                    value = self._current_chapter["metadata"][field] + "\n" + value
+                else:
+                    self._last_metadata_field_added = field
                 self._current_chapter["metadata"][field] = value
 
         # last input file, must be included in self.result


Test patch:
diff --git a/tests/test_ffmpeg_reader.py b/tests/test_ffmpeg_reader.py
index 20d163b..7667c95 100644
--- a/tests/test_ffmpeg_reader.py
+++ b/tests/test_ffmpeg_reader.py
@@ -9,7 +9,11 @@ import pytest
 from moviepy.audio.AudioClip import AudioClip
 from moviepy.config import FFMPEG_BINARY
 from moviepy.utils import close_all_clips
-from moviepy.video.io.ffmpeg_reader import FFMPEG_VideoReader, ffmpeg_parse_infos
+from moviepy.video.io.ffmpeg_reader import (
+    FFMPEG_VideoReader,
+    FFmpegInfosParser,
+    ffmpeg_parse_infos,
+)
 from moviepy.video.VideoClip import BitmapClip
 
 from tests.test_helper import TMP_DIR
@@ -295,6 +299,175 @@ def test_ffmpeg_parse_infos_metadata_with_attached_pic():
     assert len(d["metadata"].keys()) == 7
 
 
+def test_ffmpeg_parse_infos_multiline_metadata():
+    """Check that the parser can parse multiline metadata values."""
+    infos = """Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/home/110_PREV_FINAL.mov':
+  Metadata:
+    major_brand     : foo
+    minor_version   : 537199360
+    compatible_brands: bar
+    creation_time   : 2999-08-12 09:00:01
+    xmw             : <?xpacket begin="﻿" id="W5M0MpCehiHzreSzNTczkc9d"?>
+                    : <second XML line">
+                    :  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/22-rdf-syntax-ns#">
+                    :   <rdf:Description rdf:about=""
+                    :     xmlns:xmpMM="http://nowhere.ext"
+                    :     xmlns:xmpDM="http://nowhere.ext/"
+                    :     xmlns:stDim="http://nowhere.ext/Dimensions#"
+                    :     xmlns:dc="http://nowhere.ext/dc/elements/1.1/"
+                    :    xmpMM:DocumentID="xmw.did:39FA818BE85AE511B9009F953BF804AA"
+                    :    xmwMM:InstanceID="xmw.iid:39FA818BE85AE511B9009F953BF804AA"
+                    :    xmwDM:videoFrameRate="24.000000"
+                    :    xmwDM:videoFieldOrder="Progressive"
+                    :    xmwDM:videoPixelAspectRatio="1/1"
+                    :    xmwDM:audioSampleRate="44100"
+                    :    xmwDM:audioSampleType="16Int"
+                    :    xmwDM:audioChannelType="Mono"
+                    :    dc:format="QuickTimeline">
+                    :    <xmwDM:startTimecode
+                    :     xmwDM:timeValue="00:00:00:00"
+                    :     xmwDM:timeFormat="24Timecode"/>
+                    :    <xmwDM:altTimecode
+                    :     xmwDM:timeValue="00:00:00:00"
+                    :     xmwDM:timeFormat="24Timecode"/>
+                    :    <xmwDM:videoFrameSize
+                    :     stDim:w="768"
+                    :     stDim:h="576"
+                    :     stDim:unit="pixel"/>
+                    :   </rdf:Description>
+                    :  </rdf:RDF>
+                    : </x:xmwmeta>
+                    :
+                    :
+                    : <?xpacket end="w"?>
+  Duration: 00:02:10.67, start: 0.000000, bitrate: 26287 kb/s
+    Stream #0:0(eng): Video: mjpeg 768x576 26213 kb/s, 24 fps, 24 tbr (default)
+    Metadata:
+      creation_time   : 2015-09-14 14:57:32
+      handler_name    : Foo
+                      : Bar
+      encoder         : Photo - JPEG
+      timecode        : 00:00:00:00
+    Stream #0:1(eng): Audio: aac (mp4a / 0x6), 44100 Hz, mono, fltp, 64 kb/s (default)
+    Metadata:
+      creation_time   : 2015-09-14 14:57:33
+      handler_name    : Bar
+                      : Foo
+      timecode        : 00:00:00:00
+    Stream #0:2(eng): Data: none (tmcd / 0x64636D74) (default)
+    Metadata:
+      creation_time   : 2015-09-14 14:58:24
+      handler_name    : Baz
+                      : Foo
+      timecode        : 00:00:00:00
+At least one output file must be specified
+"""
+
+    d = FFmpegInfosParser(infos, "foo.mkv").parse()
+
+    # container data
+    assert d["audio_bitrate"] == 64
+    assert d["audio_found"] is True
+    assert d["audio_fps"] == 44100
+    assert d["duration"] == 130.67
+    assert d["video_duration"] == 130.67
+    assert d["video_found"] is True
+    assert d["video_fps"] == 24
+    assert d["video_n_frames"] == 3136
+    assert d["video_size"] == [768, 576]
+    assert d["start"] == 0
+    assert d["default_audio_input_number"] == 0
+    assert d["default_audio_stream_number"] == 1
+    assert d["default_data_input_number"] == 0
+    assert d["default_data_stream_number"] == 2
+    assert d["default_video_input_number"] == 0
+    assert d["default_video_stream_number"] == 0
+
+    # container metadata
+    assert d["metadata"]["compatible_brands"] == "bar"
+    assert d["metadata"]["creation_time"] == "2999-08-12 09:00:01"
+    assert d["metadata"]["major_brand"] == "foo"
+    assert d["metadata"]["minor_version"] == "537199360"
+    assert d["metadata"]["xmw"] == (
+        """<?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?>
+<second XML line">
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/22-rdf-syntax-ns#">
+<rdf:Description rdf:about=""
+xmlns:xmpMM="http://nowhere.ext"
+xmlns:xmpDM="http://nowhere.ext/"
+xmlns:stDim="http://nowhere.ext/Dimensions#"
+xmlns:dc="http://nowhere.ext/dc/elements/1.1/"
+xmpMM:DocumentID="xmw.did:39FA818BE85AE511B9009F953BF804AA"
+xmwMM:InstanceID="xmw.iid:39FA818BE85AE511B9009F953BF804AA"
+xmwDM:videoFrameRate="24.000000"
+xmwDM:videoFieldOrder="Progressive"
+xmwDM:videoPixelAspectRatio="1/1"
+xmwDM:audioSampleRate="44100"
+xmwDM:audioSampleType="16Int"
+xmwDM:audioChannelType="Mono"
+dc:format="QuickTimeline">
+<xmwDM:startTimecode
+xmwDM:timeValue="00:00:00:00"
+xmwDM:timeFormat="24Timecode"/>
+<xmwDM:altTimecode
+xmwDM:timeValue="00:00:00:00"
+xmwDM:timeFormat="24Timecode"/>
+<xmwDM:videoFrameSize
+stDim:w="768"
+stDim:h="576"
+stDim:unit="pixel"/>
+</rdf:Description>
+</rdf:RDF>
+</x:xmwmeta>
+
+
+<?xpacket end="w"?>"""
+    )
+
+    # streams
+    assert len(d["inputs"]) == 1
+
+    streams = d["inputs"][0]["streams"]
+    assert len(streams) == 3
+
+    # video stream
+    assert streams[0]["default"] is True
+    assert streams[0]["fps"] == 24
+    assert streams[0]["input_number"] == 0
+    assert streams[0]["language"] == "eng"
+    assert streams[0]["stream_number"] == 0
+    assert streams[0]["stream_type"] == "video"
+    assert streams[0]["size"] == [768, 576]
+
+    assert streams[0]["metadata"]["creation_time"] == "2015-09-14 14:57:32"
+    assert streams[0]["metadata"]["encoder"] == "Photo - JPEG"
+    assert streams[0]["metadata"]["handler_name"] == "Foo\nBar"
+    assert streams[0]["metadata"]["timecode"] == "00:00:00:00"
+
+    # audio stream
+    assert streams[1]["default"] is True
+    assert streams[1]["fps"] == 44100
+    assert streams[1]["input_number"] == 0
+    assert streams[1]["language"] == "eng"
+    assert streams[1]["stream_number"] == 1
+    assert streams[1]["stream_type"] == "audio"
+
+    assert streams[1]["metadata"]["creation_time"] == "2015-09-14 14:57:33"
+    assert streams[1]["metadata"]["timecode"] == "00:00:00:00"
+    assert streams[1]["metadata"]["handler_name"] == "Bar\nFoo"
+
+    # data stream
+    assert streams[2]["default"] is True
+    assert streams[2]["input_number"] == 0
+    assert streams[2]["language"] == "eng"
+    assert streams[2]["stream_number"] == 2
+    assert streams[2]["stream_type"] == "data"
+
+    assert streams[2]["metadata"]["creation_time"] == "2015-09-14 14:58:24"
+    assert streams[2]["metadata"]["timecode"] == "00:00:00:00"
+    assert streams[2]["metadata"]["handler_name"] == "Baz\nFoo"
+
+
 def test_sequential_frame_pos():
     """test_video.mp4 contains 5 frames at 1 fps.
     Each frame is 1x1 pixels and the sequence is Red, Green, Blue, Black, White.