[Python-modules-commits] [python-m3u8] 01/01: Import python-m3u8_0.3.0.orig.tar.gz

Ondřej Nový onovy at moszumanska.debian.org
Fri Oct 7 20:44:25 UTC 2016


This is an automated email from the git hooks/post-receive script.

onovy pushed a commit to branch upstream-jessie-backports
in repository python-m3u8.

commit 7d0c6d0a99f56be450835463bd44c7fefca9038c
Author: Ondřej Nový <onovy at debian.org>
Date:   Fri Oct 7 22:01:58 2016 +0200

    Import python-m3u8_0.3.0.orig.tar.gz
---
 .gitignore           |   4 +
 README.rst           |  70 +++++++++++++++--
 m3u8/__init__.py     |  16 +++-
 m3u8/mixins.py       |  54 ++++++++++++++
 m3u8/model.py        | 200 +++++++++++++++++++++++++------------------------
 m3u8/parser.py       |  99 +++++++++++++++++++-----
 m3u8/protocol.py     |   2 +
 setup.py             |   5 +-
 tests/playlists.py   | 181 +++++++++++++++++++++++++++++++++++++++++++-
 tests/test_loader.py |  54 ++++++++------
 tests/test_model.py  | 207 ++++++++++++++++++++++++++++++++++++++++-----------
 tests/test_parser.py |  70 ++++++++++++++---
 12 files changed, 758 insertions(+), 204 deletions(-)

diff --git a/.gitignore b/.gitignore
index 432207e..d48b86f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,9 @@
 tests/server.stdout
 dist/
 build/
+bin/
+include/
+lib/
+local/
 .coverage
 .cache
diff --git a/README.rst b/README.rst
index 4977d33..31c75a6 100644
--- a/README.rst
+++ b/README.rst
@@ -34,20 +34,76 @@ directly from a string:
 
     m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ... ')
 
-Encryption key
---------------
 
-The segments may be encrypted, in this case the ``key`` attribute will
-be an object with all the attributes from `#EXT-X-KEY`_:
+Encryption keys
+---------------
+
+The segments may be or not encrypted. The ``keys`` attribute list will
+be an list  with all the different keys as described with `#EXT-X-KEY`_:
+
+Each key has the next properties:
 
 -  ``method``: ex.: "AES-128"
 -  ``uri``: the key uri, ex.: "http://videoserver.com/key.bin"
 -  ``iv``: the initialization vector, if available. Otherwise ``None``.
 
-If no ``#EXT-X-KEY`` is found, the ``key`` attribute will be ``None``.
+If no ``#EXT-X-KEY`` is found, the ``keys`` list will have a unique element ``None``. Multiple keys are supported.
+
+If unencrypted and encrypted segments are mixed in the M3U8 file, then the list will contain a ``None`` element, with one
+or more keys afterwards.
+
+To traverse the list of keys available:
+
+::
+
+    import m3u8
+
+    m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ...')
+    len(m3u8_obj.keys) => returns the number of keys available in the list (normally 1)
+    for key in m3u8_obj.keys:
+       if key:  # First one could be None
+          key.uri
+          key.method
+          key.iv
+
+
+Getting segments encrypted with one key
+---------------------------------------
+
+There are cases where listing segments for a given key is important. It's possible to
+retrieve the list of segments encrypted with one key via ``by_key`` method in the
+``segments`` list.
+
+Example of getting the segments with no encryption:
+
+::
+
+    import m3u8
+
+    m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ...')
+    segmk1 = m3u8_obj.segments.by_key(None)
+
+    # Get the list of segments encrypted using last key
+    segm = m3u8_obj.segments.by_key( m3u8_obj.keys[-1] )
+
+
+With this method, is now possible also to change the key from some of the segments programatically:
+
+
+::
+
+    import m3u8
+
+    m3u8_obj = m3u8.loads('#EXTM3U8 ... etc ...')
+
+    # Create a new Key and replace it
+    new_key = m3u8.Key("AES-128", "/encrypted/newkey.bin", None, iv="0xf123ad23f22e441098aa87ee")
+    for segment in m3u8_obj.segments.by_key( m3u8_obj.keys[-1] ):
+        segm.key = new_key
+    # Remember to sync the key from the list as well
+    m3u8_obj.keys[-1] = new_key
+
 
-Multiple keys are not supported yet (and has a low priority), follow
-`issue 1`_ for updates.
 
 Variant playlists (variable bitrates)
 -------------------------------------
