Skip to content

Commit

Permalink
Merge pull request #50 from GrumpyOldTroll/master
Browse files Browse the repository at this point in the history
Added a strict mode (strict=True optional parameter to M3U8 constructor), plus a couple of parse fixes.
  • Loading branch information
leandromoreira committed Jul 24, 2015
2 parents c6a10cd + 4d3cf88 commit 74aa57e
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 14 deletions.
4 changes: 2 additions & 2 deletions m3u8/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@


from m3u8.model import M3U8, Playlist, IFramePlaylist, Media, Segment
from m3u8.parser import parse, is_url
from m3u8.parser import parse, is_url, ParseError

__all__ = ('M3U8', 'Playlist', 'IFramePlaylist', 'Media',
'Segment', 'loads', 'load', 'parse')
'Segment', 'loads', 'load', 'parse', 'ParseError')

def loads(content):
'''
Expand Down
4 changes: 2 additions & 2 deletions m3u8/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ class M3U8(object):
('playlist_type', 'playlist_type')
)

def __init__(self, content=None, base_path=None, base_uri=None):
def __init__(self, content=None, base_path=None, base_uri=None, strict=False):
if content is not None:
self.data = parser.parse(content)
self.data = parser.parse(content, strict)
else:
self.data = {}
self._base_uri = base_uri
Expand Down
53 changes: 43 additions & 10 deletions m3u8/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import itertools
import re
from m3u8 import protocol
import exceptions

'''
http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.2
Expand All @@ -21,7 +22,15 @@ def cast_date_time(value):
def format_date_time(value):
return value.isoformat()

def parse(content):
class ParseError(exceptions.Exception):
def __init__(self, lineno, line):
self.lineno = lineno
self.line = line

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
'''
Expand All @@ -42,36 +51,36 @@ def parse(content):
'expect_playlist': False,
}

lineno = 0
for line in string_to_lines(content):
lineno += 1
line = line.strip()

if line.startswith(protocol.ext_x_byterange):
_parse_byterange(line, state)
state['expect_segment'] = True

elif state['expect_segment']:
_parse_ts_chunk(line, data, state)
state['expect_segment'] = False

elif state['expect_playlist']:
_parse_variant_playlist(line, data, state)
state['expect_playlist'] = False

elif line.startswith(protocol.ext_x_targetduration):
_parse_simple_parameter(line, data, float)

elif line.startswith(protocol.ext_x_media_sequence):
_parse_simple_parameter(line, data, int)

elif line.startswith(protocol.ext_x_program_date_time):
_, program_date_time = _parse_simple_parameter_raw_value(line, cast_date_time)
if not data.get('program_date_time'):
data['program_date_time'] = program_date_time
state['current_program_date_time'] = program_date_time

elif line.startswith(protocol.ext_x_discontinuity):
state['discontinuity'] = True

elif line.startswith(protocol.ext_x_cue_out):
state['cue_out'] = True

elif line.startswith(protocol.ext_x_version):
_parse_simple_parameter(line, data)

elif line.startswith(protocol.ext_x_allow_cache):
_parse_simple_parameter(line, data)

Expand Down Expand Up @@ -102,6 +111,25 @@ def parse(content):
elif line.startswith(protocol.ext_x_endlist):
data['is_endlist'] = True

elif line.startswith('#'):
# comment
pass

elif line.strip() == '':
# blank lines are legal
pass

elif state['expect_segment']:
_parse_ts_chunk(line, data, state)
state['expect_segment'] = False

elif state['expect_playlist']:
_parse_variant_playlist(line, data, state)
state['expect_playlist'] = False

elif strict:
raise ParseError(lineno, line)

return data

def _parse_key(line):
Expand All @@ -114,7 +142,10 @@ def _parse_key(line):

def _parse_extinf(line, data, state):
duration, title = line.replace(protocol.extinf + ':', '').split(',')
state['segment'] = {'duration': float(duration), 'title': remove_quotes(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')
Expand Down Expand Up @@ -173,6 +204,8 @@ def _parse_variant_playlist(line, data, state):
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):
Expand Down
19 changes: 19 additions & 0 deletions tests/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,25 @@
#EXT-X-ENDLIST
'''

# reversing byterange and extinf from IFRAME.
IFRAME_PLAYLIST2 = '''
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:10
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-I-FRAMES-ONLY
#EXT-X-BYTERANGE:9400@376
#EXTINF:4.12,
segment1.ts
#EXT-X-BYTERANGE:7144@47000
#EXTINF:3.56,
segment1.ts
#EXT-X-BYTERANGE:10340@1880
#EXTINF:3.82,
segment2.ts
#EXT-X-ENDLIST
'''

PLAYLIST_USING_BYTERANGES = '''
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
Expand Down
8 changes: 8 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,14 @@ def test_dump_should_work_for_iframe_playlists():

assert expected == obj.dumps().strip()

obj = m3u8.M3U8(playlists.IFRAME_PLAYLIST2)

expected = playlists.IFRAME_PLAYLIST.strip()

# expected that dump will reverse EXTINF and EXT-X-BYTERANGE,
# hence IFRAME_PLAYLIST dump from IFRAME_PLAYLIST2 parse.
assert expected == obj.dumps().strip()

def test_dump_should_include_program_date_time():
obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST_WITH_PROGRAM_DATE_TIME)

Expand Down

0 comments on commit 74aa57e

Please sign in to comment.