diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 52e7024b..1b400519 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -8,12 +8,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 + - uses: astral-sh/ruff-action@v1.1.0 ruff_format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 + - uses: astral-sh/ruff-action@v1.1.0 with: args: format --check --diff diff --git a/m3u8/__init__.py b/m3u8/__init__.py index d11dbf30..f6fc5174 100644 --- a/m3u8/__init__.py +++ b/m3u8/__init__.py @@ -90,7 +90,8 @@ def load( Retrieves the content from a given URI and returns a M3U8 object. Raises ValueError if invalid content or IOError if request fails. """ - if urlsplit(uri).scheme: + base_uri_parts = urlsplit(uri) + if base_uri_parts.scheme and base_uri_parts.netloc: content, base_uri = http_client.download(uri, timeout, headers, verify_ssl) return M3U8(content, base_uri=base_uri, custom_tags_parser=custom_tags_parser) else: diff --git a/m3u8/mixins.py b/m3u8/mixins.py index 162b82a1..40ffb2fc 100644 --- a/m3u8/mixins.py +++ b/m3u8/mixins.py @@ -9,8 +9,10 @@ def absolute_uri(self): return None ret = urljoin(self.base_uri, self.uri) - if self.base_uri and (not urlsplit(self.base_uri).scheme): - return ret + if self.base_uri: + base_uri_parts = urlsplit(self.base_uri) + if (not base_uri_parts.scheme) and (not base_uri_parts.netloc): + return ret if not urlsplit(ret).scheme: raise ValueError("There can not be `absolute_uri` with no `base_uri` set") diff --git a/m3u8/model.py b/m3u8/model.py index 7f293d1f..9bc16071 100644 --- a/m3u8/model.py +++ b/m3u8/model.py @@ -1294,7 +1294,7 @@ def __str__(self): class RenditionReport(BasePathMixin): - def __init__(self, base_uri, uri, last_msn, last_part=None): + def __init__(self, base_uri, uri, last_msn=None, last_part=None): self.base_uri = base_uri self.uri = uri self.last_msn = last_msn @@ -1303,7 +1303,8 @@ def __init__(self, base_uri, uri, last_msn, last_part=None): def dumps(self): report = [] report.append("URI=" + quoted(self.uri)) - report.append("LAST-MSN=" + str(self.last_msn)) + if self.last_msn is not None: + report.append("LAST-MSN=" + str(self.last_msn)) if self.last_part is not None: report.append("LAST-PART=" + str(self.last_part)) diff --git a/tests/playlists.py b/tests/playlists.py index c3403dbc..b2435d2e 100755 --- a/tests/playlists.py +++ b/tests/playlists.py @@ -1111,6 +1111,58 @@ #EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=3 """ +LOW_LATENCY_OMITTED_ATTRIBUTES = """ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:2 +#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=2.171 +#EXT-X-PART-INF:PART-TARGET=1.034 +#EXT-X-MAP:URI="init_data.m4s" +#EXT-X-MEDIA-SEQUENCE:6342 +#EXT-X-PROGRAM-DATE-TIME:2024-09-24T15:31:57.350+00:00 +#EXTINF:2, +chunk_6342.m4s +#EXT-X-PROGRAM-DATE-TIME:2024-09-24T15:31:59.350+00:00 +#EXTINF:2, +chunk_6343.m4s +#EXT-X-PROGRAM-DATE-TIME:2024-09-24T15:32:01.350+00:00 +#EXTINF:2, +chunk_6344.m4s +#EXT-X-PROGRAM-DATE-TIME:2024-09-24T15:32:03.350+00:00 +#EXTINF:2, +chunk_6345.m4s +#EXT-X-PROGRAM-DATE-TIME:2024-09-24T15:32:05.350+00:00 +#EXTINF:2, +chunk_6346.m4s +#EXT-X-PROGRAM-DATE-TIME:2024-09-24T15:32:07.350+00:00 +#EXTINF:2, +chunk_6347.m4s +#EXT-X-PROGRAM-DATE-TIME:2024-09-24T15:32:09.350+00:00 +#EXTINF:2, +chunk_6348.m4s +#EXT-X-PROGRAM-DATE-TIME:2024-09-24T15:32:11.350+00:00 +#EXT-X-PART:DURATION=1,URI="chunk_6349.0.m4s",INDEPENDENT=YES +#EXT-X-PART:DURATION=1,URI="chunk_6349.1.m4s" +#EXTINF:2, +chunk_6349.m4s +#EXT-X-PROGRAM-DATE-TIME:2024-09-24T15:32:13.350+00:00 +#EXT-X-PART:DURATION=1,URI="chunk_6350.0.m4s",INDEPENDENT=YES +#EXT-X-PART:DURATION=1,URI="chunk_6350.1.m4s" +#EXTINF:2, +chunk_6350.m4s +#EXT-X-PROGRAM-DATE-TIME:2024-09-24T15:32:15.350+00:00 +#EXT-X-PART:DURATION=1,URI="chunk_6351.0.m4s?skid=default&signature=NjZmYzFjODBfYzY3NGRlODc4Zjk1MjM1OGNmMmE3NjhiM2E2NTUyNGI1Y2JiYzMyZDU5YTFjNTQzODI2MjI5ZTllZmNhMDZmNQ==&zone=1",INDEPENDENT=YES +#EXT-X-PART:DURATION=1,URI="chunk_6351.1.m4s?skid=default&signature=NjZmYzFjODBfMDcwMjA0OTZlMTE3Y2RiN2VjOGY2YjE2MDE2NTAwZThlN2Q3NjUyZTAzM2YxZTZlZmFlZTg1ZThmZWEyZmQ4Ng==&zone=1" +#EXTINF:2, +chunk_6350.m4s +#EXT-X-PRELOAD-HINT:TYPE=PART,URI="chunk_6352.0.m4s?skid=default&signature=NjZmYzFjODBfMzkyZmNiOWNjNmY5N2EwN2QwNTU3YTA3M2Q0ZTRlMWU2YjliZDMyM2Y0MTRmYTY5OTdhODIyMmIwY2QwOWY1NQ==&zone=1" +#EXT-X-RENDITION-REPORT:URI="rendition_1.m3u8" +#EXT-X-RENDITION-REPORT:URI="rendition_2.m3u8" +#EXT-X-RENDITION-REPORT:URI="rendition_3.m3u8" +#EXT-X-RENDITION-REPORT:URI="rendition_4.m3u8" +#EXT-X-RENDITION-REPORT:URI="rendition_5.m3u8" +""" + LOW_LATENCY_WITH_PRELOAD_AND_BYTERANGES_PLAYLIST = """ #EXTM3U #EXTINF:4.08, @@ -1524,5 +1576,15 @@ content-131.jpg """ +WINDOWS_PLAYLIST = r"""\ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-TARGETDURATION:10 +#EXT-X-MEDIA-SEQUENCE:55119 +#EXT-X-PROGRAM-DATE-TIME:2024-10-11T09:53:30.001Z +#EXTINF:9.600, +C:\HLS Video\test1.ts +""" del abspath, dirname, join diff --git a/tests/test_loader.py b/tests/test_loader.py index 509b7714..db114c4e 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -5,8 +5,11 @@ import os import socket import urllib.parse -import m3u8 +import unittest.mock + import pytest + +import m3u8 import playlists @@ -146,3 +149,14 @@ def test_raise_timeout_exception_if_timeout_happens_when_loading_from_uri(): assert True else: assert False + + +def test_windows_paths(): + file_path = "C:\\HLS Video\test.m3ui8" + with unittest.mock.patch("builtins.open") as mock_open: + mock_open.return_value.__enter__.return_value.read.return_value = ( + playlists.WINDOWS_PLAYLIST + ) + obj = m3u8.load(file_path) + assert obj.segments[0].uri == "C:\\HLS Video\\test1.ts" + assert obj.segments[0].absolute_uri == "C:\\HLS Video\\test1.ts" diff --git a/tests/test_model.py b/tests/test_model.py index abeb61dc..8185fe38 100755 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1345,6 +1345,14 @@ def test_ll_playlist(): assert obj.preload_hint.base_uri == "http://localhost/test_base_uri" +def test_ll_playlist_omitted_attributes(): + # RFC 8216 4.4.5.4 states that even the required attribute LAST-MSN + # can be omitted in certain conditions. + obj = m3u8.M3U8(playlists.LOW_LATENCY_OMITTED_ATTRIBUTES) + text = obj.dumps() + assert '#EXT-X-RENDITION-REPORT:URI="rendition_1.m3u8"\n' in text + + def test_add_rendition_report_to_playlist(): obj = m3u8.M3U8()