diff --git a/m3u8/__init__.py b/m3u8/__init__.py
index 0f364fb..7393089 100644
--- a/m3u8/__init__.py
+++ b/m3u8/__init__.py
@@ -25,6 +25,7 @@ from m3u8.parser import parse, is_url, ParseError
 __all__ = ('M3U8', 'Playlist', 'IFramePlaylist', 'Media',
            'Segment', 'loads', 'load', 'parse', 'ParseError')
 
+
 def loads(content):
     '''
     Given a string with a m3u8 content, returns a M3U8 object.
@@ -32,7 +33,8 @@ def loads(content):
     '''
     return M3U8(content)
 
-def load(uri, timeout = None):
+
+def load(uri, timeout=None):
     '''
     Retrieves the content from a given URI and returns a M3U8 object.
     Raises ValueError if invalid content or IOError if request fails.
@@ -44,7 +46,9 @@ def load(uri, timeout = None):
         return _load_from_file(uri)
 
 # Support for python3 inspired by https://github.com/szemtiv/m3u8/
-def _load_from_uri(uri, timeout = None):
+
+
+def _load_from_uri(uri, timeout=None):
     resource = urlopen(uri, timeout=timeout)
     base_uri = _parsed_url(_url_for(uri))
     if PYTHON_MAJOR_VERSION < (3,):
@@ -53,24 +57,28 @@ def _load_from_uri(uri, timeout = None):
         content = _read_python3x(resource)
     return M3U8(content, base_uri=base_uri)
 
+
 def _url_for(uri):
     return urlopen(uri).geturl()
 
+
 def _parsed_url(url):
     parsed_url = url_parser.urlparse(url)
     prefix = parsed_url.scheme + '://' + parsed_url.netloc
     base_path = posixpath.normpath(parsed_url.path + '/..')
     return url_parser.urljoin(prefix, base_path)
 
+
 def _read_python2x(resource):
     return resource.read().strip()
 
+
 def _read_python3x(resource):
-    return  resource.read().decode(resource.headers.get_content_charset(failobj="utf-8"))
+    return resource.read().decode(resource.headers.get_content_charset(failobj="utf-8"))
+
 
 def _load_from_file(uri):
     with open(uri) as fileobj:
         raw_content = fileobj.read().strip()
     base_uri = os.path.dirname(uri)
     return M3U8(raw_content, base_uri=base_uri)
-
diff --git a/m3u8/mixins.py b/m3u8/mixins.py
new file mode 100644
index 0000000..e2576f5
--- /dev/null
+++ b/m3u8/mixins.py
@@ -0,0 +1,54 @@
+
+import os
+from m3u8.parser import is_url
+
+try:
+    import urlparse as url_parser
+except ImportError:
+    import urllib.parse as url_parser
+
+
+def _urijoin(base_uri, path):
+    if is_url(base_uri):
+        return url_parser.urljoin(base_uri, path)
+    else:
+        return os.path.normpath(os.path.join(base_uri, path.strip('/')))
+
+
+class BasePathMixin(object):
+
+    @property
+    def absolute_uri(self):
+        if self.uri is None:
+            return None
+        if is_url(self.uri):
+            return self.uri
+        else:
+            if self.base_uri is None:
+                raise ValueError('There can not be `absolute_uri` with no `base_uri` set')
+            return _urijoin(self.base_uri, self.uri)
+
+    @property
+    def base_path(self):
+        return os.path.dirname(self.uri)
+
+    @base_path.setter
+    def base_path(self, newbase_path):
+        if not self.base_path:
+            self.uri = "%s/%s" % (newbase_path, self.uri)
+        self.uri = self.uri.replace(self.base_path, newbase_path)
+
+
+class GroupedBasePathMixin(object):
+
+    def _set_base_uri(self, new_base_uri):
+        for item in self:
+            item.base_uri = new_base_uri
+
+    base_uri = property(None, _set_base_uri)
+
+    def _set_base_path(self, newbase_path):
+        for item in self:
+            item.base_path = newbase_path
+
+    base_path = property(None, _set_base_path)
diff --git a/m3u8/model.py b/m3u8/model.py
index 8f653e9..591faa7 100644
--- a/m3u8/model.py
+++ b/m3u8/model.py
@@ -8,12 +8,8 @@ import os
 import errno
 import math
 
-try:
-    import urlparse as url_parser
-except ImportError:
-    import urllib.parse as url_parser
-
-from m3u8 import parser
+from m3u8.parser import parse, format_date_time
+from m3u8.mixins import BasePathMixin, GroupedBasePathMixin
 
 
 class M3U8(object):
@@ -41,8 +37,22 @@ class M3U8(object):
 
     Attributes:
 
-     `key`
-       it's a `Key` object, the EXT-X-KEY from m3u8. Or None
+     `keys`
+       Returns the list of `Key` objects used to encrypt the segments from m3u8.
+       It covers the whole list of possible situations when encryption either is
+       used or not.
+
+       1. No encryption.
+       `keys` list will only contain a `None` element.
+
+       2. Encryption enabled for all segments.
+       `keys` list will contain the key used for the segments.
+
+       3. No encryption for first element(s), encryption is applied afterwards
+       `keys` list will contain `None` and the key used for the rest of segments.
+
+       4. Multiple keys used during the m3u8 manifest.
+       `keys` list will contain the key used for each set of segments.
 
      `segments`
        a `SegmentList` object, represents the list of `Segment`s from this playlist
@@ -122,11 +132,11 @@ class M3U8(object):
         ('version',          'version'),
         ('allow_cache',      'allow_cache'),
         ('playlist_type',    'playlist_type')
-        )
+    )
 
     def __init__(self, content=None, base_path=None, base_uri=None, strict=False):
         if content is not None:
-            self.data = parser.parse(content, strict)
+            self.data = parse(content, strict)
         else:
             self.data = {}
         self._base_uri = base_uri
@@ -137,35 +147,35 @@ class M3U8(object):
         self._initialize_attributes()
         self.base_path = base_path
 
-    def _initialize_attributes(self):
-        self.key = Key(base_uri=self.base_uri, **self.data['key']) if 'key' in self.data else None
-        self.segments = SegmentList([ Segment(base_uri=self.base_uri, **params)
-                                      for params in self.data.get('segments', []) ])
 
+    def _initialize_attributes(self):
+        self.keys = [ Key(base_uri=self.base_uri, **params) if params else None
+                      for params in self.data.get('keys', []) ]
+        self.segments = SegmentList([ Segment(base_uri=self.base_uri, keyobject=find_key(segment.get('key', {}), self.keys), **segment)
+                                      for segment in self.data.get('segments', []) ])
+        #self.keys = get_uniques([ segment.key for segment in self.segments ])
         for attr, param in self.simple_attributes:
             setattr(self, attr, self.data.get(param))
 
         self.files = []
-        if self.key:
-            self.files.append(self.key.uri)
+        for key in self.keys:
+            # Avoid None key, it could be the first one, don't repeat them
+            if key and key.uri not in self.files:
+                self.files.append(key.uri)
         self.files.extend(self.segments.uri)
 
-        self.media = MediaList([ Media(base_uri=self.base_uri,
-                                       **media)
+        self.media = MediaList([ Media(base_uri=self.base_uri, **media)
                                  for media in self.data.get('media', []) ])
 
-        self.playlists = PlaylistList([ Playlist(base_uri=self.base_uri,
-                                                 media=self.media,
-                                                 **playlist)
+        self.playlists = PlaylistList([ Playlist(base_uri=self.base_uri, media=self.media, **playlist)
                                         for playlist in self.data.get('playlists', []) ])
 
         self.iframe_playlists = PlaylistList()
         for ifr_pl in self.data.get('iframe_playlists', []):
-            self.iframe_playlists.append(
-                IFramePlaylist(base_uri=self.base_uri,
-                               uri=ifr_pl['uri'],
-                               iframe_stream_info=ifr_pl['iframe_stream_info'])
-            )
+            self.iframe_playlists.append(IFramePlaylist(base_uri=self.base_uri,
+                                         uri=ifr_pl['uri'],
+                                         iframe_stream_info=ifr_pl['iframe_stream_info'])
+                                        )
 
     def __unicode__(self):
         return self.dumps()
@@ -180,6 +190,9 @@ class M3U8(object):
         self.media.base_uri = new_base_uri
         self.playlists.base_uri = new_base_uri
         self.segments.base_uri = new_base_uri
+        for key in self.keys:
+            if key:
+                key.base_uri = new_base_uri
 
     @property
     def base_path(self):
@@ -193,11 +206,13 @@ class M3U8(object):
     def _update_base_path(self):
         if self._base_path is None:
             return
-        if self.key:
-            self.key.base_path = self.base_path
-        self.media.base_path = self.base_path
-        self.segments.base_path = self.base_path
-        self.playlists.base_path = self.base_path
+        for key in self.keys:
+            if key:
+                key.base_path = self._base_path
+        self.media.base_path = self._base_path
+        self.segments.base_path = self._base_path
+        self.playlists.base_path = self._base_path
+
 
     def add_playlist(self, playlist):
         self.is_variant = True
@@ -228,15 +243,13 @@ class M3U8(object):
             output.append('#EXT-X-ALLOW-CACHE:' + self.allow_cache.upper())
         if self.version:
             output.append('#EXT-X-VERSION:' + self.version)
-        if self.key:
-            output.append(str(self.key))
         if self.target_duration:
-            output.append('#EXT-X-TARGETDURATION:' + int_or_float_to_string(self.target_duration))
+            output.append('#EXT-X-TARGETDURATION:' +
+                          int_or_float_to_string(self.target_duration))
         if self.program_date_time is not None:
-            output.append('#EXT-X-PROGRAM-DATE-TIME:' + parser.format_date_time(self.program_date_time))
+            output.append('#EXT-X-PROGRAM-DATE-TIME:' + format_date_time(self.program_date_time))
         if not (self.playlist_type is None or self.playlist_type == ''):
-            output.append(
-                '#EXT-X-PLAYLIST-TYPE:%s' % str(self.playlist_type).upper())
+            output.append('#EXT-X-PLAYLIST-TYPE:%s' % str(self.playlist_type).upper())
         if self.is_i_frames_only:
             output.append('#EXT-X-I-FRAMES-ONLY')
         if self.is_variant:
@@ -245,7 +258,6 @@ class M3U8(object):
             output.append(str(self.playlists))
             if self.iframe_playlists:
                 output.append(str(self.iframe_playlists))
-
         output.append(str(self.segments))
 
         if self.is_endlist:
@@ -270,42 +282,6 @@ class M3U8(object):
             if error.errno != errno.EEXIST:
                 raise
 
-class BasePathMixin(object):
-
-    @property
-    def absolute_uri(self):
-        if self.uri is None:
-            return None
-        if parser.is_url(self.uri):
-            return self.uri
-        else:
-            if self.base_uri is None:
-                raise ValueError('There can not be `absolute_uri` with no `base_uri` set')
-            return _urijoin(self.base_uri, self.uri)
-
-    @property
-    def base_path(self):
-        return os.path.dirname(self.uri)
-
-    @base_path.setter
-    def base_path(self, newbase_path):
-        if not self.base_path:
-            self.uri = "%s/%s" % (newbase_path, self.uri)
-        self.uri = self.uri.replace(self.base_path, newbase_path)
-
-class GroupedBasePathMixin(object):
-
-    def _set_base_uri(self, new_base_uri):
-        for item in self:
-            item.base_uri = new_base_uri
-
-    base_uri = property(None, _set_base_uri)
-
-    def _set_base_path(self, newbase_path):
-        for item in self:
-            item.base_path = newbase_path
-
-    base_path = property(None, _set_base_path)
 
 class Segment(BasePathMixin):
     '''
