[Python-modules-commits] [python-m3u8] 01/05: import python-m3u8_0.2.8.orig.tar.gz

Mattia Rizzolo mattia at debian.org
Sun Feb 28 20:04:50 UTC 2016


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

mattia pushed a commit to branch master
in repository python-m3u8.

commit 23ca89f389d9b2c3358d7082a03fb27a6eb0eaa5
Author: Ondřej Nový <novy at ondrej.org>
Date:   Sun Feb 28 20:48:31 2016 +0100

    import python-m3u8_0.2.8.orig.tar.gz
---
 .coveralls.yml                         |   1 +
 .gitignore                             |   6 +
 .python-version                        |   1 +
 .travis.yml                            |   8 +
 LICENSE                                |  11 +
 MANIFEST.in                            |   1 +
 README.rst                             | 136 +++++++
 m3u8/__init__.py                       |  75 ++++
 m3u8/model.py                          | 674 +++++++++++++++++++++++++++++++++
 m3u8/parser.py                         | 261 +++++++++++++
 m3u8/protocol.py                       |  22 ++
 requirements-dev.txt                   |   6 +
 requirements.txt                       |   1 +
 runtests                               |  36 ++
 setup.py                               |  22 ++
 tests/m3u8server.py                    |  34 ++
 tests/playlists.py                     | 337 +++++++++++++++++
 tests/playlists/relative-playlist.m3u8 |  14 +
 tests/playlists/simple-playlist.m3u8   |   5 +
 tests/test_loader.py                   | 103 +++++
 tests/test_model.py                    | 573 ++++++++++++++++++++++++++++
 tests/test_parser.py                   | 174 +++++++++
 tests/test_strict_validations.py       |  42 ++
 tests/test_variant_m3u8.py             | 127 +++++++
 24 files changed, 2670 insertions(+)

