From 890a1cbb2ac85c1dd40612ba32e76d294bf87531 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Thu, 26 Oct 2023 07:46:29 -0600 Subject: [PATCH 01/17] Run pyupgrade for 3.7 --- m3u8/__init__.py | 1 - m3u8/mixins.py | 6 +++--- m3u8/model.py | 27 +++++++++++++-------------- m3u8/parser.py | 1 - m3u8/protocol.py | 1 - tests/m3u8server.py | 1 - tests/playlists.py | 1 - tests/test_loader.py | 15 +++++++-------- tests/test_model.py | 1 - tests/test_parser.py | 1 - tests/test_strict_validations.py | 1 - tests/test_variant_m3u8.py | 1 - 12 files changed, 23 insertions(+), 34 deletions(-) diff --git a/m3u8/__init__.py b/m3u8/__init__.py index f0e368d7..aeb70294 100644 --- a/m3u8/__init__.py +++ b/m3u8/__init__.py @@ -1,4 +1,3 @@ -# 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. diff --git a/m3u8/mixins.py b/m3u8/mixins.py index 21faf2a0..162b82a1 100644 --- a/m3u8/mixins.py +++ b/m3u8/mixins.py @@ -2,7 +2,7 @@ from urllib.parse import urljoin, urlsplit -class BasePathMixin(object): +class BasePathMixin: @property def absolute_uri(self): if self.uri is None: @@ -31,12 +31,12 @@ def get_path_from_uri(self): def base_path(self, newbase_path): if self.uri is not None: if not self.base_path: - self.uri = "%s/%s" % (newbase_path, self.uri) + self.uri = f"{newbase_path}/{self.uri}" else: self.uri = self.uri.replace(self.base_path, newbase_path) -class GroupedBasePathMixin(object): +class GroupedBasePathMixin: def _set_base_uri(self, new_base_uri): for item in self: item.base_uri = new_base_uri diff --git a/m3u8/model.py b/m3u8/model.py index a2bcef70..cd1f7397 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -1,4 +1,3 @@ -# 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. @@ -22,7 +21,7 @@ class MalformedPlaylistError(Exception): pass -class M3U8(object): +class M3U8: """ Represents a single M3U8 playlist. Should be instantiated with the content as string. @@ -376,7 +375,7 @@ def dumps(self): output.append("#EXT-X-MEDIA-SEQUENCE:" + str(self.media_sequence)) if self.discontinuity_sequence: output.append( - "#EXT-X-DISCONTINUITY-SEQUENCE:{}".format(self.discontinuity_sequence) + f"#EXT-X-DISCONTINUITY-SEQUENCE:{self.discontinuity_sequence}" ) if self.allow_cache: output.append("#EXT-X-ALLOW-CACHE:" + self.allow_cache.upper()) @@ -680,7 +679,7 @@ def __str__(self): @property def base_path(self): - return super(Segment, self).base_path + return super().base_path @base_path.setter def base_path(self, newbase_path): @@ -995,7 +994,7 @@ def __str__(self): else: media_types += [media.type] media_type = media.type.upper() - stream_inf.append('%s="%s"' % (media_type, media.group_id)) + stream_inf.append(f'{media_type}="{media.group_id}"') return "#EXT-X-STREAM-INF:" + ",".join(stream_inf) + "\n" + self.uri @@ -1087,7 +1086,7 @@ def __str__(self): return "#EXT-X-I-FRAME-STREAM-INF:" + ",".join(iframe_stream_inf) -class StreamInfo(object): +class StreamInfo: bandwidth = None closed_captions = None average_bandwidth = None @@ -1266,7 +1265,7 @@ class SessionDataList(TagList): pass -class Start(object): +class Start: def __init__(self, time_offset, precise=None): self.time_offset = float(time_offset) self.precise = precise @@ -1305,7 +1304,7 @@ def __str__(self): return "\n".join(output) -class ServerControl(object): +class ServerControl: def __init__( self, can_skip_until=None, @@ -1346,7 +1345,7 @@ def __str__(self): return self.dumps() -class Skip(object): +class Skip: def __init__(self, skipped_segments, recently_removed_dateranges=None): self.skipped_segments = skipped_segments self.recently_removed_dateranges = recently_removed_dateranges @@ -1366,7 +1365,7 @@ def __str__(self): return self.dumps() -class PartInformation(object): +class PartInformation: def __init__(self, part_target=None): self.part_target = part_target @@ -1397,7 +1396,7 @@ def dumps(self): for attr in ["byterange_start", "byterange_length"]: if self[attr] is not None: - hint.append("%s=%s" % (denormalize_attribute(attr), self[attr])) + hint.append(f"{denormalize_attribute(attr)}={self[attr]}") return "#EXT-X-PRELOAD-HINT:" + ",".join(hint) @@ -1405,7 +1404,7 @@ def __str__(self): return self.dumps() -class SessionData(object): +class SessionData: def __init__(self, data_id, value=None, uri=None, language=None): self.data_id = data_id self.value = value @@ -1432,7 +1431,7 @@ class DateRangeList(TagList): pass -class DateRange(object): +class DateRange: def __init__(self, **kwargs): self.id = kwargs["id"] self.start_date = kwargs.get("start_date") @@ -1479,7 +1478,7 @@ def dumps(self): # client attributes sorted alphabetically output order is predictable for attr, value in sorted(self.x_client_attrs): - daterange.append("%s=%s" % (denormalize_attribute(attr), value)) + daterange.append(f"{denormalize_attribute(attr)}={value}") return "#EXT-X-DATERANGE:" + ",".join(daterange) diff --git a/m3u8/parser.py b/m3u8/parser.py index 55e56e6b..9a2345db 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -1,4 +1,3 @@ -# 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. diff --git a/m3u8/protocol.py b/m3u8/protocol.py index 39300d1e..5e042064 100644 --- a/m3u8/protocol.py +++ b/m3u8/protocol.py @@ -1,4 +1,3 @@ -# 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. diff --git a/tests/m3u8server.py b/tests/m3u8server.py index 76de8d19..01fc234d 100644 --- a/tests/m3u8server.py +++ b/tests/m3u8server.py @@ -1,4 +1,3 @@ -# 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. diff --git a/tests/playlists.py b/tests/playlists.py index 1f565050..14de6919 100755 --- a/tests/playlists.py +++ b/tests/playlists.py @@ -1,4 +1,3 @@ -# 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. diff --git a/tests/test_loader.py b/tests/test_loader.py index 41bcbaa8..862a86f9 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,4 +1,3 @@ -# 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. @@ -77,28 +76,28 @@ def test_load_should_create_object_from_uri_with_relative_segments(): urlparsed = urllib.parse.urlparse(playlists.RELATIVE_PLAYLIST_URI) base_uri = os.path.normpath(urlparsed.path + "/..") prefix = urlparsed.scheme + "://" + urlparsed.netloc - expected_key_abspath = "%s%skey.bin" % ( + expected_key_abspath = "{}{}key.bin".format( prefix, os.path.normpath(base_uri + "/..") + "/", ) expected_key_path = "../key.bin" - expected_ts1_abspath = "%s%sentire1.ts" % (prefix, "/") + expected_ts1_abspath = "{}{}entire1.ts".format(prefix, "/") expected_ts1_path = "/entire1.ts" - expected_ts2_abspath = "%s%sentire2.ts" % ( + expected_ts2_abspath = "{}{}entire2.ts".format( prefix, os.path.normpath(base_uri + "/..") + "/", ) expected_ts2_path = "../entire2.ts" - expected_ts3_abspath = "%s%sentire3.ts" % ( + expected_ts3_abspath = "{}{}entire3.ts".format( prefix, os.path.normpath(base_uri + "/../.."), ) expected_ts3_path = "../../entire3.ts" - expected_ts4_abspath = "%s%sentire4.ts" % (prefix, base_uri + "/") + expected_ts4_abspath = "{}{}entire4.ts".format(prefix, base_uri + "/") expected_ts4_path = "entire4.ts" - expected_ts5_abspath = "%s%sentire5.ts" % (prefix, base_uri + "/") + expected_ts5_abspath = "{}{}entire5.ts".format(prefix, base_uri + "/") expected_ts5_path = "./entire5.ts" - expected_ts6_abspath = "%s%sentire6.ts" % (prefix, base_uri + "/") + expected_ts6_abspath = "{}{}entire6.ts".format(prefix, base_uri + "/") expected_ts6_path = ".//entire6.ts" assert isinstance(obj, m3u8.M3U8) diff --git a/tests/test_model.py b/tests/test_model.py index 40c41e30..6edbb197 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1,4 +1,3 @@ -# 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. diff --git a/tests/test_parser.py b/tests/test_parser.py index 7592b097..0e8878af 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,4 +1,3 @@ -# 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. diff --git a/tests/test_strict_validations.py b/tests/test_strict_validations.py index 29774619..75cde48e 100644 --- a/tests/test_strict_validations.py +++ b/tests/test_strict_validations.py @@ -1,4 +1,3 @@ -# 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. diff --git a/tests/test_variant_m3u8.py b/tests/test_variant_m3u8.py index 5fcd6940..d5e05894 100644 --- a/tests/test_variant_m3u8.py +++ b/tests/test_variant_m3u8.py @@ -1,4 +1,3 @@ -# 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 67679892cb7f96a697e97d5f01980c190dc70f6d Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Fri, 13 Oct 2023 09:18:09 -0600 Subject: [PATCH 02/17] Upgrade actions to latest stable version --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0e7708e..fd40068a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,9 +28,9 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} From 093f50199577553d1a60614b9a2ab4782e728137 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Fri, 13 Oct 2023 09:09:12 -0600 Subject: [PATCH 03/17] Add 3.12 to the list of versions to test --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fd40068a..bd84fec3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: # You can use PyPy versions in python-version. # For example, pypy2 and pypy3 matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] # Steps represent a sequence of tasks that will be executed as part of the job steps: From e19b882472f13bf8e1681760b31c4d85d8ee37cf Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Thu, 16 Nov 2023 16:27:05 -0700 Subject: [PATCH 04/17] Pass timespec all the way down in dumps --- m3u8/model.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/m3u8/model.py b/m3u8/model.py index cd1f7397..81f02acd 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -363,7 +363,7 @@ def add_segment(self, segment): def add_rendition_report(self, report): self.rendition_reports.append(report) - def dumps(self): + def dumps(self, timespec="milliseconds"): """ Returns the current m3u8 as a string. You could also use unicode() or str() @@ -415,7 +415,7 @@ def dumps(self): for key in self.session_keys: output.append(str(key)) - output.append(str(self.segments)) + output.append(self.segments.dumps(timespec)) if self.preload_hint: output.append(str(self.preload_hint)) @@ -701,14 +701,17 @@ def base_uri(self, newbase_uri): class SegmentList(list, GroupedBasePathMixin): - def __str__(self): + def dumps(self, timespec="milliseconds"): output = [] last_segment = None for segment in self: - output.append(segment.dumps(last_segment)) + output.append(segment.dumps(last_segment, timespec)) last_segment = segment return "\n".join(output) + def __str__(self): + return self.dumps() + @property def uri(self): return [seg.uri for seg in self] From 1d53f6f544549ac28b179718f25a26fe51b8ad60 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Thu, 16 Nov 2023 16:48:25 -0700 Subject: [PATCH 05/17] Add test of using timespec on the playlist --- tests/test_model.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index 6edbb197..a7ab77a9 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -764,6 +764,13 @@ def test_dump_segment_honors_timespec(): assert "EXT-X-PROGRAM-DATE-TIME:2014-08-13T13:36:33.000000+00:00" in segment_text +def test_dump_honors_timespec(): + obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST_WITH_PROGRAM_DATE_TIME) + obj_text = obj.dumps(timespec="microseconds").strip() + + assert "EXT-X-PROGRAM-DATE-TIME:2014-08-13T13:36:33.000000+00:00" in obj_text + + def test_dump_should_not_ignore_zero_duration(): obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST_WITH_ZERO_DURATION) From 260328672d5c3a42cfc5fd43590c0fc02e187fb9 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 16 Oct 2023 14:26:49 -0600 Subject: [PATCH 06/17] Remove unused imports --- m3u8/__init__.py | 1 - m3u8/model.py | 1 - m3u8/parser.py | 1 - tests/test_model.py | 2 +- 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/m3u8/__init__.py b/m3u8/__init__.py index aeb70294..5eae2376 100644 --- a/m3u8/__init__.py +++ b/m3u8/__init__.py @@ -3,7 +3,6 @@ # license that can be found in the LICENSE file. import os -import sys from urllib.parse import urljoin, urlsplit from m3u8.httpclient import DefaultHTTPClient diff --git a/m3u8/model.py b/m3u8/model.py index 81f02acd..b1e77a3f 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -2,7 +2,6 @@ # Use of this source code is governed by a MIT License # license that can be found in the LICENSE file. import decimal -import errno import os from m3u8.mixins import BasePathMixin, GroupedBasePathMixin diff --git a/m3u8/parser.py b/m3u8/parser.py index 9a2345db..47c10f36 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -5,7 +5,6 @@ import itertools import re from datetime import datetime, timedelta -from urllib.parse import urljoin as _urljoin try: from backports.datetime_fromisoformat import MonkeyPatch diff --git a/tests/test_model.py b/tests/test_model.py index a7ab77a9..349119fa 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1277,7 +1277,7 @@ def test_find_key_throws_when_no_match(): # deliberately empty ], ) - except KeyError as e: + except KeyError: threw = True finally: assert threw From cac677c74b5029cf34fb55da7f5245b6c743d900 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 16 Oct 2023 14:30:54 -0600 Subject: [PATCH 07/17] Compare to singletons with is --- tests/test_loader.py | 2 +- tests/test_model.py | 76 ++++++++++++++++++++++---------------------- tests/test_parser.py | 44 ++++++++++++------------- 3 files changed, 61 insertions(+), 61 deletions(-) diff --git a/tests/test_loader.py b/tests/test_loader.py index 862a86f9..eb77de26 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -140,7 +140,7 @@ def test_presence_of_base_uri_if_provided_when_loading_from_string(): def test_raise_timeout_exception_if_timeout_happens_when_loading_from_uri(): try: - obj = m3u8.load(playlists.TIMEOUT_SIMPLE_PLAYLIST_URI, timeout=1) + m3u8.load(playlists.TIMEOUT_SIMPLE_PLAYLIST_URI, timeout=1) except: assert True else: diff --git a/tests/test_model.py b/tests/test_model.py index 349119fa..480af21e 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -135,30 +135,30 @@ def test_segment_discontinuity_attribute(): obj = m3u8.M3U8(playlists.DISCONTINUITY_PLAYLIST_WITH_PROGRAM_DATE_TIME) segments = obj.segments - assert segments[0].discontinuity == False - assert segments[5].discontinuity == True - assert segments[6].discontinuity == False + assert segments[0].discontinuity is False + assert segments[5].discontinuity is True + assert segments[6].discontinuity is False def test_segment_cue_out_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_PLAYLIST) segments = obj.segments - assert segments[1].cue_out == True - assert segments[2].cue_out == True - assert segments[3].cue_out == False + assert segments[1].cue_out is True + assert segments[2].cue_out is True + assert segments[3].cue_out is False def test_segment_cue_out_start_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_WITH_DURATION_PLAYLIST) - assert obj.segments[0].cue_out_start == True + assert obj.segments[0].cue_out_start is True def test_segment_cue_in_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_WITH_DURATION_PLAYLIST) - assert obj.segments[2].cue_in == True + assert obj.segments[2].cue_in is True def test_segment_cue_out_cont_dumps(): @@ -219,8 +219,8 @@ def test_segment_cue_out_in_dumps(): def test_segment_elemental_scte35_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_ELEMENTAL_PLAYLIST) segments = obj.segments - assert segments[4].cue_out == True - assert segments[9].cue_out == False + assert segments[4].cue_out is True + assert segments[9].cue_out is False assert ( segments[4].scte35 == "/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg==" ) @@ -229,14 +229,14 @@ def test_segment_elemental_scte35_attribute(): def test_segment_envivio_scte35_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_ENVIVIO_PLAYLIST) segments = obj.segments - assert segments[3].cue_out == True + assert segments[3].cue_out is True assert ( segments[4].scte35 == "/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==" ) assert ( segments[5].scte35 == "/DAlAAAENOOQAP/wFAUBAABrf+//N25XDf4B9p/gAAEBAQAAxKni9A==" ) - assert segments[7].cue_out == False + assert segments[7].cue_out is False def test_segment_unknown_scte35_attribute(): @@ -247,8 +247,8 @@ def test_segment_unknown_scte35_attribute(): def test_segment_cue_out_no_duration(): obj = m3u8.M3U8(playlists.CUE_OUT_NO_DURATION_PLAYLIST) - assert obj.segments[0].cue_out_start == True - assert obj.segments[2].cue_in == True + assert obj.segments[0].cue_out_start is True + assert obj.segments[2].cue_in is True def test_segment_asset_metadata_dumps(): @@ -301,7 +301,7 @@ def test_key_attribute_without_initialization_vector(): assert "AES-128" == obj.keys[0].method assert "/key" == obj.keys[0].uri - assert None == obj.keys[0].iv + assert None is obj.keys[0].iv def test_session_keys_on_clear_playlist(): @@ -341,7 +341,7 @@ def test_session_key_attribute_without_initialization_vector(): assert "AES-128" == obj.session_keys[0].method assert "/key" == obj.session_keys[0].uri - assert None == obj.session_keys[0].iv + assert None is obj.session_keys[0].iv def test_segments_attribute(): @@ -371,7 +371,7 @@ def test_segments_attribute_without_title(): assert "/foo/bar-1.ts" == obj.segments[0].uri assert 1500 == obj.segments[0].duration - assert None == obj.segments[0].title + assert None is obj.segments[0].title def test_segments_attribute_without_duration(): @@ -384,7 +384,7 @@ def test_segments_attribute_without_duration(): assert "/foo/bar-1.ts" == obj.segments[0].uri assert "Segment title" == obj.segments[0].title - assert None == obj.segments[0].duration + assert None is obj.segments[0].duration def test_segments_attribute_with_byterange(): @@ -502,34 +502,34 @@ def test_playlists_attribute(): assert "/url/1.m3u8" == obj.playlists[0].uri assert 1 == obj.playlists[0].stream_info.program_id assert 320000 == obj.playlists[0].stream_info.bandwidth - assert None == obj.playlists[0].stream_info.closed_captions - assert None == obj.playlists[0].stream_info.codecs + assert None is obj.playlists[0].stream_info.closed_captions + assert None is obj.playlists[0].stream_info.codecs - assert None == obj.playlists[0].media[0].uri + assert None is obj.playlists[0].media[0].uri assert "high" == obj.playlists[0].media[0].group_id assert "VIDEO" == obj.playlists[0].media[0].type - assert None == obj.playlists[0].media[0].language + assert None is obj.playlists[0].media[0].language assert "High" == obj.playlists[0].media[0].name - assert None == obj.playlists[0].media[0].default - assert None == obj.playlists[0].media[0].autoselect - assert None == obj.playlists[0].media[0].forced - assert None == obj.playlists[0].media[0].characteristics + assert None is obj.playlists[0].media[0].default + assert None is obj.playlists[0].media[0].autoselect + assert None is obj.playlists[0].media[0].forced + assert None is obj.playlists[0].media[0].characteristics assert "/url/2.m3u8" == obj.playlists[1].uri assert 1 == obj.playlists[1].stream_info.program_id assert 120000 == obj.playlists[1].stream_info.bandwidth - assert None == obj.playlists[1].stream_info.closed_captions + assert None is obj.playlists[1].stream_info.closed_captions assert "mp4a.40.5" == obj.playlists[1].stream_info.codecs - assert None == obj.playlists[1].media[0].uri + assert None is obj.playlists[1].media[0].uri assert "low" == obj.playlists[1].media[0].group_id assert "VIDEO" == obj.playlists[1].media[0].type - assert None == obj.playlists[1].media[0].language + assert None is obj.playlists[1].media[0].language assert "Low" == obj.playlists[1].media[0].name assert "YES" == obj.playlists[1].media[0].default assert "YES" == obj.playlists[1].media[0].autoselect - assert None == obj.playlists[1].media[0].forced - assert None == obj.playlists[1].media[0].characteristics + assert None is obj.playlists[1].media[0].forced + assert None is obj.playlists[1].media[0].characteristics assert [] == obj.iframe_playlists @@ -545,8 +545,8 @@ def test_playlists_attribute_without_program_id(): assert "/url/1.m3u8" == obj.playlists[0].uri assert 320000 == obj.playlists[0].stream_info.bandwidth - assert None == obj.playlists[0].stream_info.codecs - assert None == obj.playlists[0].stream_info.program_id + assert None is obj.playlists[0].stream_info.codecs + assert None is obj.playlists[0].stream_info.program_id def test_playlists_attribute_with_resolution(): @@ -554,7 +554,7 @@ def test_playlists_attribute_with_resolution(): assert 2 == len(obj.playlists) assert (512, 288) == obj.playlists[0].stream_info.resolution - assert None == obj.playlists[1].stream_info.resolution + assert None is obj.playlists[1].stream_info.resolution def test_iframe_playlists_attribute(): @@ -587,9 +587,9 @@ def test_iframe_playlists_attribute(): assert "avc1.4d001f" == obj.iframe_playlists[0].iframe_stream_info.codecs assert "/url/2.m3u8" == obj.iframe_playlists[1].uri - assert None == obj.iframe_playlists[1].iframe_stream_info.program_id + assert None is obj.iframe_playlists[1].iframe_stream_info.program_id assert "120000" == obj.iframe_playlists[1].iframe_stream_info.bandwidth - assert None == obj.iframe_playlists[1].iframe_stream_info.resolution + assert None is obj.iframe_playlists[1].iframe_stream_info.resolution assert "avc1.4d400d" == obj.iframe_playlists[1].iframe_stream_info.codecs @@ -599,7 +599,7 @@ def test_version_attribute(): assert 2 == obj.version mock_parser_data(obj, {}) - assert None == obj.version + assert None is obj.version def test_version_settable_as_int(): @@ -622,7 +622,7 @@ def test_allow_cache_attribute(): assert "no" == obj.allow_cache mock_parser_data(obj, {}) - assert None == obj.allow_cache + assert None is obj.allow_cache def test_files_attribute_should_list_all_files_including_segments_and_key(): diff --git a/tests/test_parser.py b/tests/test_parser.py index 0e8878af..0a61077a 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -162,8 +162,8 @@ def test_should_parse_variant_playlist(): data = m3u8.parse(playlists.VARIANT_PLAYLIST) playlists_list = list(data["playlists"]) - assert True == data["is_variant"] - assert None == data["media_sequence"] + assert True is data["is_variant"] + assert None is data["media_sequence"] assert 4 == len(playlists_list) assert "http://example.com/low.m3u8" == playlists_list[0]["uri"] @@ -180,8 +180,8 @@ def test_should_parse_variant_playlist_with_cc_subtitles_and_audio(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_CC_SUBS_AND_AUDIO) playlists_list = list(data["playlists"]) - assert True == data["is_variant"] - assert None == data["media_sequence"] + assert True is data["is_variant"] + assert None is data["media_sequence"] assert 2 == len(playlists_list) assert "http://example.com/with-cc-hi.m3u8" == playlists_list[0]["uri"] @@ -252,7 +252,7 @@ def test_should_parse_variant_playlist_with_iframe_playlists(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IFRAME_PLAYLISTS) iframe_playlists = list(data["iframe_playlists"]) - assert True == data["is_variant"] + assert True is data["is_variant"] assert 4 == len(iframe_playlists) @@ -271,7 +271,7 @@ def test_should_parse_variant_playlist_with_alt_iframe_playlists_layout(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_ALT_IFRAME_PLAYLISTS_LAYOUT) iframe_playlists = list(data["iframe_playlists"]) - assert True == data["is_variant"] + assert True is data["is_variant"] assert 4 == len(iframe_playlists) @@ -289,7 +289,7 @@ def test_should_parse_variant_playlist_with_alt_iframe_playlists_layout(): def test_should_parse_iframe_playlist(): data = m3u8.parse(playlists.IFRAME_PLAYLIST) - assert True == data["is_i_frames_only"] + assert True is data["is_i_frames_only"] assert 4.12 == data["segments"][0]["duration"] assert "9400@376" == data["segments"][0]["byterange"] assert "segment1.ts" == data["segments"][0]["uri"] @@ -299,7 +299,7 @@ def test_should_parse_variant_playlist_with_image_playlists(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS) image_playlists = list(data['image_playlists']) - assert True == data['is_variant'] + assert True is data['is_variant'] assert 2 == len(image_playlists) assert '320x180' == image_playlists[0]['image_stream_info']['resolution'] assert 'jpeg' == image_playlists[0]['image_stream_info']['codecs'] @@ -311,7 +311,7 @@ def test_should_parse_variant_playlist_with_image_playlists(): def test_should_parse_vod_image_playlist(): data = m3u8.parse(playlists.VOD_IMAGE_PLAYLIST) - assert True == data['is_images_only'] + assert True is data['is_images_only'] assert 8 == len(data['tiles']) assert 'preroll-ad-1.jpg' == data['segments'][0]['uri'] assert '640x360' == data['tiles'][0]['resolution'] @@ -322,7 +322,7 @@ def test_should_parse_vod_image_playlist(): def test_should_parse_vod_image_playlist2(): data = m3u8.parse(playlists.VOD_IMAGE_PLAYLIST2) - assert True == data['is_images_only'] + assert True is data['is_images_only'] assert '640x360' == data['tiles'][0]['resolution'] assert '4x3' == data['tiles'][0]['layout'] assert 2.002 == data['tiles'][0]['duration'] @@ -332,7 +332,7 @@ def test_should_parse_vod_image_playlist2(): def test_should_parse_live_image_playlist(): data = m3u8.parse(playlists.LIVE_IMAGE_PLAYLIST) - assert True == data['is_images_only'] + assert True is data['is_images_only'] assert 10 == len(data['segments']) assert 'content-123.jpg' == data['segments'][0]['uri'] assert 'content-124.jpg' == data['segments'][1]['uri'] @@ -348,7 +348,7 @@ def test_should_parse_live_image_playlist(): def test_should_parse_playlist_using_byteranges(): data = m3u8.parse(playlists.PLAYLIST_USING_BYTERANGES) - assert False == data["is_i_frames_only"] + assert False is data["is_i_frames_only"] assert 10 == data["segments"][0]["duration"] assert "76242@0" == data["segments"][0]["byterange"] assert "segment.ts" == data["segments"][0]["uri"] @@ -356,10 +356,10 @@ def test_should_parse_playlist_using_byteranges(): def test_should_parse_endlist_playlist(): data = m3u8.parse(playlists.SIMPLE_PLAYLIST) - assert True == data["is_endlist"] + assert True is data["is_endlist"] data = m3u8.parse(playlists.SLIDING_WINDOW_PLAYLIST) - assert False == data["is_endlist"] + assert False is data["is_endlist"] def test_should_parse_ALLOW_CACHE(): @@ -769,8 +769,8 @@ def test_gap(): data = m3u8.parse(playlists.GAP_PLAYLIST) assert data["segments"][0]["gap_tag"] is None - assert data["segments"][1]["gap_tag"] == True - assert data["segments"][2]["gap_tag"] == True + assert data["segments"][1]["gap_tag"] is True + assert data["segments"][2]["gap_tag"] is True assert data["segments"][3]["gap_tag"] is None @@ -781,7 +781,7 @@ def test_gap_in_parts(): assert data["segments"][0]["parts"][0].get("gap", None) is None assert data["segments"][0]["parts"][1]["gap_tag"] is None assert data["segments"][0]["parts"][1]["gap"] == "YES" - assert data["segments"][0]["parts"][2]["gap_tag"] == True + assert data["segments"][0]["parts"][2]["gap_tag"] is True assert data["segments"][0]["parts"][2].get("gap", None) is None @@ -789,7 +789,7 @@ def test_should_parse_variant_playlist_with_iframe_with_average_bandwidth(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IFRAME_AVERAGE_BANDWIDTH) iframe_playlists = list(data["iframe_playlists"]) - assert True == data["is_variant"] + assert True is data["is_variant"] assert 4 == len(iframe_playlists) @@ -812,7 +812,7 @@ def test_should_parse_variant_playlist_with_iframe_with_video_range(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IFRAME_VIDEO_RANGE) iframe_playlists = list(data["iframe_playlists"]) - assert True == data["is_variant"] + assert True is data["is_variant"] assert 4 == len(iframe_playlists) @@ -830,7 +830,7 @@ def test_should_parse_variant_playlist_with_iframe_with_hdcp_level(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IFRAME_HDCP_LEVEL) iframe_playlists = list(data["iframe_playlists"]) - assert True == data["is_variant"] + assert True is data["is_variant"] assert 4 == len(iframe_playlists) @@ -868,13 +868,13 @@ def test_content_steering(): def test_cue_in_pops_scte35_data_and_duration(): data = m3u8.parse(playlists.CUE_OUT_ELEMENTAL_PLAYLIST) - assert data["segments"][9]["cue_in"] == True + assert data["segments"][9]["cue_in"] is True assert ( data["segments"][9]["scte35"] == "/DAlAAAAAAAAAP/wFAUAAAABf+//wpiQkv4ARKogAAEBAQAAQ6sodg==" ) assert data["segments"][9]["scte35_duration"] == "50" - assert data["segments"][10]["cue_in"] == False + assert data["segments"][10]["cue_in"] is False assert data["segments"][10]["scte35"] is None assert data["segments"][10]["scte35_duration"] is None From e23d75cc052463dbb6cc1e74a63109c978e66ae9 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 16 Oct 2023 14:32:03 -0600 Subject: [PATCH 08/17] Remove duplicate definition of ImagePlaylist --- m3u8/model.py | 75 --------------------------------------------------- 1 file changed, 75 deletions(-) diff --git a/m3u8/model.py b/m3u8/model.py index b1e77a3f..8b94a9e4 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -1611,81 +1611,6 @@ def dumps(self): def __str__(self): return self.dumps() -class ImagePlaylist(BasePathMixin): - ''' - ImagePlaylist object representing a link to a - variant M3U8 image playlist with a specific bitrate. - - Attributes: - - `image_stream_info` is a named tuple containing the attributes: - `bandwidth`, `resolution` which is a tuple (w, h) of integers and `codecs`, - - More info: https://github.com/image-media-playlist/spec/blob/master/image_media_playlist_v0_4.pdf - ''' - - def __init__(self, base_uri, uri, image_stream_info): - self.uri = uri - self.base_uri = base_uri - - resolution = image_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.image_stream_info = StreamInfo( - bandwidth=image_stream_info.get('bandwidth'), - average_bandwidth=image_stream_info.get('average_bandwidth'), - video=image_stream_info.get('video'), - # Audio, subtitles, closed captions, video range and hdcp level should not exist in - # EXT-X-IMAGE-STREAM-INF, so just hardcode them to None. - audio=None, - subtitles=None, - closed_captions=None, - program_id=image_stream_info.get('program_id'), - resolution=resolution_pair, - codecs=image_stream_info.get('codecs'), - video_range=None, - hdcp_level=None, - frame_rate=None, - pathway_id=image_stream_info.get('pathway_id'), - stable_variant_id=image_stream_info.get('stable_variant_id') - ) - - def __str__(self): - image_stream_inf = [] - if self.image_stream_info.program_id: - image_stream_inf.append('PROGRAM-ID=%d' % - self.image_stream_info.program_id) - if self.image_stream_info.bandwidth: - image_stream_inf.append('BANDWIDTH=%d' % - self.image_stream_info.bandwidth) - if self.image_stream_info.average_bandwidth: - image_stream_inf.append('AVERAGE-BANDWIDTH=%d' % - self.image_stream_info.average_bandwidth) - if self.image_stream_info.resolution: - res = (str(self.image_stream_info.resolution[0]) + 'x' + - str(self.image_stream_info.resolution[1])) - image_stream_inf.append('RESOLUTION=' + res) - if self.image_stream_info.codecs: - image_stream_inf.append('CODECS=' + - quoted(self.image_stream_info.codecs)) - if self.uri: - image_stream_inf.append('URI=' + quoted(self.uri)) - if self.image_stream_info.pathway_id: - image_stream_inf.append( - 'PATHWAY-ID=' + quoted(self.image_stream_info.pathway_id) - ) - if self.image_stream_info.stable_variant_id: - image_stream_inf.append( - 'STABLE-VARIANT-ID=' + quoted(self.image_stream_info.stable_variant_id) - ) - - return '#EXT-X-IMAGE-STREAM-INF:' + ','.join(image_stream_inf) - - def find_key(keydata, keylist): if not keydata: return None From 0a758ddbdf92ea0f23472971603bf1b08d869998 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 16 Oct 2023 14:49:22 -0600 Subject: [PATCH 09/17] Remove test that was just for Python 2 --- tests/test_model.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index 480af21e..bba43706 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -7,7 +7,6 @@ import datetime import os -import sys import playlists import pytest @@ -1237,15 +1236,6 @@ def test_should_round_frame_rate(): assert expected == obj.dumps().strip() -@pytest.mark.skipif(sys.version_info >= (3,), reason="unicode not available in v3") -def test_m3u8_unicode_method(): - obj = m3u8.M3U8(playlists.SIMPLE_PLAYLIST) - - result = unicode(obj).strip() - expected = playlists.SIMPLE_PLAYLIST.strip() - assert result == expected - - def test_add_segment_to_playlist(): obj = m3u8.M3U8() From 164075d6ae1062a34d23d448bcaee4ed0e404bb5 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 16 Oct 2023 14:49:38 -0600 Subject: [PATCH 10/17] Remove duplicate tests --- tests/test_model.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index bba43706..8931d4a0 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1015,22 +1015,6 @@ def test_should_normalize_variant_streams_urls_if_base_path_passed_to_constructo assert obj.dumps().strip() == expected -def test_should_normalize_segments_and_key_urls_if_base_path_attribute_updated(): - base_path = "http://videoserver.com/hls/live" - - obj = m3u8.M3U8(playlists.PLAYLIST_WITH_ENCRYPTED_SEGMENTS_AND_IV) - obj.base_path = base_path # update later - - expected = ( - playlists.PLAYLIST_WITH_ENCRYPTED_SEGMENTS_AND_IV_SORTED.replace(", IV", ",IV") - .replace("../../../../hls", base_path) - .replace("/hls-key", base_path) - .strip() - ) - - assert obj.dumps() == expected - - def test_should_normalize_segments_and_key_urls_if_base_path_attribute_updated(): base_path = "http://videoserver.com/hls/live" @@ -1569,13 +1553,6 @@ def test_add_content_steering_base_uri_update(): ) -def test_dump_should_work_for_variant_playlists_with_image_playlists(): - obj = m3u8.M3U8(playlists.VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS) - - expected = playlists.VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS.strip() - - assert expected == obj.dumps().strip() - def test_dump_should_work_for_variant_playlists_with_image_playlists(): obj = m3u8.M3U8(playlists.VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS) From 1fabfef6ef8f34b32ecc99a182b5c03eb6266a94 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 16 Oct 2023 14:50:08 -0600 Subject: [PATCH 11/17] Use different names for tests --- tests/m3u8server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/m3u8server.py b/tests/m3u8server.py index 01fc234d..1d135e7f 100644 --- a/tests/m3u8server.py +++ b/tests/m3u8server.py @@ -14,7 +14,7 @@ @route("/path/to/redirect_me") -def simple(): +def redirect_route(): redirect("/simple.m3u8") @@ -25,14 +25,14 @@ def simple(): @route("/timeout_simple.m3u8") -def simple(): +def timeout_simple(): time.sleep(5) response.set_header("Content-Type", "application/vnd.apple.mpegurl") return m3u8_file("simple-playlist.m3u8") @route("/path/to/relative-playlist.m3u8") -def simple(): +def relative_playlist(): response.set_header("Content-Type", "application/vnd.apple.mpegurl") return m3u8_file("relative-playlist.m3u8") From 6bd86d5224c43010948b2f2b9b92073dc4f8c980 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 16 Oct 2023 14:50:30 -0600 Subject: [PATCH 12/17] Catch the expected type of error --- tests/test_loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_loader.py b/tests/test_loader.py index eb77de26..509b7714 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -3,6 +3,7 @@ # license that can be found in the LICENSE file. import os +import socket import urllib.parse import m3u8 import pytest @@ -141,7 +142,7 @@ def test_presence_of_base_uri_if_provided_when_loading_from_string(): def test_raise_timeout_exception_if_timeout_happens_when_loading_from_uri(): try: m3u8.load(playlists.TIMEOUT_SIMPLE_PLAYLIST_URI, timeout=1) - except: + except socket.timeout: assert True else: assert False From d26063676eaed5e1b62c65b10ecbd7c2b4d37d45 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Thu, 2 Nov 2023 18:39:13 -0600 Subject: [PATCH 13/17] Add action to check ruff --- .github/workflows/ruff.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/ruff.yml diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 00000000..47c12129 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,11 @@ +name: Ruff +run-name: Ruff + +on: [ push, pull_request ] + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 From 69b60eacfd614e8bc2836951f80b69c05dfe72f3 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 20 Nov 2023 12:07:31 -0700 Subject: [PATCH 14/17] Run format with ruff --- .github/workflows/ruff.yml | 8 +++++ m3u8/__init__.py | 2 +- m3u8/model.py | 31 ++++++++-------- m3u8/parser.py | 22 +++++------- m3u8/protocol.py | 6 ++-- tests/playlists.py | 17 +++++---- tests/test_model.py | 9 ++--- tests/test_parser.py | 72 ++++++++++++++++++++------------------ tests/test_variant_m3u8.py | 67 ++++++++++++++++++++++------------- 9 files changed, 128 insertions(+), 106 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 47c12129..70f6c2a5 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -9,3 +9,11 @@ jobs: steps: - uses: actions/checkout@v4 - uses: chartboost/ruff-action@v1 + + ruff_format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 + with: + args: format --check diff --git a/m3u8/__init__.py b/m3u8/__init__.py index 5eae2376..d11dbf30 100644 --- a/m3u8/__init__.py +++ b/m3u8/__init__.py @@ -60,7 +60,7 @@ "loads", "load", "parse", - "ParseError" + "ParseError", ) diff --git a/m3u8/model.py b/m3u8/model.py index 8b94a9e4..c855469f 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -151,7 +151,7 @@ class M3U8: ("allow_cache", "allow_cache"), ("playlist_type", "playlist_type"), ("discontinuity_sequence", "discontinuity_sequence"), - ("is_images_only", "is_images_only") + ("is_images_only", "is_images_only"), ) def __init__( @@ -232,12 +232,12 @@ def _initialize_attributes(self): ) self.image_playlists = PlaylistList() - for img_pl in self.data.get('image_playlists', []): + for img_pl in self.data.get("image_playlists", []): self.image_playlists.append( ImagePlaylist( base_uri=self.base_uri, uri=img_pl["uri"], - image_stream_info=img_pl["image_stream_info"] + image_stream_info=img_pl["image_stream_info"], ) ) @@ -1547,27 +1547,28 @@ def __init__(self, base_uri, uri, image_stream_info): hdcp_level=None, frame_rate=None, pathway_id=image_stream_info.get("pathway_id"), - stable_variant_id=image_stream_info.get("stable_variant_id") + stable_variant_id=image_stream_info.get("stable_variant_id"), ) def __str__(self): image_stream_inf = [] if self.image_stream_info.program_id: - image_stream_inf.append("PROGRAM-ID=%d" % - self.image_stream_info.program_id) + image_stream_inf.append("PROGRAM-ID=%d" % self.image_stream_info.program_id) if self.image_stream_info.bandwidth: - image_stream_inf.append("BANDWIDTH=%d" % - self.image_stream_info.bandwidth) + image_stream_inf.append("BANDWIDTH=%d" % self.image_stream_info.bandwidth) if self.image_stream_info.average_bandwidth: - image_stream_inf.append("AVERAGE-BANDWIDTH=%d" % - self.image_stream_info.average_bandwidth) + image_stream_inf.append( + "AVERAGE-BANDWIDTH=%d" % self.image_stream_info.average_bandwidth + ) if self.image_stream_info.resolution: - res = (str(self.image_stream_info.resolution[0]) + "x" + - str(self.image_stream_info.resolution[1])) + res = ( + str(self.image_stream_info.resolution[0]) + + "x" + + str(self.image_stream_info.resolution[1]) + ) image_stream_inf.append("RESOLUTION=" + res) if self.image_stream_info.codecs: - image_stream_inf.append("CODECS=" + - quoted(self.image_stream_info.codecs)) + image_stream_inf.append("CODECS=" + quoted(self.image_stream_info.codecs)) if self.uri: image_stream_inf.append("URI=" + quoted(self.uri)) if self.image_stream_info.pathway_id: @@ -1581,6 +1582,7 @@ def __str__(self): return "#EXT-X-IMAGE-STREAM-INF:" + ",".join(image_stream_inf) + class Tiles(BasePathMixin): """ Image tiles from a M3U8 playlist @@ -1611,6 +1613,7 @@ def dumps(self): def __str__(self): return self.dumps() + def find_key(keydata, keylist): if not keydata: return None diff --git a/m3u8/parser.py b/m3u8/parser.py index 47c10f36..1d04c2f5 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -8,6 +8,7 @@ try: from backports.datetime_fromisoformat import MonkeyPatch + MonkeyPatch.patch_fromisoformat() except ImportError: pass @@ -226,7 +227,7 @@ def parse(content, strict=False, custom_tags_parser=None): _parse_image_stream_inf(line, data) elif line.startswith(protocol.ext_x_images_only): - data['is_images_only'] = True + data["is_images_only"] = True elif line.startswith(protocol.ext_x_tiles): _parse_tiles(line, data, state) @@ -239,11 +240,11 @@ def parse(content, strict=False, custom_tags_parser=None): # blank lines are legal pass - elif (not line.startswith('#')) and (state["expect_segment"]): + elif (not line.startswith("#")) and (state["expect_segment"]): _parse_ts_chunk(line, data, state) state["expect_segment"] = False - elif (not line.startswith('#')) and (state["expect_playlist"]): + elif (not line.startswith("#")) and (state["expect_playlist"]): _parse_variant_playlist(line, data, state) state["expect_playlist"] = False @@ -290,9 +291,7 @@ def _parse_ts_chunk(line, data, state): segment["program_date_time"] = state.pop("program_date_time") if state.get("current_program_date_time"): segment["current_program_date_time"] = state["current_program_date_time"] - state["current_program_date_time"] += timedelta( - seconds=segment["duration"] - ) + state["current_program_date_time"] += timedelta(seconds=segment["duration"]) segment["uri"] = line segment["cue_in"] = state.pop("cue_in", False) segment["cue_out"] = state.pop("cue_out", False) @@ -391,21 +390,18 @@ def _parse_image_stream_inf(line, data): ) image_playlist = { "uri": image_stream_info.pop("uri"), - "image_stream_info": image_stream_info + "image_stream_info": image_stream_info, } data["image_playlists"].append(image_playlist) - def _parse_tiles(line, data, state): attribute_parser = remove_quotes_parser("uri") attribute_parser["resolution"] = str attribute_parser["layout"] = str attribute_parser["duration"] = float - tiles_info = _parse_attribute_list( - protocol.ext_x_tiles, line, attribute_parser - ) + tiles_info = _parse_attribute_list(protocol.ext_x_tiles, line, attribute_parser) data["tiles"].append(tiles_info) @@ -578,9 +574,7 @@ def _parse_part(line, data, state): # this should always be true according to spec if state.get("current_program_date_time"): part["program_date_time"] = state["current_program_date_time"] - state["current_program_date_time"] += timedelta( - seconds=part["duration"] - ) + state["current_program_date_time"] += timedelta(seconds=part["duration"]) part["dateranges"] = state.pop("dateranges", None) part["gap_tag"] = state.pop("gap", None) diff --git a/m3u8/protocol.py b/m3u8/protocol.py index 5e042064..29c53d93 100644 --- a/m3u8/protocol.py +++ b/m3u8/protocol.py @@ -40,6 +40,6 @@ ext_x_daterange = "#EXT-X-DATERANGE" ext_x_gap = "#EXT-X-GAP" ext_x_content_steering = "#EXT-X-CONTENT-STEERING" -ext_x_image_stream_inf = '#EXT-X-IMAGE-STREAM-INF' -ext_x_images_only = '#EXT-X-IMAGES-ONLY' -ext_x_tiles = '#EXT-X-TILES' +ext_x_image_stream_inf = "#EXT-X-IMAGE-STREAM-INF" +ext_x_images_only = "#EXT-X-IMAGES-ONLY" +ext_x_tiles = "#EXT-X-TILES" diff --git a/tests/playlists.py b/tests/playlists.py index 14de6919..6b850493 100755 --- a/tests/playlists.py +++ b/tests/playlists.py @@ -1230,7 +1230,7 @@ #EXT-X-MEDIA:TYPE=AUDIO,NAME="audio-aac-eng",STABLE-RENDITION-ID="a8213e27c12a158ea8660e0fe8bdcac6072ca26d984e7e8603652bc61fdceffa",URI="http://example.com/eng.m3u8" """ -VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS = ''' +VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS = """ #EXTM3U #EXT-X-VERSION:3 #EXT-X-INDEPENDENT-SEGMENTS @@ -1246,9 +1246,9 @@ index_0_a/new_index_0_a.m3u8S #EXT-X-IMAGE-STREAM-INF:BANDWIDTH=16460,RESOLUTION=320x180,CODECS="jpeg",URI="5x2_320x180/320x180-5x2.m3u8" #EXT-X-IMAGE-STREAM-INF:BANDWIDTH=32920,RESOLUTION=640x360,CODECS="jpeg",URI="5x2_640x360/640x360-5x2.m3u8" -''' +""" -VOD_IMAGE_PLAYLIST = ''' +VOD_IMAGE_PLAYLIST = """ #EXTM3U #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:6 @@ -1295,9 +1295,9 @@ #EXT-X-TILES:RESOLUTION=640x360,LAYOUT=5x2,DURATION=6.006 content-7.jpg #EXT-X-ENDLIST -''' +""" -VOD_IMAGE_PLAYLIST2 = ''' +VOD_IMAGE_PLAYLIST2 = """ #EXTM3U #EXT-X-TARGETDURATION:6 #EXT-X-VERSION:7 @@ -1331,9 +1331,9 @@ #EXT-X-TILES:RESOLUTION=640x360,LAYOUT=4x3,DURATION=2.002 credits_2_1.jpg #EXT-X-ENDLIST -''' +""" -LIVE_IMAGE_PLAYLIST = ''' +LIVE_IMAGE_PLAYLIST = """ #EXTM3U #EXT-X-TARGETDURATION:6 #EXT-X-VERSION:7 @@ -1368,8 +1368,7 @@ content-130.jpg #EXTINF:6.006, content-131.jpg -''' - +""" del abspath, dirname, join diff --git a/tests/test_model.py b/tests/test_model.py index 8931d4a0..7d0bee6b 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -854,9 +854,7 @@ def test_should_dump_multiple_keys(): obj = m3u8.M3U8( playlists.PLAYLIST_WITH_ENCRYPTED_SEGMENTS_AND_IV_WITH_MULTIPLE_KEYS ) - expected = ( - playlists.PLAYLIST_WITH_ENCRYPTED_SEGMENTS_AND_IV_WITH_MULTIPLE_KEYS_SORTED.strip() - ) + expected = playlists.PLAYLIST_WITH_ENCRYPTED_SEGMENTS_AND_IV_WITH_MULTIPLE_KEYS_SORTED.strip() assert expected == obj.dumps().strip() @@ -885,9 +883,7 @@ def test_should_dump_complex_unencrypted_encrypted_keys_no_uri_attr(): obj = m3u8.M3U8( playlists.PLAYLIST_WITH_MULTIPLE_KEYS_UNENCRYPTED_AND_ENCRYPTED_NONE_AND_NO_URI_ATTR ) - expected = ( - playlists.PLAYLIST_WITH_MULTIPLE_KEYS_UNENCRYPTED_AND_ENCRYPTED_NONE_AND_NO_URI_ATTR.strip() - ) + expected = playlists.PLAYLIST_WITH_MULTIPLE_KEYS_UNENCRYPTED_AND_ENCRYPTED_NONE_AND_NO_URI_ATTR.strip() assert expected == obj.dumps().strip() @@ -1560,6 +1556,7 @@ def test_dump_should_work_for_variant_playlists_with_image_playlists(): assert expected == obj.dumps().strip() + def test_segment_media_sequence(): obj = m3u8.M3U8(playlists.SLIDING_WINDOW_PLAYLIST) assert [s.media_sequence for s in obj.segments] == [2680, 2681, 2682] diff --git a/tests/test_parser.py b/tests/test_parser.py index 0a61077a..913e7e4e 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -297,53 +297,57 @@ def test_should_parse_iframe_playlist(): def test_should_parse_variant_playlist_with_image_playlists(): data = m3u8.parse(playlists.VARIANT_PLAYLIST_WITH_IMAGE_PLAYLISTS) - image_playlists = list(data['image_playlists']) + image_playlists = list(data["image_playlists"]) - assert True is data['is_variant'] + assert True is data["is_variant"] assert 2 == len(image_playlists) - assert '320x180' == image_playlists[0]['image_stream_info']['resolution'] - assert 'jpeg' == image_playlists[0]['image_stream_info']['codecs'] - assert '5x2_320x180/320x180-5x2.m3u8' == image_playlists[0]['uri'] - assert '640x360' == image_playlists[1]['image_stream_info']['resolution'] - assert 'jpeg' == image_playlists[1]['image_stream_info']['codecs'] - assert '5x2_640x360/640x360-5x2.m3u8' == image_playlists[1]['uri'] + assert "320x180" == image_playlists[0]["image_stream_info"]["resolution"] + assert "jpeg" == image_playlists[0]["image_stream_info"]["codecs"] + assert "5x2_320x180/320x180-5x2.m3u8" == image_playlists[0]["uri"] + assert "640x360" == image_playlists[1]["image_stream_info"]["resolution"] + assert "jpeg" == image_playlists[1]["image_stream_info"]["codecs"] + assert "5x2_640x360/640x360-5x2.m3u8" == image_playlists[1]["uri"] + def test_should_parse_vod_image_playlist(): data = m3u8.parse(playlists.VOD_IMAGE_PLAYLIST) - assert True is data['is_images_only'] - assert 8 == len(data['tiles']) - assert 'preroll-ad-1.jpg' == data['segments'][0]['uri'] - assert '640x360' == data['tiles'][0]['resolution'] - assert '5x2' == data['tiles'][0]['layout'] - assert 6.006 == data['tiles'][0]['duration'] - assert 'byterange' not in data['tiles'][0] + assert True is data["is_images_only"] + assert 8 == len(data["tiles"]) + assert "preroll-ad-1.jpg" == data["segments"][0]["uri"] + assert "640x360" == data["tiles"][0]["resolution"] + assert "5x2" == data["tiles"][0]["layout"] + assert 6.006 == data["tiles"][0]["duration"] + assert "byterange" not in data["tiles"][0] + def test_should_parse_vod_image_playlist2(): data = m3u8.parse(playlists.VOD_IMAGE_PLAYLIST2) - assert True is data['is_images_only'] - assert '640x360' == data['tiles'][0]['resolution'] - assert '4x3' == data['tiles'][0]['layout'] - assert 2.002 == data['tiles'][0]['duration'] - assert 6 == len(data['tiles']) - assert 'promo_1.jpg' == data['segments'][0]['uri'] + assert True is data["is_images_only"] + assert "640x360" == data["tiles"][0]["resolution"] + assert "4x3" == data["tiles"][0]["layout"] + assert 2.002 == data["tiles"][0]["duration"] + assert 6 == len(data["tiles"]) + assert "promo_1.jpg" == data["segments"][0]["uri"] + def test_should_parse_live_image_playlist(): data = m3u8.parse(playlists.LIVE_IMAGE_PLAYLIST) - assert True is data['is_images_only'] - assert 10 == len(data['segments']) - assert 'content-123.jpg' == data['segments'][0]['uri'] - assert 'content-124.jpg' == data['segments'][1]['uri'] - assert 'content-125.jpg' == data['segments'][2]['uri'] - assert 'missing-midroll.jpg' == data['segments'][3]['uri'] - assert 'missing-midroll.jpg' == data['segments'][4]['uri'] - assert 'missing-midroll.jpg' == data['segments'][5]['uri'] - assert 'content-128.jpg' == data['segments'][6]['uri'] - assert 'content-129.jpg' == data['segments'][7]['uri'] - assert 'content-130.jpg' == data['segments'][8]['uri'] - assert 'content-131.jpg' == data['segments'][9]['uri'] + assert True is data["is_images_only"] + assert 10 == len(data["segments"]) + assert "content-123.jpg" == data["segments"][0]["uri"] + assert "content-124.jpg" == data["segments"][1]["uri"] + assert "content-125.jpg" == data["segments"][2]["uri"] + assert "missing-midroll.jpg" == data["segments"][3]["uri"] + assert "missing-midroll.jpg" == data["segments"][4]["uri"] + assert "missing-midroll.jpg" == data["segments"][5]["uri"] + assert "content-128.jpg" == data["segments"][6]["uri"] + assert "content-129.jpg" == data["segments"][7]["uri"] + assert "content-130.jpg" == data["segments"][8]["uri"] + assert "content-131.jpg" == data["segments"][9]["uri"] + def test_should_parse_playlist_using_byteranges(): data = m3u8.parse(playlists.PLAYLIST_USING_BYTERANGES) @@ -673,7 +677,7 @@ def parse_iptv_attributes(line, lineno, data, state): def test_tag_after_extinf(): parsed_playlist = m3u8.loads(playlists.IPTV_PLAYLIST_WITH_EARLY_EXTINF) actual = parsed_playlist.segments[0].uri - expected = 'http://str00.iptv.domain/7331/mpegts?token=longtokenhere' + expected = "http://str00.iptv.domain/7331/mpegts?token=longtokenhere" assert actual == expected diff --git a/tests/test_variant_m3u8.py b/tests/test_variant_m3u8.py index d5e05894..9e1001a0 100644 --- a/tests/test_variant_m3u8.py +++ b/tests/test_variant_m3u8.py @@ -372,42 +372,59 @@ def test_create_a_variant_m3u8_with_iframe_with_hdcp_level_playlists(): def test_create_a_variant_m3u8_with_two_playlists_and_two_image_playlists(): variant_m3u8 = m3u8.M3U8() - subtitles = m3u8.Media('english_sub.m3u8', 'SUBTITLES', 'subs', 'en', - 'English', 'YES', 'YES', 'NO', None) + subtitles = m3u8.Media( + "english_sub.m3u8", + "SUBTITLES", + "subs", + "en", + "English", + "YES", + "YES", + "NO", + None, + ) variant_m3u8.add_media(subtitles) low_playlist = m3u8.Playlist( - uri='video-800k.m3u8', - stream_info={'bandwidth': 800000, - 'program_id': 1, - 'resolution': '624x352', - 'codecs': 'avc1.4d001f, mp4a.40.5', - 'subtitles': 'subs'}, + uri="video-800k.m3u8", + stream_info={ + "bandwidth": 800000, + "program_id": 1, + "resolution": "624x352", + "codecs": "avc1.4d001f, mp4a.40.5", + "subtitles": "subs", + }, media=[subtitles], - base_uri='http://example.com/' + base_uri="http://example.com/", ) high_playlist = m3u8.Playlist( - uri='video-1200k.m3u8', - stream_info={'bandwidth': 1200000, - 'program_id': 1, - 'codecs': 'avc1.4d001f, mp4a.40.5', - 'subtitles': 'subs'}, + uri="video-1200k.m3u8", + stream_info={ + "bandwidth": 1200000, + "program_id": 1, + "codecs": "avc1.4d001f, mp4a.40.5", + "subtitles": "subs", + }, media=[subtitles], - base_uri='http://example.com/' + base_uri="http://example.com/", ) low_image_playlist = m3u8.ImagePlaylist( - uri='thumbnails-sd.m3u8', - image_stream_info={'bandwidth': 151288, - 'resolution': '320x160', - 'codecs': 'jpeg'}, - base_uri='http://example.com/' + uri="thumbnails-sd.m3u8", + image_stream_info={ + "bandwidth": 151288, + "resolution": "320x160", + "codecs": "jpeg", + }, + base_uri="http://example.com/", ) high_image_playlist = m3u8.ImagePlaylist( - uri='thumbnails-hd.m3u8', - image_stream_info={'bandwidth': 193350, - 'resolution': '640x320', - 'codecs': 'jpeg'}, - base_uri='http://example.com/' + uri="thumbnails-hd.m3u8", + image_stream_info={ + "bandwidth": 193350, + "resolution": "640x320", + "codecs": "jpeg", + }, + base_uri="http://example.com/", ) variant_m3u8.add_playlist(low_playlist) From 306da4c6c464c0781adb38a8c25602b26b487caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Tue, 12 Dec 2023 20:11:47 -0300 Subject: [PATCH 15/17] Release new version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f1c01d4a..4cd22e8c 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name="m3u8", author="Globo.com", - version="3.6.0", + version="4.0.0", license="MIT", zip_safe=False, include_package_data=True, From 3201055c066e799d7b74a041d6aecfa34cdf099b Mon Sep 17 00:00:00 2001 From: Bo Bayles Date: Tue, 31 Oct 2023 14:45:03 -0500 Subject: [PATCH 16/17] Support EXT-X-CUE-OUT:Elapsed/Duration format --- m3u8/parser.py | 11 +++++++++++ tests/playlists.py | 20 ++++++++++++++++++++ tests/test_model.py | 14 ++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/m3u8/parser.py b/m3u8/parser.py index 1d04c2f5..e3ee37a5 100644 --- a/m3u8/parser.py +++ b/m3u8/parser.py @@ -464,6 +464,17 @@ def _parse_cueout_cont(line, state): if len(elements) != 2: return + # EXT-X-CUE-OUT-CONT:2.436/120 style + res = re.match( + r"^[-+]?([0-9]+(\.[0-9]+)?|\.[0-9]+)/[-+]?([0-9]+(\.[0-9]+)?|\.[0-9]+)$", + elements[1] + ) + if res: + state["current_cue_out_elapsedtime"] = res.group(1) + state["current_cue_out_duration"] = res.group(3) + return + + # EXT-X-CUE-OUT-CONT:ElapsedTime=10,Duration=60,SCTE35=... style cue_info = _parse_attribute_list( protocol.ext_x_cue_out_cont, line, diff --git a/tests/playlists.py b/tests/playlists.py index 6b850493..32ed41b9 100755 --- a/tests/playlists.py +++ b/tests/playlists.py @@ -737,6 +737,26 @@ master2500_47234.ts """ +CUE_OUT_CONT_ALT_PLAYLIST = """ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:7 +#EXT-X-MEDIA-SEQUENCE:19980226 +#EXT-X-DISCONTINUITY-SEQUENCE:1 +#EXT-X-CUE-OUT:119.987 +#EXTINF:2.000, +segment_19980226.ts +#EXT-X-CUE-OUT-CONT:2/120 +#EXTINF:6.000, +segment_19980227.ts +#EXT-X-CUE-OUT-CONT:8/120.0 +#EXTINF:6.001, +segment_19980228.ts +#EXT-X-CUE-OUT-CONT:14.001/120.0 +#EXTINF:6.001, +segment_19980229.ts +""" + CUE_OUT_ENVIVIO_PLAYLIST = """ #EXTM3U #EXT-X-VERSION:3 diff --git a/tests/test_model.py b/tests/test_model.py index 7d0bee6b..1eae3d4a 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -225,6 +225,20 @@ def test_segment_elemental_scte35_attribute(): ) +def test_segment_cue_out_cont_alt(): + obj = m3u8.M3U8(playlists.CUE_OUT_CONT_ALT_PLAYLIST) + segments = obj.segments + + assert segments[1].scte35_elapsedtime == '2' + assert segments[1].scte35_duration == '120' + + assert segments[2].scte35_elapsedtime == '8' + assert segments[2].scte35_duration == '120.0' + + assert segments[3].scte35_elapsedtime == '14.001' + assert segments[3].scte35_duration == '120.0' + + def test_segment_envivio_scte35_attribute(): obj = m3u8.M3U8(playlists.CUE_OUT_ENVIVIO_PLAYLIST) segments = obj.segments From 3c352ffd738cfa630c11a6920a9fbc605fc2a047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Mon, 1 Apr 2024 14:40:36 -0300 Subject: [PATCH 17/17] Release new version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4cd22e8c..3a4a9ac4 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name="m3u8", author="Globo.com", - version="4.0.0", + version="4.1.0", license="MIT", zip_safe=False, include_package_data=True,