@@ -349,7 +325,7 @@ class Segment(BasePathMixin):
 
     def __init__(self, uri, base_uri, program_date_time=None, duration=None,
                  title=None, byterange=None, cue_out=False, discontinuity=False, key=None,
-                 scte35=None, scte35_duration=None):
+                 scte35=None, scte35_duration=None, keyobject=None):
         self.uri = uri
         self.duration = duration
         self.title = title
@@ -360,19 +336,25 @@ class Segment(BasePathMixin):
         self.cue_out = cue_out
         self.scte35 = scte35
         self.scte35_duration = scte35_duration
-        self.key = Key(base_uri=base_uri,**key) if key else None
-
+        self.key = keyobject
+        # Key(base_uri=base_uri, **key) if key else None
 
     def dumps(self, last_segment):
         output = []
         if last_segment and self.key != last_segment.key:
-          output.append(str(self.key))
-          output.append('\n')
+            output.append(str(self.key))
+            output.append('\n')
+        else:
+            # The key must be checked anyway now for the first segment
+            if self.key and last_segment is None:
+                output.append(str(self.key))
+                output.append('\n')
 
         if self.discontinuity:
             output.append('#EXT-X-DISCONTINUITY\n')
             if self.program_date_time:
-                output.append('#EXT-X-PROGRAM-DATE-TIME:%s\n' % parser.format_date_time(self.program_date_time))
+                output.append('#EXT-X-PROGRAM-DATE-TIME:%s\n' %
+                              format_date_time(self.program_date_time))
         if self.cue_out:
             output.append('#EXT-X-CUE-OUT-CONT\n')
         output.append('#EXTINF:%s,' % int_or_float_to_string(self.duration))
