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

Coverage increased (+0.6%) to 95.122% when pulling **2876d443742bc91dfeda646072a6b71235bacfa8 on ziima:73-commaless-extinf** into **651c14ab5368692fd23f321a78bfe558d2b22f33 on globocom:master**.

leandromoreira: Thanks @ziima can you fixe https://travis-ci.org/globocom/m3u8/jobs/154559924#L442 for python 3 ?
ziima: I think that's unrelated existing bug. This is just first time it's covered in tests.
ziima: The bug should be addressed in #78
leandromoreira: Thanks @ziima can you rebase it?
diff --git a/m3u8/parser.py b/m3u8/parser.py
index f1c7590..a0b7f5d 100644
--- a/m3u8/parser.py
+++ b/m3u8/parser.py
@@ -91,7 +91,7 @@ def parse(content, strict=False):
             data['key'] = data.get('key', state['current_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):
@@ -145,8 +145,16 @@ 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)

EXTINF tag with optional comma?
m3u8 library requires EXTINF tag to have title, but the title is optional, see https://tools.ietf.org/html/draft-pantos-http-live-streaming-09#section-3.3.2 
diff --git a/tests/playlists.py b/tests/playlists.py
index b663eba..909af78 100755
--- a/tests/playlists.py
+++ b/tests/playlists.py
@@ -287,6 +287,15 @@ JUNK
 #EXT-X-ENDLIST
 '''
 
+# Playlist with EXTINF record not ending with comma
+SIMPLE_PLAYLIST_COMMALESS_EXTINF = '''
+#EXTM3U
+#EXT-X-TARGETDURATION:5220
+#EXTINF:5220
+http://media.example.com/entire.ts
+#EXT-X-ENDLIST
+'''
+
 DISCONTINUITY_PLAYLIST_WITH_PROGRAM_DATE_TIME = '''
 #EXTM3U
 #EXT-X-MEDIA-SEQUENCE:50116
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 9c20f8d..fbbf834 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -191,3 +191,15 @@ def test_parse_simple_playlist_messy_strict():
     with pytest.raises(ParseError) as catch:
         m3u8.parse(playlists.SIMPLE_PLAYLIST_MESSY, strict=True)
     assert str(catch.value) == 'Syntax error in manifest on line 5: JUNK'
+
+def test_commaless_extinf():
+    data = m3u8.parse(playlists.SIMPLE_PLAYLIST_COMMALESS_EXTINF)
+    assert 5220 == data['targetduration']
+    assert 0 == data['media_sequence']
+    assert ['http://media.example.com/entire.ts'] == [c['uri'] for c in data['segments']]
+    assert [5220] == [c['duration'] for c in data['segments']]
+
+def test_commaless_extinf_strict():
+    with pytest.raises(ParseError) as e:
+        m3u8.parse(playlists.SIMPLE_PLAYLIST_COMMALESS_EXTINF, strict=True)
+    assert str(e.value) == 'Syntax error in manifest on line 3: #EXTINF:5220'

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup==1.2.2
iniconfig==2.1.0
iso8601==2.1.0
-e git+https://github.com/globocom/m3u8.git@1897052d9362c21a4af52340e945edaec7de8fe8#egg=m3u8
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
tomli==2.2.1

name: m3u8
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - iso8601==2.1.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - tomli==2.2.1
prefix: /opt/conda/envs/m3u8

tests/test_parser.py::test_commaless_extinf tests/test_parser.py::test_commaless_extinf_strict
tests/test_parser.py::test_should_parse_simple_playlist_from_string tests/test_parser.py::test_should_parse_non_integer_duration_from_playlist_string tests/test_parser.py::test_should_parse_simple_playlist_from_string_with_different_linebreaks tests/test_parser.py::test_should_parse_sliding_window_playlist_from_string tests/test_parser.py::test_should_parse_playlist_with_encripted_segments_from_string tests/test_parser.py::test_should_load_playlist_with_iv_from_string tests/test_parser.py::test_should_add_key_attribute_to_segment_from_playlist tests/test_parser.py::test_should_parse_title_from_playlist tests/test_parser.py::test_should_parse_variant_playlist tests/test_parser.py::test_should_parse_variant_playlist_with_average_bandwidth tests/test_parser.py::test_should_parse_variant_playlist_with_iframe_playlists tests/test_parser.py::test_should_parse_variant_playlist_with_alt_iframe_playlists_layout tests/test_parser.py::test_should_parse_iframe_playlist tests/test_parser.py::test_should_parse_playlist_using_byteranges tests/test_parser.py::test_should_parse_endlist_playlist tests/test_parser.py::test_should_parse_ALLOW_CACHE tests/test_parser.py::test_should_parse_VERSION tests/test_parser.py::test_should_parse_program_date_time_from_playlist tests/test_parser.py::test_should_parse_scte35_from_playlist tests/test_parser.py::test_parse_simple_playlist_messy tests/test_parser.py::test_parse_simple_playlist_messy_strict
m3u8/parser.py
m3u8/parser.py