diff --git a/.coveralls.yml b/.coveralls.yml
new file mode 100644
index 0000000..f62e437
--- /dev/null
+++ b/.coveralls.yml
@@ -0,0 +1 @@
+repo_token: YiOOP7xuzmxWqSWpUQh9xJlTFFD0DTV2g
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2ad5a2b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+*.pyc
+*.egg-info
+tests/server.stdout
+dist/
+build/
+.coverage
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..acd43d0
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+2.7.8 at m3u8
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..80ef662
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+language: python
+sudo: false
+python:
+  - "2.6"
+  - "2.7"
+  - "3.3"
+script: ./runtests
+after_success: coveralls
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b8fc57d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,11 @@
+m3u8 is licensed under the MIT License:
+
+The MIT License
+
+Copyright (c) 2012 globo.com webmedia at corp.globo.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..f9bd145
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include requirements.txt
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..4977d33
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,136 @@
+.. image:: https://travis-ci.org/globocom/m3u8.svg
+    :target: https://travis-ci.org/globocom/m3u8
+
+.. image:: https://coveralls.io/repos/globocom/m3u8/badge.png?branch=master
+    :target: https://coveralls.io/r/globocom/m3u8?branch=master
+
+.. image:: https://gemnasium.com/leandromoreira/m3u8.svg
+    :target: https://gemnasium.com/leandromoreira/m3u8
+
+.. image:: https://badge.fury.io/py/m3u8.svg
+    :target: https://badge.fury.io/py/m3u8
+
+
+m3u8
+====
+
+Python `m3u8`_ parser.
+
+Documentation
+=============
+
+The basic usage is to create a playlist object from uri, file path or
+directly from a string:
+
+::
+
+    import m3u8
+
+    m3u8_obj = m3u8.load('http://videoserver.com/playlist.m3u8')  # this could also be an absolute filename
+    print m3u8_obj.segments
+    print m3u8_obj.target_duration
+
+    # if you already have the content as string, use
+
+    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`_:
+
+-  ``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``.
+
+Multiple keys are not supported yet (and has a low priority), follow
+`issue 1`_ for updates.
+
+Variant playlists (variable bitrates)
+-------------------------------------
+
+A playlist can have a list to other playlist files, this is used to
+represent multiple bitrates videos, and it's called `variant streams`_.
+See an `example here`_.
+
+::
+
+    variant_m3u8 = m3u8.loads('#EXTM3U8 ... contains a variant stream ...')
+    variant_m3u8.is_variant    # in this case will be True
+
+    for playlist in variant_m3u8.playlists:
+        playlist.uri
+        playlist.stream_info.bandwidth
+
+the playlist object used in the for loop above has a few attributes:
+
+-  ``uri``: the url to the stream
+-  ``stream_info``: a ``StreamInfo`` object (actually a namedtuple) with
+   all the attributes available to `#EXT-X-STREAM-INF`_
+-  ``media``: a list of related ``Media`` objects with all the attributes
+   available to `#EXT-X-MEDIA`_
+-  ``playlist_type``: the type of the playlist, which can be one of `VOD`_
+   (video on demand) or `EVENT`_
+
+**NOTE: the following attributes are not implemented yet**, follow
+`issue 4`_ for updates
+
+-  ``alternative_audios``: its an empty list, unless it's a playlist
+   with `Alternative audio`_, in this case it's a list with ``Media``
+   objects with all the attributes available to `#EXT-X-MEDIA`_
+-  ``alternative_videos``: same as ``alternative_audios``
+
+A variant playlist can also have links to `I-frame playlists`_, which are used
+to specify where the I-frames are in a video. See `Apple's documentation`_ on
+this for more information. These I-frame playlists can be accessed in a similar
+way to regular playlists.
+
+::
+
+    variant_m3u8 = m3u8.loads('#EXTM3U ... contains a variant stream ...')
+
+    for iframe_playlist in variant_m3u8.iframe_playlists:
+        iframe_playlist.uri
+        iframe_playlist.iframe_stream_info.bandwidth
+
+The iframe_playlist object used in the for loop above has a few attributes:
+
+-  ``uri``: the url to the I-frame playlist
+-  ``base_uri``: the base uri of the variant playlist (if given)
+-  ``iframe_stream_info``: a ``StreamInfo`` object (same as a regular playlist)
+
+Running Tests
+=============
+
+::
+
+    $ ./runtests
+
+Contributing
+============
+
+All contribution is welcome, but we will merge a pull request if, and only if, it
+
+-  has tests
+-  follows the code conventions
+
+If you plan to implement a new feature or something that will take more
+than a few minutes, please open an issue to make sure we don't work on
+the same thing.
+
+.. _m3u8: http://tools.ietf.org/html/draft-pantos-http-live-streaming-09
+.. _#EXT-X-KEY: http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.4
+.. _issue 1: https://github.com/globocom/m3u8/issues/1
+.. _variant streams: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-6.2.4
+.. _example here: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-8.5
+.. _#EXT-X-STREAM-INF: https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-4.3.4.2
+.. _issue 4: https://github.com/globocom/m3u8/issues/4
+.. _I-frame playlists: https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-4.3.4.3
+.. _Apple's documentation: https://developer.apple.com/library/ios/technotes/tn2288/_index.html#//apple_ref/doc/uid/DTS40012238-CH1-I_FRAME_PLAYLIST
+.. _Alternative audio: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-8.7
+.. _#EXT-X-MEDIA: https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-4.3.4.1
+.. _VOD: https://developer.apple.com/library/mac/technotes/tn2288/_index.html#//apple_ref/doc/uid/DTS40012238-CH1-TNTAG2
+.. _EVENT: https://developer.apple.com/library/mac/technotes/tn2288/_index.html#//apple_ref/doc/uid/DTS40012238-CH1-EVENT_PLAYLIST
diff --git a/m3u8/__init__.py b/m3u8/__init__.py
new file mode 100644
index 0000000..80a5da0
--- /dev/null
+++ b/m3u8/__init__.py
@@ -0,0 +1,75 @@
+# coding: utf-8
+# Copyright 2014 Globo.com Player authors. All rights reserved.
+# Use of this source code is governed by a MIT License
+# license that can be found in the LICENSE file.
+
+import sys
+PYTHON_MAJOR_VERSION = sys.version_info
+
+import os
+import posixpath
+
+try:
+    import urlparse as url_parser
+    import urllib2
+    urlopen = urllib2.urlopen
+except ImportError:
+    import urllib.parse as url_parser
+    from urllib.request import urlopen as url_opener
+    urlopen = url_opener
+
+
+from m3u8.model import M3U8, Playlist, IFramePlaylist, Media, Segment
+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.
+    Raises ValueError if invalid content
+    '''
+    return M3U8(content)
+
+def load(uri):
+    '''
+    Retrieves the content from a given URI and returns a M3U8 object.
+    Raises ValueError if invalid content or IOError if request fails.
+    '''
+    if is_url(uri):
+        return _load_from_uri(uri)
+    else:
+        return _load_from_file(uri)
+
+# Support for python3 inspired by https://github.com/szemtiv/m3u8/
+def _load_from_uri(uri):
+    resource = urlopen(uri)
+    base_uri = _parsed_url(_url_for(uri))
+    if PYTHON_MAJOR_VERSION < (3,):
+        content = _read_python2x(resource)
+    else:
+        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"))
+
+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/model.py b/m3u8/model.py
new file mode 100644
index 0000000..4ca8c66
--- /dev/null
+++ b/m3u8/model.py
@@ -0,0 +1,674 @@
+# coding: utf-8
+# Copyright 2014 Globo.com Player authors. All rights reserved.
+# Use of this source code is governed by a MIT License
+# license that can be found in the LICENSE file.
+
+from collections import namedtuple
+import os
+import errno
+import math
+
+try:
+    import urlparse as url_parser
+except ImportError:
+    import urllib.parse as url_parser
+
+from m3u8 import parser
+
+
+class M3U8(object):
+    '''
+    Represents a single M3U8 playlist. Should be instantiated with
+    the content as string.
+
+    Parameters:
+
+     `content`
+       the m3u8 content as string
+
+     `base_path`
+       all urls (key and segments url) will be updated with this base_path,
+       ex.:
+           base_path = "http://videoserver.com/hls"
+
+            /foo/bar/key.bin           -->  http://videoserver.com/hls/key.bin
+            http://vid.com/segment1.ts -->  http://videoserver.com/hls/segment1.ts
+
+       can be passed as parameter or setted as an attribute to ``M3U8`` object.
+     `base_uri`
+      uri the playlist comes from. it is propagated to SegmentList and Key
+      ex.: http://example.com/path/to
+
+    Attributes:
+
+     `key`
+       it's a `Key` object, the EXT-X-KEY from m3u8. Or None
+
+     `segments`
+       a `SegmentList` object, represents the list of `Segment`s from this playlist
+
+     `is_variant`
+        Returns true if this M3U8 is a variant playlist, with links to
+        other M3U8s with different bitrates.
+
+        If true, `playlists` is a list of the playlists available,
+        and `iframe_playlists` is a list of the i-frame playlists available.
+
+     `is_endlist`
+        Returns true if EXT-X-ENDLIST tag present in M3U8.
+        http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.8
+
+      `playlists`
+        If this is a variant playlist (`is_variant` is True), returns a list of
+        Playlist objects
+
+      `iframe_playlists`
+        If this is a variant playlist (`is_variant` is True), returns a list of
+        IFramePlaylist objects
+
+      `playlist_type`
+        A lower-case string representing the type of the playlist, which can be
+        one of VOD (video on demand) or EVENT.
+
+      `media`
+        If this is a variant playlist (`is_variant` is True), returns a list of
+        Media objects
+
+      `target_duration`
+        Returns the EXT-X-TARGETDURATION as an integer
+        http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.2
+
+      `media_sequence`
+        Returns the EXT-X-MEDIA-SEQUENCE as an integer
+        http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.3
+
+      `program_date_time`
+        Returns the EXT-X-PROGRAM-DATE-TIME as a string
+        http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.5
+
+      `version`
+        Return the EXT-X-VERSION as is
+
+      `allow_cache`
+        Return the EXT-X-ALLOW-CACHE as is
+
+      `files`
+        Returns an iterable with all files from playlist, in order. This includes
+        segments and key uri, if present.
+
+      `base_uri`
+        It is a property (getter and setter) used by
+        SegmentList and Key to have absolute URIs.
+
+      `is_i_frames_only`
+        Returns true if EXT-X-I-FRAMES-ONLY tag present in M3U8.
+        http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.12
+
+      `is_independent_segments`
+        Returns true if EXT-X-INDEPENDENT-SEGMENTS tag present in M3U8.
+        https://tools.ietf.org/html/draft-pantos-http-live-streaming-13#section-3.4.16
+
+    '''
+
+    simple_attributes = (
+        # obj attribute      # parser attribute
+        ('is_variant',       'is_variant'),
+        ('is_endlist',       'is_endlist'),
+        ('is_i_frames_only', 'is_i_frames_only'),
+        ('target_duration',  'targetduration'),
+        ('media_sequence',   'media_sequence'),
+        ('program_date_time',   'program_date_time'),
+        ('is_independent_segments', 'is_independent_segments'),
+        ('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)
+        else:
+            self.data = {}
+        self._base_uri = base_uri
+        if self._base_uri:
+            if not self._base_uri.endswith('/'):
+                self._base_uri += '/'
+
+        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', []) ])
+
+        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)
+        self.files.extend(self.segments.uri)
+
+        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)
+                                        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'])
+            )
+
+    def __unicode__(self):
+        return self.dumps()
+
+    @property
+    def base_uri(self):
+        return self._base_uri
+
+    @base_uri.setter
+    def base_uri(self, new_base_uri):
+        self._base_uri = new_base_uri
+        self.media.base_uri = new_base_uri
+        self.playlists.base_uri = new_base_uri
+        self.segments.base_uri = new_base_uri
+
+    @property
+    def base_path(self):
+        return self._base_path
+
+    @base_path.setter
+    def base_path(self, newbase_path):
+        self._base_path = newbase_path
+        self._update_base_path()
+
+    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
+
+    def add_playlist(self, playlist):
+        self.is_variant = True
+        self.playlists.append(playlist)
+
+    def add_iframe_playlist(self, iframe_playlist):
+        if iframe_playlist is not None:
+            self.is_variant = True
+            self.iframe_playlists.append(iframe_playlist)
+
+    def add_media(self, media):
+        self.media.append(media)
+
+    def add_segment(self, segment):
+        self.segments.append(segment)
+
+    def dumps(self):
+        '''
+        Returns the current m3u8 as a string.
+        You could also use unicode(<this obj>) or str(<this obj>)
+        '''
+        output = ['#EXTM3U']
+        if self.is_independent_segments:
+            output.append('#EXT-X-INDEPENDENT-SEGMENTS')
+        if self.media_sequence:
+            output.append('#EXT-X-MEDIA-SEQUENCE:' + str(self.media_sequence))
+        if self.allow_cache:
+            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))
+        if self.program_date_time is not None:
+            output.append('#EXT-X-PROGRAM-DATE-TIME:' + parser.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())
+        if self.is_i_frames_only:
+            output.append('#EXT-X-I-FRAMES-ONLY')
+        if self.is_variant:
+            if self.media:
+                output.append(str(self.media))
+            output.append(str(self.playlists))
+            if self.iframe_playlists:
+                output.append(str(self.iframe_playlists))
+
+        output.append(str(self.segments))
+
+        if self.is_endlist:
+            output.append('#EXT-X-ENDLIST')
+
+        return '\n'.join(output)
+
+    def dump(self, filename):
+        '''
+        Saves the current m3u8 to ``filename``
+        '''
+        self._create_sub_directories(filename)
+
+        with open(filename, 'w') as fileobj:
+            fileobj.write(self.dumps())
+
+    def _create_sub_directories(self, filename):
+        basename = os.path.dirname(filename)
+        try:
+            os.makedirs(basename)
+        except OSError as error:
+            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):
+    '''
+    A video segment from a M3U8 playlist
+
+    `uri`
+      a string with the segment uri
+
+    `title`
+      title attribute from EXTINF parameter
+
+    `program_date_time`
+      Returns the EXT-X-PROGRAM-DATE-TIME as a datetime
+      http://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.5
+
+    `discontinuity`
+      Returns a boolean indicating if a EXT-X-DISCONTINUITY tag exists
+      http://tools.ietf.org/html/draft-pantos-http-live-streaming-13#section-3.4.11
+
+    `cue_out`
+      Returns a boolean indicating if a EXT-X-CUE-OUT-CONT tag exists
+
+    `duration`
+      duration attribute from EXTINF parameter
+
+    `base_uri`
+      uri the key comes from in URI hierarchy. ex.: http://example.com/path/to
+
+    `byterange`
+      byterange attribute from EXT-X-BYTERANGE parameter
+
+    `key`
+      Key used to encrypt the segment (EXT-X-KEY)
+    '''
+
+    def __init__(self, uri, base_uri, program_date_time=None, duration=None,
+                 title=None, byterange=None, cue_out=False, discontinuity=False, key=None):
+        self.uri = uri
+        self.duration = duration
+        self.title = title
+        self.base_uri = base_uri
+        self.byterange = byterange
+        self.program_date_time = program_date_time
+        self.discontinuity = discontinuity
+        self.cue_out = cue_out
+        self.key = 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')
+
+        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))
+        if self.cue_out:
+            output.append('#EXT-X-CUE-OUT-CONT\n')
+        output.append('#EXTINF:%s,' % int_or_float_to_string(self.duration))
+        if self.title:
+            output.append(quoted(self.title))
+
+        output.append('\n')
+
+        if self.byterange:
+            output.append('#EXT-X-BYTERANGE:%s\n' % self.byterange)
+
+        output.append(self.uri)
+
+        return ''.join(output)
+
+    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
+        return '\n'.join(output)
+
+    @property
+    def uri(self):
+        return [seg.uri for seg in self]
+
+class Key(BasePathMixin):
+    '''
+    Key used to encrypt the segments in a m3u8 playlist (EXT-X-KEY)
+
+    `method`
+      is a string. ex.: "AES-128"
+
+    `uri`
+      is a string. ex:: "https://priv.example.com/key.php?r=52"
+
+    `base_uri`
+      uri the key comes from in URI hierarchy. ex.: http://example.com/path/to
+
+    `iv`
+      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
+        self.iv = iv
+        self.keyformat = keyformat
+        self.keyformatversions = keyformatversions
+        self.base_uri = base_uri
+
+    def __str__(self):
+        output = [
+            'METHOD=%s' % self.method,
+            ]
+        if self.uri:
+            output.append('URI="%s"' % self.uri)
+        if self.iv:
+            output.append('IV=%s' % self.iv)
+        if self.keyformat:
+            output.append('KEYFORMAT="%s"' % self.keyformat)
+        if self.keyformatversions:
+            output.append('KEYFORMATVERSIONS="%s"' % self.keyformatversions)
+
+        return '#EXT-X-KEY:' + ','.join(output)
+
+    def __eq__(self, other):
+        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
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+class Playlist(BasePathMixin):
+    '''
+    Playlist object representing a link to a variant M3U8 with a specific bitrate.
+
+    Attributes:
+
+    `stream_info` is a named tuple containing the attributes: `program_id`,
+    `bandwidth`, `average_bandwidth`, `resolution`, `codecs` and `resolution`
+    which is a a tuple (w, h) of integers
+
+    `media` is a list of related Media entries.
+
+    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
+
+        resolution = stream_info.get('resolution')
+        if resolution != None:
+            resolution = resolution.strip('"')
+            values = resolution.split('x')
+            resolution_pair = (int(values[0]), int(values[1]))
+        else:
+            resolution_pair = None
+
+        self.stream_info = StreamInfo(
+            bandwidth=stream_info['bandwidth'],
+            average_bandwidth=stream_info.get('average_bandwidth'),
+            program_id=stream_info.get('program_id'),
+            resolution=resolution_pair,
+            codecs=stream_info.get('codecs')
+        )
+        self.media = []
+        for media_type in ('audio', 'video', 'subtitles'):
+            group_id = stream_info.get(media_type)
+            if not group_id:
+                continue
+
+            self.media += filter(lambda m: m.group_id == group_id, media)
+
+    def __str__(self):
+        stream_inf = []
+        if self.stream_info.program_id:
+            stream_inf.append('PROGRAM-ID=%d' % self.stream_info.program_id)
+        if self.stream_info.bandwidth:
+            stream_inf.append('BANDWIDTH=%d' % self.stream_info.bandwidth)
+        if self.stream_info.average_bandwidth:
+            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])
+            stream_inf.append('RESOLUTION=' + res)
+        if self.stream_info.codecs:
+            stream_inf.append('CODECS=' + quoted(self.stream_info.codecs))
+
+        for media in self.media:
+            media_type = media.type.upper()
+            stream_inf.append('%s="%s"' % (media_type, media.group_id))
+
+        return '#EXT-X-STREAM-INF:' + ','.join(stream_inf) + '\n' + self.uri
+
+class IFramePlaylist(BasePathMixin):
+    '''
+    IFramePlaylist object representing a link to a
+    variant M3U8 i-frame playlist with a specific bitrate.
+
+    Attributes:
+
+    `iframe_stream_info` is a named tuple containing the attributes:
+     `program_id`, `bandwidth`, `codecs` and `resolution` which
+     is a tuple (w, h) of integers
+
+    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
+
+        resolution = iframe_stream_info.get('resolution')
+        if resolution is not None:
+            values = resolution.split('x')
+            resolution_pair = (int(values[0]), int(values[1]))
+        else:
+            resolution_pair = None
+
+        self.iframe_stream_info = StreamInfo(
+            bandwidth=iframe_stream_info.get('bandwidth'),
+            average_bandwidth=None,
+            program_id=iframe_stream_info.get('program_id'),
+            resolution=resolution_pair,
+            codecs=iframe_stream_info.get('codecs')
+        )
+
+    def __str__(self):
+        iframe_stream_inf = []
+        if self.iframe_stream_info.program_id:
+            iframe_stream_inf.append('PROGRAM-ID=%d' %
+                                     self.iframe_stream_info.program_id)
+        if self.iframe_stream_info.bandwidth:
+            iframe_stream_inf.append('BANDWIDTH=%d' %
+                                     self.iframe_stream_info.bandwidth)
+        if self.iframe_stream_info.resolution:
+            res = (str(self.iframe_stream_info.resolution[0]) + 'x' +
+                   str(self.iframe_stream_info.resolution[1]))
+            iframe_stream_inf.append('RESOLUTION=' + res)
+        if self.iframe_stream_info.codecs:
+            iframe_stream_inf.append('CODECS=' +
+                                     quoted(self.iframe_stream_info.codecs))
+        if self.uri:
+            iframe_stream_inf.append('URI=' + quoted(self.uri))
+
+        return '#EXT-X-I-FRAME-STREAM-INF:' + ','.join(iframe_stream_inf)
+
+StreamInfo = namedtuple(
+    'StreamInfo',
+    ['bandwidth', 'average_bandwidth', 'program_id', 'resolution', 'codecs']
+)
+
+class Media(BasePathMixin):
+    '''
+    A media object from a M3U8 playlist
+    https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-4.3.4.1
+
+    `uri`
+      a string with the media uri
+
+    `type`
+    `group_id`
+    `language`
+    `assoc-language`
+    `name`
+    `default`
+    `autoselect`
+    `forced`
+    `instream_id`
+    `characteristics`
+      attributes in the EXT-MEDIA tag
+
+    `base_uri`
+      uri the media comes from in URI hierarchy. ex.: http://example.com/path/to
+    '''
+
+    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):
+        self.base_uri = base_uri
+        self.uri = uri
+        self.type = type
+        self.group_id = group_id
+        self.language = language
+        self.name = name
+        self.default = default
+        self.autoselect = autoselect
+        self.forced = forced
+        self.assoc_language = assoc_language
+        self.instream_id = instream_id
+        self.characteristics = characteristics
+        self.extras = extras
+
+    def dumps(self):
+        media_out = []
+
+        if self.uri:
+            media_out.append('URI=' + quoted(self.uri))
+        if self.type:
+            media_out.append('TYPE=' + self.type)
+        if self.group_id:
+            media_out.append('GROUP-ID=' + quoted(self.group_id))
+        if self.language:
+            media_out.append('LANGUAGE=' + quoted(self.language))
+        if self.assoc_language:
+            media_out.append('ASSOC-LANGUAGE=' + quoted(self.assoc_language))
+        if self.name:
+            media_out.append('NAME=' + quoted(self.name))
+        if self.default:
+            media_out.append('DEFAULT=' + self.default)
+        if self.autoselect:
+            media_out.append('AUTOSELECT=' + self.autoselect)
+        if self.forced:
+            media_out.append('FORCED=' + self.forced)
+        if self.instream_id:
+            media_out.append('INSTREAM-ID=' + self.instream_id)
+        if self.characteristics:
+            media_out.append('CHARACTERISTICS=' + quoted(self.characteristics))
+
+        return ('#EXT-X-MEDIA:' + ','.join(media_out))
+
+    def __str__(self):
+        return self.dumps()
+
+class MediaList(list, GroupedBasePathMixin):
+
+    def __str__(self):
+        output = [str(playlist) for playlist in self]
+        return '\n'.join(output)
+
+    @property
+    def uri(self):
+        return [media.uri for media in self]
+
+class PlaylistList(list, GroupedBasePathMixin):
+
+    def __str__(self):
+        output = [str(playlist) for playlist in self]
+        return '\n'.join(output)
+
+
+def denormalize_attribute(attribute):
+    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
... 1846 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