@@ -391,20 +373,27 @@ class Segment(BasePathMixin):
     def __str__(self):
         return self.dumps(None)
 
+
 class SegmentList(list, GroupedBasePathMixin):
 
     def __str__(self):
         output = []
         last_segment = None
         for segment in self:
-          output.append(segment.dumps(last_segment))
-          last_segment = segment
+            output.append(segment.dumps(last_segment))
+            last_segment = segment
         return '\n'.join(output)
 
     @property
     def uri(self):
         return [seg.uri for seg in self]
 
+
+    def by_key(self, key):
+        return [ segment for segment in self if segment.key == key ]
+
+
+
 class Key(BasePathMixin):
     '''
     Key used to encrypt the segments in a m3u8 playlist (EXT-X-KEY)
@@ -422,6 +411,7 @@ class Key(BasePathMixin):
       initialization vector. a string representing a hexadecimal number. ex.: 0X12A
 
     '''
+
     def __init__(self, method, uri, base_uri, iv=None, keyformat=None, keyformatversions=None):
         self.method = method
         self.uri = uri
@@ -433,7 +423,7 @@ class Key(BasePathMixin):
     def __str__(self):
         output = [
             'METHOD=%s' % self.method,
-            ]
+        ]
         if self.uri:
             output.append('URI="%s"' % self.uri)
         if self.iv:
