Skip to content

Commit

Permalink
feat: add description and how to fix errors
Browse files Browse the repository at this point in the history
  • Loading branch information
davigps committed Apr 8, 2024
1 parent 8633dee commit 6543b7a
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 103 deletions.
7 changes: 5 additions & 2 deletions m3u8/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ def parse(content, strict=False, custom_tags_parser=None):

lines = string_to_lines(content)
if strict:
version_matching.validate(lines)
found_errors = version_matching.validate(lines)

if len(found_errors) > 0:
raise Exception(found_errors)

lineno = 0
for line in lines:
Expand Down Expand Up @@ -471,7 +474,7 @@ def _parse_cueout_cont(line, state):
# 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]
elements[1],
)
if res:
state["current_cue_out_elapsedtime"] = res.group(1)
Expand Down
42 changes: 19 additions & 23 deletions m3u8/version_matching.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
from typing import Callable, List
from typing import List

from m3u8 import protocol
from m3u8.version_matching_rules import *


class VersionMatchingError(Exception):
def __init__(self, lineno, line):
self.lineno = lineno
self.line = line

def __str__(self):
return f"Version matching error at line {self.lineno}: {self.line}"
from m3u8.version_matching_rules import VersionMatchingError, available_rules


def get_version(file_lines: List[str]):
Expand All @@ -22,22 +13,27 @@ def get_version(file_lines: List[str]):
return None


def valid_in_all_rules(line_number: int, line: str, version: float):
rules: List[Callable[[str, float], bool]] = [
valid_iv_in_EXT_X_KEY,
valid_floating_point_EXTINF,
valid_EXT_X_BYTERANGE_or_EXT_X_I_FRAMES_ONLY,
]
def valid_in_all_rules(
line_number: int, line: str, version: float
) -> List[VersionMatchingError]:
errors = []
for rule in available_rules:
validator = rule(version, line_number, line)

for rule in rules:
if not rule(line, version):
raise VersionMatchingError(line_number, line)
if not validator.validate():
errors.append(validator.get_error())

return errors

def validate(file_lines: List[str]):

def validate(file_lines: List[str]) -> List[VersionMatchingError]:
found_version = get_version(file_lines)
if found_version is None:
return
return []

errors = []
for number, line in enumerate(file_lines):
valid_in_all_rules(number, line, found_version)
errors_in_line = valid_in_all_rules(number, line, found_version)
errors.extend(errors_in_line)

return errors
119 changes: 92 additions & 27 deletions m3u8/version_matching_rules.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,109 @@
from dataclasses import dataclass
from typing import List

from m3u8 import protocol


# You must use at least protocol version 2 if you have IV in EXT-X-KEY.
def valid_iv_in_EXT_X_KEY(line: str, version: float):
if not protocol.ext_x_key in line:
return True
@dataclass
class VersionMatchingError(Exception):
line_number: int
line: str
how_to_fix: str = "Please fix the version matching error."
description: str = "There is a version matching error in the file."

def __str__(self):
return (
"Version matching error found in the file when parsing in strict mode.\n"
f"Line {self.line_number}: {self.description}\n"
f"Line content: {self.line}\n"
f"How to fix: {self.how_to_fix}"
"\n"
)


class VersionMatchRuleBase:
description: str = ""
how_to_fix: str = ""
version: float
line_number: int
line: str

def __init__(self, version: float, line_number: int, line: str) -> None:
self.version = version
self.line_number = line_number
self.line = line

def validate(self):
raise NotImplementedError

def get_error(self):
return VersionMatchingError(
line_number=self.line_number,
line=self.line,
description=self.description,
how_to_fix=self.how_to_fix,
)


if "IV" in line:
return version >= 2
class ValidIVInEXTXKEY(VersionMatchRuleBase):
description = (
"You must use at least protocol version 2 if you have IV in EXT-X-KEY."
)
how_to_fix = "Change the protocol version to 2 or higher."

return True
def validate(self):
if not protocol.ext_x_key in self.line:
return True

if "IV" in self.line:
return self.version >= 2

# You must use at least protocol version 3 if you have floating point EXTINF duration values.
def valid_floating_point_EXTINF(line: str, version: float):
if not protocol.extinf in line:
return True

chunks = line.replace(protocol.extinf + ":", "").split(",", 1)
duration = chunks[0]

def is_number(value: str):
try:
float(value)
class ValidFloatingPointEXTINF(VersionMatchRuleBase):
description = "You must use at least protocol version 3 if you have floating point EXTINF duration values."
how_to_fix = "Change the protocol version to 3 or higher."

def validate(self):
if not protocol.extinf in self.line:
return True
except:
return False

def is_floating_number(value: str):
return is_number(value) and "." in value
chunks = self.line.replace(protocol.extinf + ":", "").split(",", 1)
duration = chunks[0]

if is_floating_number(duration):
return version >= 3
def is_number(value: str):
try:
float(value)
return True
except:
return False

return is_number(duration)
def is_floating_number(value: str):
return is_number(value) and "." in value

if is_floating_number(duration):
return self.version >= 3

return is_number(duration)


class ValidEXTXBYTERANGEOrEXTXIFRAMESONLY(VersionMatchRuleBase):
description = "You must use at least protocol version 4 if you have EXT-X-BYTERANGE or EXT-X-IFRAME-ONLY."
how_to_fix = "Change the protocol version to 4 or higher."

def validate(self):
if (
not protocol.ext_x_byterange in self.line
and not protocol.ext_i_frames_only in self.line
):
return True

return self.version >= 4

# You must use at least protocol version 4 if you have EXT-X-BYTERANGE or EXT-X-IFRAME-ONLY.
def valid_EXT_X_BYTERANGE_or_EXT_X_I_FRAMES_ONLY(line: str, version: float):
if not protocol.ext_x_byterange in line and not protocol.ext_i_frames_only in line:
return True

return version >= 4
available_rules: List[type[VersionMatchRuleBase]] = [
ValidIVInEXTXKEY,
ValidFloatingPointEXTINF,
ValidEXTXBYTERANGEOrEXTXIFRAMESONLY,
]
7 changes: 5 additions & 2 deletions tests/test_strict_validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

import m3u8
import m3u8.version_matching_rules


@pytest.mark.xfail
Expand Down Expand Up @@ -33,9 +34,11 @@ def test_should_fail_if_EXT_X_MEDIA_SEQUENCE_is_not_a_number():
assert 0


@pytest.mark.xfail
def test_should_validate_supported_EXT_X_VERSION():
m3u8.parse(invalid_versioned_playlists.M3U8_RULE_IV)
with pytest.raises(
Exception,
):
m3u8.parse(invalid_versioned_playlists.M3U8_RULE_IV, strict=True)


@pytest.mark.xfail
Expand Down
Loading

0 comments on commit 6543b7a

Please sign in to comment.