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)