@@ -446,12 +436,14 @@ class Key(BasePathMixin):
         return '#EXT-X-KEY:' + ','.join(output)
 
     def __eq__(self, other):
+        if not other:
+            return False
         return self.method == other.method and \
-               self.uri == other.uri and \
-               self.iv == other.iv and \
-               self.base_uri == other.base_uri and \
-               self.keyformat == other.keyformat and \
-               self.keyformatversions == other.keyformatversions
+            self.uri == other.uri and \
+            self.iv == other.iv and \
+            self.base_uri == other.base_uri and \
+            self.keyformat == other.keyformat and \
+            self.keyformatversions == other.keyformatversions
 
     def __ne__(self, other):
         return not self.__eq__(other)
@@ -471,6 +463,7 @@ class Playlist(BasePathMixin):
 
     More info: http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.10
     '''
+
     def __init__(self, uri, stream_info, media, base_uri):
         self.uri = uri
         self.base_uri = base_uri
@@ -508,7 +501,8 @@ class Playlist(BasePathMixin):
             stream_inf.append('AVERAGE-BANDWIDTH=%d' %
                               self.stream_info.average_bandwidth)
         if self.stream_info.resolution:
-            res = str(self.stream_info.resolution[0]) + 'x' + str(self.stream_info.resolution[1])
+            res = str(self.stream_info.resolution[
+                      0]) + 'x' + str(self.stream_info.resolution[1])
             stream_inf.append('RESOLUTION=' + res)
         if self.stream_info.codecs:
             stream_inf.append('CODECS=' + quoted(self.stream_info.codecs))
@@ -524,6 +518,7 @@ class Playlist(BasePathMixin):
 
         return '#EXT-X-STREAM-INF:' + ','.join(stream_inf) + '\n' + self.uri
 
+
 class IFramePlaylist(BasePathMixin):
     '''
     IFramePlaylist object representing a link to a
@@ -537,6 +532,7 @@ class IFramePlaylist(BasePathMixin):
 
     More info: http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.13
     '''
+
     def __init__(self, base_uri, uri, iframe_stream_info):
         self.uri = uri
         self.base_uri = base_uri
@@ -581,6 +577,7 @@ StreamInfo = namedtuple(
     ['bandwidth', 'average_bandwidth', 'program_id', 'resolution', 'codecs']
 )
 
+
 class Media(BasePathMixin):
     '''
     A media object from a M3U8 playlist
@@ -608,7 +605,7 @@ class Media(BasePathMixin):
     def __init__(self, uri=None, type=None, group_id=None, language=None,
                  name=None, default=None, autoselect=None, forced=None,
                  characteristics=None, assoc_language=None,
-                 instream_id=None,base_uri=None, **extras):
+                 instream_id=None, base_uri=None, **extras):
         self.base_uri = base_uri
         self.uri = uri
         self.type = type
@@ -654,6 +651,7 @@ class Media(BasePathMixin):
     def __str__(self):
         return self.dumps()
 
+
 class MediaList(list, GroupedBasePathMixin):
 
     def __str__(self):
@@ -664,6 +662,7 @@ class MediaList(list, GroupedBasePathMixin):
     def uri(self):
         return [media.uri for media in self]
 
+
 class PlaylistList(list, GroupedBasePathMixin):
 
     def __str__(self):
@@ -671,17 +670,26 @@ class PlaylistList(list, GroupedBasePathMixin):
         return '\n'.join(output)
 
 
+def find_key(keydata, keylist):
+    if not keydata:
+        return None
+    for key in keylist:
+        if key:
+            # Check the intersection of keys and values
+            if keydata.get('uri', None) == key.uri and \
+               keydata.get('method', 'NONE') == key.method and \
+               keydata.get('iv', None) == key.iv:
+                return key
+    raise KeyError("No key found for key data")
+
+
 def denormalize_attribute(attribute):
-    return attribute.replace('_','-').upper()
+    return attribute.replace('_', '-').upper()
+
 
 def quoted(string):
     return '"%s"' % string
 
-def _urijoin(base_uri, path):
-    if parser.is_url(base_uri):
-        return url_parser.urljoin(base_uri, path)
-    else:
-        return os.path.normpath(os.path.join(base_uri, path.strip('/')))
 
 def int_or_float_to_string(number):
     return str(int(number)) if number == math.floor(number) else str(number)
diff --git a/m3u8/parser.py b/m3u8/parser.py
index 9840d1a..21e6207 100644
--- a/m3u8/parser.py
+++ b/m3u8/parser.py
@@ -9,25 +9,24 @@ import itertools
 import re
 from m3u8 import protocol
 
-try:
-    import exceptions
-    ExceptionBaseClass = exceptions.Exception
-except ImportError:
-    ExceptionBaseClass = object
-
 '''
 http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.2
 http://stackoverflow.com/questions/2785755/how-to-split-but-ignore-separators-in-quoted-strings-in-python
 '''
 ATTRIBUTELISTPATTERN = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''')
 
+
 def cast_date_time(value):
     return iso8601.parse_date(value)
 
+
 def format_date_time(value):
     return value.isoformat()
 
-class ParseError(ExceptionBaseClass):
+
+
+class ParseError(Exception):
+
     def __init__(self, lineno, line):
         self.lineno = lineno
         self.line = line
@@ -35,6 +34,8 @@ class ParseError(ExceptionBaseClass):
     def __str__(self):
         return 'Syntax error in manifest on line %d: %s' % (self.lineno, self.line)
 
+
+
 def parse(content, strict=False):
     '''
     Given a M3U8 playlist content returns a dictionary with all data found
@@ -47,15 +48,17 @@ def parse(content, strict=False):
         'is_independent_segments': False,
         'playlist_type': None,
         'playlists': [],
-        'iframe_playlists': [],
         'segments': [],
+        'iframe_playlists': [],
         'media': [],
-        }
+        'keys': [],
+    }
 
     state = {
         'expect_segment': False,
         'expect_playlist': False,
-        }
+        'current_key': None,
+    }
 
     lineno = 0
     for line in string_to_lines(content):
@@ -86,6 +89,15 @@ def parse(content, strict=False):
             state['cue_out'] = True
             state['cue_start'] = True
 
+        elif line.startswith(protocol.ext_x_cue_out_start):
+            _parse_cueout_start(line, state, string_to_lines(content)[lineno - 2])
+            state['cue_out'] = True
+            state['cue_start'] = True
+
+        elif line.startswith(protocol.ext_x_cue_span):
+            state['cue_out'] = True
+            state['cue_start'] = True
+
         elif line.startswith(protocol.ext_x_version):
             _parse_simple_parameter(line, data)
 
@@ -93,11 +105,13 @@ def parse(content, strict=False):
             _parse_simple_parameter(line, data)
 
         elif line.startswith(protocol.ext_x_key):
-            state['current_key'] = _parse_key(line)
-            data['key'] = data.get('key', state['current_key'])
+            key = _parse_key(line)
+            state['current_key'] = key
+            if key not in data['keys']:
+                data['keys'].append(key)
 
         elif line.startswith(protocol.extinf):
-            _parse_extinf(line, data, state)
+            _parse_extinf(line, data, state, lineno, strict)
             state['expect_segment'] = True
 
         elif line.startswith(protocol.ext_x_stream_inf):
@@ -143,6 +157,7 @@ def parse(content, strict=False):
 
     return data
 
+
 def _parse_key(line):
     params = ATTRIBUTELISTPATTERN.split(line.replace(protocol.ext_x_key + ':', ''))[1::2]
     key = {}
@@ -151,13 +166,23 @@ def _parse_key(line):
         key[normalize_attribute(name)] = remove_quotes(value)
     return key
 
-def _parse_extinf(line, data, state):
-    duration, title = line.replace(protocol.extinf + ':', '').split(',')
+
+def _parse_extinf(line, data, state, lineno, strict):
+    chunks = line.replace(protocol.extinf + ':', '').split(',')
+    if len(chunks) == 2:
+        duration, title = chunks
+    elif len(chunks) == 1:
+        if strict:
+            raise ParseError(lineno, line)
+        else:
+            duration = chunks[0]
+            title = ''
     if 'segment' not in state:
         state['segment'] = {}
     state['segment']['duration'] = float(duration)
     state['segment']['title'] = remove_quotes(title)
 
+
 def _parse_ts_chunk(line, data, state):
     segment = state.pop('segment')
     if state.get('current_program_date_time'):
@@ -167,12 +192,17 @@ def _parse_ts_chunk(line, data, state):
     segment['cue_out'] = state.pop('cue_out', False)
     if state.get('current_cue_out_scte35'):
         segment['scte35'] = state['current_cue_out_scte35']
-        segment['scte35_duration'] = state['current_cue_out_duration'] 
+        segment['scte35_duration'] = state['current_cue_out_duration']
     segment['discontinuity'] = state.pop('discontinuity', False)
     if state.get('current_key'):
-      segment['key'] = state['current_key']
+        segment['key'] = state['current_key']
+    else:
+        # For unencrypted segments, the initial key would be None
+        if None not in data['keys']:
+            data['keys'].append(None)
     data['segments'].append(segment)
 
+
 def _parse_attribute_list(prefix, line, atribute_parser):
     params = ATTRIBUTELISTPATTERN.split(line.replace(prefix + ':', ''))[1::2]
 
@@ -188,6 +218,7 @@ def _parse_attribute_list(prefix, line, atribute_parser):
 
     return attributes
 
+
 def _parse_stream_inf(line, data, state):
     data['is_variant'] = True
     data['media_sequence'] = None
@@ -197,6 +228,7 @@ def _parse_stream_inf(line, data, state):
     atribute_parser["average_bandwidth"] = int
     state['stream_info'] = _parse_attribute_list(protocol.ext_x_stream_inf, line, atribute_parser)
 
+
 def _parse_i_frame_stream_inf(line, data):
     atribute_parser = remove_quotes_parser('codecs', 'uri')
     atribute_parser["program_id"] = int
@@ -207,22 +239,26 @@ def _parse_i_frame_stream_inf(line, data):
 
     data['iframe_playlists'].append(iframe_playlist)
 
+
 def _parse_media(line, data, state):
     quoted = remove_quotes_parser('uri', 'group_id', 'language', 'name', 'characteristics')
     media = _parse_attribute_list(protocol.ext_x_media, line, quoted)
     data['media'].append(media)
 
+
 def _parse_variant_playlist(line, data, state):
     playlist = {'uri': line,
                 'stream_info': state.pop('stream_info')}
 
     data['playlists'].append(playlist)
 
+
 def _parse_byterange(line, state):
     if 'segment' not in state:
         state['segment'] = {}
     state['segment']['byterange'] = line.replace(protocol.ext_x_byterange + ':', '')
 
+
 def _parse_simple_parameter_raw_value(line, cast_to=str, normalize=False):
     param, value = line.split(':', 1)
     param = normalize_attribute(param.replace('#EXT-X-', ''))
@@ -230,14 +266,17 @@ def _parse_simple_parameter_raw_value(line, cast_to=str, normalize=False):
         value = normalize_attribute(value)
     return param, cast_to(value)
 
+
 def _parse_and_set_simple_parameter_raw_value(line, data, cast_to=str, normalize=False):
     param, value = _parse_simple_parameter_raw_value(line, cast_to, normalize)
     data[param] = value
     return data[param]
 
+
 def _parse_simple_parameter(line, data, cast_to=str):
     return _parse_and_set_simple_parameter_raw_value(line, data, cast_to, True)
 
+
 def _parse_cueout(line, state):
     param, value = line.split(':', 1)
     res = re.match('.*Duration=(.*),SCTE35=(.*)$', value)
@@ -245,12 +284,36 @@ def _parse_cueout(line, state):
         state['current_cue_out_duration'] = res.group(1)
         state['current_cue_out_scte35'] = res.group(2)
 
+def _cueout_elemental(line, state, prevline):
+    param, value = line.split(':', 1)
+    res = re.match('.*EXT-OATCLS-SCTE35:(.*)$', prevline)
+    if res:
+        return (res.group(1), value)
+    else:
+        return None
+
+def _cueout_envivio(line, state, prevline):
+    param, value = line.split(':', 1)
+    res = re.match('.*DURATION=(.*),.*,CUE="(.*)"', value)
+    if res:
+        return (res.group(2), res.group(1))
+    else:
+        return None
+
+def _parse_cueout_start(line, state, prevline):
+    _cueout_state = _cueout_elemental(line, state, prevline) or _cueout_envivio(line, state, prevline)
+    if _cueout_state:
+        state['current_cue_out_scte35'] = _cueout_state[0]
+        state['current_cue_out_duration'] = _cueout_state[1]
+    
 def string_to_lines(string):
     return string.strip().replace('\r\n', '\n').split('\n')
 
+
 def remove_quotes_parser(*attrs):
     return dict(zip(attrs, itertools.repeat(remove_quotes)))
 
+
 def remove_quotes(string):
     '''
     Remove quotes from string.
@@ -266,8 +329,10 @@ def remove_quotes(string):
         return string[1:-1]
     return string
 
+
 def normalize_attribute(attribute):
     return attribute.replace('-', '_').lower().strip()
 
+
 def is_url(uri):
     return re.match(r'https?://', uri) is not None
diff --git a/m3u8/protocol.py b/m3u8/protocol.py
index bfa5992..2fb3be5 100644
--- a/m3u8/protocol.py
+++ b/m3u8/protocol.py
@@ -18,8 +18,10 @@ ext_i_frames_only = '#EXT-X-I-FRAMES-ONLY'
 ext_x_byterange = '#EXT-X-BYTERANGE'
 ext_x_i_frame_stream_inf = '#EXT-X-I-FRAME-STREAM-INF'
 ext_x_discontinuity = '#EXT-X-DISCONTINUITY'
+ext_x_cue_out_start = '#EXT-X-CUE-OUT'
 ext_x_cue_out = '#EXT-X-CUE-OUT-CONT'
 ext_is_independent_segments = '#EXT-X-INDEPENDENT-SEGMENTS'
 ext_x_scte35 = '#EXT-OATCLS-SCTE35'
 ext_x_cue_start = '#EXT-X-CUE-OUT'
 ext_x_cue_end = '#EXT-X-CUE-IN'
+ext_x_cue_span = '#EXT-X-CUE-SPAN'
diff --git a/setup.py b/setup.py
index 1315e2c..8c727c2 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,8 @@ from setuptools import setup
 
 long_description = None
 if exists("README.rst"):
-    long_description = open("README.rst").read()
+    with  open("README.rst") as file:
+        long_description = file.read()
 
 install_reqs = [req for req in open(abspath(join(dirname(__file__), 'requirements.txt')))]
 
@@ -11,7 +12,7 @@ setup(
     name="m3u8",
     author='Globo.com',
     author_email='videos3 at corp.globo.com',
-    version="0.2.10",
+    version="0.3.0",
     license='MIT',
     zip_safe=False,
     include_package_data=True,
diff --git a/tests/playlists.py b/tests/playlists.py
index 3c9c78d..bad5da0 100755
--- a/tests/playlists.py
+++ b/tests/playlists.py
@@ -15,7 +15,8 @@ http://media.example.com/entire.ts
 #EXT-X-ENDLIST
 '''
 
-SIMPLE_PLAYLIST_FILENAME = abspath(join(dirname(__file__), 'playlists/simple-playlist.m3u8'))
+SIMPLE_PLAYLIST_FILENAME = abspath(
+    join(dirname(__file__), 'playlists/simple-playlist.m3u8'))
 
 SIMPLE_PLAYLIST_URI = TEST_HOST + '/simple.m3u8'
 TIMEOUT_SIMPLE_PLAYLIST_URI = TEST_HOST + '/timeout_simple.m3u8'
@@ -191,6 +192,27 @@ PLAYLIST_WITH_ENCRIPTED_SEGMENTS_AND_IV = '''
 ../../../../hls/streamNum82405.ts
 '''
 
+PLAYLIST_WITH_ENCRIPTED_SEGMENTS_AND_IV_SORTED = '''
+#EXTM3U
+#EXT-X-MEDIA-SEQUENCE:82400
+#EXT-X-ALLOW-CACHE:NO
+#EXT-X-VERSION:2
+#EXT-X-TARGETDURATION:8
+#EXT-X-KEY:METHOD=AES-128,URI="/hls-key/key.bin", IV=0X10ef8f758ca555115584bb5b3c687f52
+#EXTINF:8,
+../../../../hls/streamNum82400.ts
+#EXTINF:8,
+../../../../hls/streamNum82401.ts
+#EXTINF:8,
+../../../../hls/streamNum82402.ts
+#EXTINF:8,
+../../../../hls/streamNum82403.ts
+#EXTINF:8,
+../../../../hls/streamNum82404.ts
+#EXTINF:8,
+../../../../hls/streamNum82405.ts
... 995 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-m3u8.git



More information about the Python-modules-commits mailing list