second commit
This commit is contained in:
104
env/lib/python3.11/site-packages/m3u8/__init__.py
vendored
Normal file
104
env/lib/python3.11/site-packages/m3u8/__init__.py
vendored
Normal file
@ -0,0 +1,104 @@
|
||||
# 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.
|
||||
|
||||
import os
|
||||
from urllib.parse import urljoin, urlsplit
|
||||
|
||||
from m3u8.httpclient import DefaultHTTPClient
|
||||
from m3u8.model import (
|
||||
M3U8,
|
||||
ContentSteering,
|
||||
DateRange,
|
||||
DateRangeList,
|
||||
IFramePlaylist,
|
||||
ImagePlaylist,
|
||||
Key,
|
||||
Media,
|
||||
MediaList,
|
||||
PartialSegment,
|
||||
PartialSegmentList,
|
||||
PartInformation,
|
||||
Playlist,
|
||||
PlaylistList,
|
||||
PreloadHint,
|
||||
RenditionReport,
|
||||
RenditionReportList,
|
||||
Segment,
|
||||
SegmentList,
|
||||
ServerControl,
|
||||
Skip,
|
||||
Start,
|
||||
Tiles,
|
||||
)
|
||||
from m3u8.parser import ParseError, parse
|
||||
|
||||
__all__ = (
|
||||
"M3U8",
|
||||
"Segment",
|
||||
"SegmentList",
|
||||
"PartialSegment",
|
||||
"PartialSegmentList",
|
||||
"Key",
|
||||
"Playlist",
|
||||
"IFramePlaylist",
|
||||
"Media",
|
||||
"MediaList",
|
||||
"PlaylistList",
|
||||
"Start",
|
||||
"RenditionReport",
|
||||
"RenditionReportList",
|
||||
"ServerControl",
|
||||
"Skip",
|
||||
"PartInformation",
|
||||
"PreloadHint",
|
||||
"DateRange",
|
||||
"DateRangeList",
|
||||
"ContentSteering",
|
||||
"ImagePlaylist",
|
||||
"Tiles",
|
||||
"loads",
|
||||
"load",
|
||||
"parse",
|
||||
"ParseError",
|
||||
)
|
||||
|
||||
|
||||
def loads(content, uri=None, custom_tags_parser=None):
|
||||
"""
|
||||
Given a string with a m3u8 content, returns a M3U8 object.
|
||||
Optionally parses a uri to set a correct base_uri on the M3U8 object.
|
||||
Raises ValueError if invalid content
|
||||
"""
|
||||
|
||||
if uri is None:
|
||||
return M3U8(content, custom_tags_parser=custom_tags_parser)
|
||||
else:
|
||||
base_uri = urljoin(uri, ".")
|
||||
return M3U8(content, base_uri=base_uri, custom_tags_parser=custom_tags_parser)
|
||||
|
||||
|
||||
def load(
|
||||
uri,
|
||||
timeout=None,
|
||||
headers={},
|
||||
custom_tags_parser=None,
|
||||
http_client=DefaultHTTPClient(),
|
||||
verify_ssl=True,
|
||||
):
|
||||
"""
|
||||
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:
|
||||
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:
|
||||
return _load_from_file(uri, custom_tags_parser)
|
||||
|
||||
|
||||
def _load_from_file(uri, custom_tags_parser=None):
|
||||
with open(uri, encoding="utf8") as fileobj:
|
||||
raw_content = fileobj.read().strip()
|
||||
base_uri = os.path.dirname(uri)
|
||||
return M3U8(raw_content, base_uri=base_uri, custom_tags_parser=custom_tags_parser)
|
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/httpclient.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/httpclient.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/mixins.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/mixins.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/model.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/model.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/parser.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/parser.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/protocol.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/protocol.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/version_matching.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/version_matching.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/version_matching_rules.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/m3u8/__pycache__/version_matching_rules.cpython-311.pyc
vendored
Normal file
Binary file not shown.
36
env/lib/python3.11/site-packages/m3u8/httpclient.py
vendored
Normal file
36
env/lib/python3.11/site-packages/m3u8/httpclient.py
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
import gzip
|
||||
import ssl
|
||||
import urllib.request
|
||||
from urllib.parse import urljoin
|
||||
|
||||
|
||||
class DefaultHTTPClient:
|
||||
def __init__(self, proxies=None):
|
||||
self.proxies = proxies
|
||||
|
||||
def download(self, uri, timeout=None, headers={}, verify_ssl=True):
|
||||
proxy_handler = urllib.request.ProxyHandler(self.proxies)
|
||||
https_handler = HTTPSHandler(verify_ssl=verify_ssl)
|
||||
opener = urllib.request.build_opener(proxy_handler, https_handler)
|
||||
opener.addheaders = headers.items()
|
||||
resource = opener.open(uri, timeout=timeout)
|
||||
base_uri = urljoin(resource.geturl(), ".")
|
||||
|
||||
if resource.info().get("Content-Encoding") == "gzip":
|
||||
content = gzip.decompress(resource.read()).decode(
|
||||
resource.headers.get_content_charset(failobj="utf-8")
|
||||
)
|
||||
else:
|
||||
content = resource.read().decode(
|
||||
resource.headers.get_content_charset(failobj="utf-8")
|
||||
)
|
||||
return content, base_uri
|
||||
|
||||
|
||||
class HTTPSHandler:
|
||||
def __new__(self, verify_ssl=True):
|
||||
context = ssl.create_default_context()
|
||||
if not verify_ssl:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
return urllib.request.HTTPSHandler(context=context)
|
50
env/lib/python3.11/site-packages/m3u8/mixins.py
vendored
Normal file
50
env/lib/python3.11/site-packages/m3u8/mixins.py
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
from os.path import dirname
|
||||
from urllib.parse import urljoin, urlsplit
|
||||
|
||||
|
||||
class BasePathMixin:
|
||||
@property
|
||||
def absolute_uri(self):
|
||||
if self.uri is None:
|
||||
return None
|
||||
|
||||
ret = urljoin(self.base_uri, self.uri)
|
||||
if self.base_uri and (not urlsplit(self.base_uri).scheme):
|
||||
return ret
|
||||
|
||||
if not urlsplit(ret).scheme:
|
||||
raise ValueError("There can not be `absolute_uri` with no `base_uri` set")
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
def base_path(self):
|
||||
if self.uri is None:
|
||||
return None
|
||||
return dirname(self.get_path_from_uri())
|
||||
|
||||
def get_path_from_uri(self):
|
||||
"""Some URIs have a slash in the query string."""
|
||||
return self.uri.split("?")[0]
|
||||
|
||||
@base_path.setter
|
||||
def base_path(self, newbase_path):
|
||||
if self.uri is not None:
|
||||
if not self.base_path:
|
||||
self.uri = f"{newbase_path}/{self.uri}"
|
||||
else:
|
||||
self.uri = self.uri.replace(self.base_path, newbase_path)
|
||||
|
||||
|
||||
class GroupedBasePathMixin:
|
||||
def _set_base_uri(self, new_base_uri):
|
||||
for item in self:
|
||||
item.base_uri = new_base_uri
|
||||
|
||||
base_uri = property(None, _set_base_uri)
|
||||
|
||||
def _set_base_path(self, newbase_path):
|
||||
for item in self:
|
||||
item.base_path = newbase_path
|
||||
|
||||
base_path = property(None, _set_base_path)
|
1661
env/lib/python3.11/site-packages/m3u8/model.py
vendored
Normal file
1661
env/lib/python3.11/site-packages/m3u8/model.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
795
env/lib/python3.11/site-packages/m3u8/parser.py
vendored
Normal file
795
env/lib/python3.11/site-packages/m3u8/parser.py
vendored
Normal file
@ -0,0 +1,795 @@
|
||||
# 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.
|
||||
|
||||
import itertools
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
try:
|
||||
from backports.datetime_fromisoformat import MonkeyPatch
|
||||
|
||||
MonkeyPatch.patch_fromisoformat()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
from m3u8 import protocol, version_matching
|
||||
|
||||
"""
|
||||
http://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.2
|
||||
http://stackoverflow.com/questions/2785755/how-to-split-but-ignore-separators-in-quoted-strings-in-python
|
||||
"""
|
||||
ATTRIBUTELISTPATTERN = re.compile(r"""((?:[^,"']|"[^"]*"|'[^']*')+)""")
|
||||
|
||||
|
||||
def cast_date_time(value):
|
||||
return datetime.fromisoformat(value)
|
||||
|
||||
|
||||
def format_date_time(value, **kwargs):
|
||||
return value.isoformat(**kwargs)
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
def __init__(self, lineno, line):
|
||||
self.lineno = lineno
|
||||
self.line = line
|
||||
|
||||
def __str__(self):
|
||||
return "Syntax error in manifest on line %d: %s" % (self.lineno, self.line)
|
||||
|
||||
|
||||
def parse(content, strict=False, custom_tags_parser=None):
|
||||
"""
|
||||
Given a M3U8 playlist content returns a dictionary with all data found
|
||||
"""
|
||||
data = {
|
||||
"media_sequence": 0,
|
||||
"is_variant": False,
|
||||
"is_endlist": False,
|
||||
"is_i_frames_only": False,
|
||||
"is_independent_segments": False,
|
||||
"is_images_only": False,
|
||||
"playlist_type": None,
|
||||
"playlists": [],
|
||||
"segments": [],
|
||||
"iframe_playlists": [],
|
||||
"image_playlists": [],
|
||||
"tiles": [],
|
||||
"media": [],
|
||||
"keys": [],
|
||||
"rendition_reports": [],
|
||||
"skip": {},
|
||||
"part_inf": {},
|
||||
"session_data": [],
|
||||
"session_keys": [],
|
||||
"segment_map": [],
|
||||
}
|
||||
|
||||
state = {
|
||||
"expect_segment": False,
|
||||
"expect_playlist": False,
|
||||
"current_key": None,
|
||||
"current_segment_map": None,
|
||||
}
|
||||
|
||||
lines = string_to_lines(content)
|
||||
if strict:
|
||||
found_errors = version_matching.validate(lines)
|
||||
|
||||
if len(found_errors) > 0:
|
||||
raise Exception(found_errors)
|
||||
|
||||
for lineno, line in enumerate(lines, 1):
|
||||
line = line.strip()
|
||||
parse_kwargs = {
|
||||
"line": line,
|
||||
"lineno": lineno,
|
||||
"data": data,
|
||||
"state": state,
|
||||
"strict": strict,
|
||||
}
|
||||
|
||||
# Call custom parser if needed
|
||||
if line.startswith("#") and callable(custom_tags_parser):
|
||||
go_to_next_line = custom_tags_parser(line, lineno, data, state)
|
||||
|
||||
# Do not try to parse other standard tags on this line if custom_tags_parser
|
||||
# function returns `True`
|
||||
if go_to_next_line:
|
||||
continue
|
||||
|
||||
if line.startswith(protocol.ext_x_byterange):
|
||||
_parse_byterange(**parse_kwargs)
|
||||
continue
|
||||
|
||||
elif line.startswith(protocol.ext_x_bitrate):
|
||||
_parse_bitrate(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_targetduration):
|
||||
_parse_targetduration(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_media_sequence):
|
||||
_parse_media_sequence(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_discontinuity_sequence):
|
||||
_parse_discontinuity_sequence(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_program_date_time):
|
||||
_parse_program_date_time(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_discontinuity):
|
||||
_parse_discontinuity(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_cue_out_cont):
|
||||
_parse_cueout_cont(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_cue_out):
|
||||
_parse_cueout(**parse_kwargs)
|
||||
|
||||
elif line.startswith(f"{protocol.ext_oatcls_scte35}:"):
|
||||
_parse_oatcls_scte35(**parse_kwargs)
|
||||
|
||||
elif line.startswith(f"{protocol.ext_x_asset}:"):
|
||||
_parse_asset(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_cue_in):
|
||||
_parse_cue_in(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_cue_span):
|
||||
_parse_cue_span(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_version):
|
||||
_parse_version(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_allow_cache):
|
||||
_parse_allow_cache(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_key):
|
||||
_parse_key(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.extinf):
|
||||
_parse_extinf(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_stream_inf):
|
||||
_parse_stream_inf(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_i_frame_stream_inf):
|
||||
_parse_i_frame_stream_inf(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_media):
|
||||
_parse_media(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_playlist_type):
|
||||
_parse_playlist_type(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_i_frames_only):
|
||||
_parse_i_frames_only(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_is_independent_segments):
|
||||
_parse_is_independent_segments(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_endlist):
|
||||
_parse_endlist(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_map):
|
||||
_parse_x_map(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_start):
|
||||
_parse_start(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_server_control):
|
||||
_parse_server_control(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_part_inf):
|
||||
_parse_part_inf(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_rendition_report):
|
||||
_parse_rendition_report(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_part):
|
||||
_parse_part(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_skip):
|
||||
_parse_skip(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_session_data):
|
||||
_parse_session_data(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_session_key):
|
||||
_parse_session_key(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_preload_hint):
|
||||
_parse_preload_hint(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_daterange):
|
||||
_parse_daterange(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_gap):
|
||||
_parse_gap(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_content_steering):
|
||||
_parse_content_steering(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_image_stream_inf):
|
||||
_parse_image_stream_inf(**parse_kwargs)
|
||||
|
||||
elif line.startswith(protocol.ext_x_images_only):
|
||||
_parse_is_images_only(**parse_kwargs)
|
||||
elif line.startswith(protocol.ext_x_tiles):
|
||||
_parse_tiles(**parse_kwargs)
|
||||
|
||||
# #EXTM3U should be present.
|
||||
elif line.startswith(protocol.ext_m3u):
|
||||
pass
|
||||
|
||||
# Blank lines are ignored.
|
||||
elif line.strip() == "":
|
||||
pass
|
||||
|
||||
# Lines that don't start with # are either segments or playlists.
|
||||
elif (not line.startswith("#")) and (state["expect_segment"]):
|
||||
_parse_ts_chunk(**parse_kwargs)
|
||||
|
||||
elif (not line.startswith("#")) and (state["expect_playlist"]):
|
||||
_parse_variant_playlist(**parse_kwargs)
|
||||
|
||||
# Lines that haven't been recognized by any of the parsers above are illegal
|
||||
# in strict mode.
|
||||
elif strict:
|
||||
raise ParseError(lineno, line)
|
||||
|
||||
# Handle remaining partial segments.
|
||||
if "segment" in state:
|
||||
data["segments"].append(state.pop("segment"))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _parse_key(line, data, state, **kwargs):
|
||||
params = ATTRIBUTELISTPATTERN.split(line.replace(protocol.ext_x_key + ":", ""))[
|
||||
1::2
|
||||
]
|
||||
key = {}
|
||||
for param in params:
|
||||
name, value = param.split("=", 1)
|
||||
key[normalize_attribute(name)] = remove_quotes(value)
|
||||
|
||||
state["current_key"] = key
|
||||
if key not in data["keys"]:
|
||||
data["keys"].append(key)
|
||||
|
||||
|
||||
def _parse_extinf(line, state, lineno, strict, **kwargs):
|
||||
chunks = line.replace(protocol.extinf + ":", "").split(",", 1)
|
||||
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)
|
||||
state["segment"]["title"] = title
|
||||
state["expect_segment"] = True
|
||||
|
||||
|
||||
def _parse_ts_chunk(line, data, state, **kwargs):
|
||||
segment = state.pop("segment")
|
||||
if state.get("program_date_time"):
|
||||
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"])
|
||||
segment["uri"] = line
|
||||
segment["cue_in"] = state.pop("cue_in", False)
|
||||
segment["cue_out"] = state.pop("cue_out", False)
|
||||
segment["cue_out_start"] = state.pop("cue_out_start", False)
|
||||
segment["cue_out_explicitly_duration"] = state.pop(
|
||||
"cue_out_explicitly_duration", False
|
||||
)
|
||||
|
||||
scte_op = state.get if segment["cue_out"] else state.pop
|
||||
segment["scte35"] = scte_op("current_cue_out_scte35", None)
|
||||
segment["oatcls_scte35"] = scte_op("current_cue_out_oatcls_scte35", None)
|
||||
segment["scte35_duration"] = scte_op("current_cue_out_duration", None)
|
||||
segment["scte35_elapsedtime"] = scte_op("current_cue_out_elapsedtime", None)
|
||||
segment["asset_metadata"] = scte_op("asset_metadata", None)
|
||||
|
||||
segment["discontinuity"] = state.pop("discontinuity", False)
|
||||
if state.get("current_key"):
|
||||
segment["key"] = state["current_key"]
|
||||
else:
|
||||
# For unencrypted segments, the initial key would be None
|
||||
if None not in data["keys"]:
|
||||
data["keys"].append(None)
|
||||
if state.get("current_segment_map"):
|
||||
segment["init_section"] = state["current_segment_map"]
|
||||
segment["dateranges"] = state.pop("dateranges", None)
|
||||
segment["gap_tag"] = state.pop("gap", None)
|
||||
data["segments"].append(segment)
|
||||
state["expect_segment"] = False
|
||||
|
||||
|
||||
def _parse_attribute_list(prefix, line, attribute_parser, default_parser=None):
|
||||
params = ATTRIBUTELISTPATTERN.split(line.replace(prefix + ":", ""))[1::2]
|
||||
|
||||
attributes = {}
|
||||
if not line.startswith(prefix + ":"):
|
||||
return attributes
|
||||
|
||||
for param in params:
|
||||
param_parts = param.split("=", 1)
|
||||
if len(param_parts) == 1:
|
||||
name = ""
|
||||
value = param_parts[0]
|
||||
else:
|
||||
name, value = param_parts
|
||||
|
||||
name = normalize_attribute(name)
|
||||
if name in attribute_parser:
|
||||
value = attribute_parser[name](value)
|
||||
elif default_parser is not None:
|
||||
value = default_parser(value)
|
||||
|
||||
attributes[name] = value
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def _parse_stream_inf(line, data, state, **kwargs):
|
||||
state["expect_playlist"] = True
|
||||
data["is_variant"] = True
|
||||
data["media_sequence"] = None
|
||||
attribute_parser = remove_quotes_parser(
|
||||
"codecs",
|
||||
"audio",
|
||||
"video",
|
||||
"video_range",
|
||||
"subtitles",
|
||||
"pathway_id",
|
||||
"stable_variant_id",
|
||||
)
|
||||
attribute_parser["program_id"] = int
|
||||
attribute_parser["bandwidth"] = lambda x: int(float(x))
|
||||
attribute_parser["average_bandwidth"] = int
|
||||
attribute_parser["frame_rate"] = float
|
||||
attribute_parser["hdcp_level"] = str
|
||||
state["stream_info"] = _parse_attribute_list(
|
||||
protocol.ext_x_stream_inf, line, attribute_parser
|
||||
)
|
||||
|
||||
|
||||
def _parse_i_frame_stream_inf(line, data, **kwargs):
|
||||
attribute_parser = remove_quotes_parser(
|
||||
"codecs", "uri", "pathway_id", "stable_variant_id"
|
||||
)
|
||||
attribute_parser["program_id"] = int
|
||||
attribute_parser["bandwidth"] = int
|
||||
attribute_parser["average_bandwidth"] = int
|
||||
attribute_parser["hdcp_level"] = str
|
||||
iframe_stream_info = _parse_attribute_list(
|
||||
protocol.ext_x_i_frame_stream_inf, line, attribute_parser
|
||||
)
|
||||
iframe_playlist = {
|
||||
"uri": iframe_stream_info.pop("uri"),
|
||||
"iframe_stream_info": iframe_stream_info,
|
||||
}
|
||||
|
||||
data["iframe_playlists"].append(iframe_playlist)
|
||||
|
||||
|
||||
def _parse_image_stream_inf(line, data, **kwargs):
|
||||
attribute_parser = remove_quotes_parser(
|
||||
"codecs", "uri", "pathway_id", "stable_variant_id"
|
||||
)
|
||||
attribute_parser["program_id"] = int
|
||||
attribute_parser["bandwidth"] = int
|
||||
attribute_parser["average_bandwidth"] = int
|
||||
attribute_parser["resolution"] = str
|
||||
image_stream_info = _parse_attribute_list(
|
||||
protocol.ext_x_image_stream_inf, line, attribute_parser
|
||||
)
|
||||
image_playlist = {
|
||||
"uri": image_stream_info.pop("uri"),
|
||||
"image_stream_info": image_stream_info,
|
||||
}
|
||||
|
||||
data["image_playlists"].append(image_playlist)
|
||||
|
||||
|
||||
def _parse_is_images_only(line, data, **kwargs):
|
||||
data["is_images_only"] = True
|
||||
|
||||
|
||||
def _parse_tiles(line, data, state, **kwargs):
|
||||
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)
|
||||
data["tiles"].append(tiles_info)
|
||||
|
||||
|
||||
def _parse_media(line, data, **kwargs):
|
||||
quoted = remove_quotes_parser(
|
||||
"uri",
|
||||
"group_id",
|
||||
"language",
|
||||
"assoc_language",
|
||||
"name",
|
||||
"instream_id",
|
||||
"characteristics",
|
||||
"channels",
|
||||
"stable_rendition_id",
|
||||
"thumbnails",
|
||||
"image",
|
||||
)
|
||||
media = _parse_attribute_list(protocol.ext_x_media, line, quoted)
|
||||
data["media"].append(media)
|
||||
|
||||
|
||||
def _parse_variant_playlist(line, data, state, **kwargs):
|
||||
playlist = {"uri": line, "stream_info": state.pop("stream_info")}
|
||||
data["playlists"].append(playlist)
|
||||
state["expect_playlist"] = False
|
||||
|
||||
|
||||
def _parse_bitrate(state, **kwargs):
|
||||
if "segment" not in state:
|
||||
state["segment"] = {}
|
||||
state["segment"]["bitrate"] = _parse_simple_parameter(cast_to=int, **kwargs)
|
||||
|
||||
|
||||
def _parse_byterange(line, state, **kwargs):
|
||||
if "segment" not in state:
|
||||
state["segment"] = {}
|
||||
state["segment"]["byterange"] = line.replace(protocol.ext_x_byterange + ":", "")
|
||||
state["expect_segment"] = True
|
||||
|
||||
|
||||
def _parse_targetduration(**parse_kwargs):
|
||||
return _parse_simple_parameter(cast_to=int, **parse_kwargs)
|
||||
|
||||
|
||||
def _parse_media_sequence(**parse_kwargs):
|
||||
return _parse_simple_parameter(cast_to=int, **parse_kwargs)
|
||||
|
||||
|
||||
def _parse_discontinuity_sequence(**parse_kwargs):
|
||||
return _parse_simple_parameter(cast_to=int, **parse_kwargs)
|
||||
|
||||
|
||||
def _parse_program_date_time(line, state, data, **parse_kwargs):
|
||||
_, program_date_time = _parse_simple_parameter_raw_value(
|
||||
line, cast_to=cast_date_time, **parse_kwargs
|
||||
)
|
||||
if not data.get("program_date_time"):
|
||||
data["program_date_time"] = program_date_time
|
||||
state["current_program_date_time"] = program_date_time
|
||||
state["program_date_time"] = program_date_time
|
||||
|
||||
|
||||
def _parse_discontinuity(state, **parse_kwargs):
|
||||
state["discontinuity"] = True
|
||||
|
||||
|
||||
def _parse_cue_in(state, **parse_kwargs):
|
||||
state["cue_in"] = True
|
||||
|
||||
|
||||
def _parse_cue_span(state, **parse_kwargs):
|
||||
state["cue_out"] = True
|
||||
|
||||
|
||||
def _parse_version(**parse_kwargs):
|
||||
return _parse_simple_parameter(cast_to=int, **parse_kwargs)
|
||||
|
||||
|
||||
def _parse_allow_cache(**parse_kwargs):
|
||||
return _parse_simple_parameter(cast_to=str, **parse_kwargs)
|
||||
|
||||
|
||||
def _parse_playlist_type(line, data, **kwargs):
|
||||
return _parse_simple_parameter(line, data)
|
||||
|
||||
|
||||
def _parse_x_map(line, data, state, **kwargs):
|
||||
quoted_parser = remove_quotes_parser("uri", "byterange")
|
||||
segment_map_info = _parse_attribute_list(protocol.ext_x_map, line, quoted_parser)
|
||||
state["current_segment_map"] = segment_map_info
|
||||
data["segment_map"].append(segment_map_info)
|
||||
|
||||
|
||||
def _parse_start(line, data, **kwargs):
|
||||
attribute_parser = {"time_offset": lambda x: float(x)}
|
||||
start_info = _parse_attribute_list(protocol.ext_x_start, line, attribute_parser)
|
||||
data["start"] = start_info
|
||||
|
||||
|
||||
def _parse_gap(state, **kwargs):
|
||||
state["gap"] = True
|
||||
|
||||
|
||||
def _parse_simple_parameter_raw_value(line, cast_to=str, normalize=False, **kwargs):
|
||||
param, value = line.split(":", 1)
|
||||
param = normalize_attribute(param.replace("#EXT-X-", ""))
|
||||
if normalize:
|
||||
value = value.strip().lower()
|
||||
return param, cast_to(value)
|
||||
|
||||
|
||||
def _parse_and_set_simple_parameter_raw_value(
|
||||
line, data, cast_to=str, normalize=False, **kwargs
|
||||
):
|
||||
param, value = _parse_simple_parameter_raw_value(line, cast_to, normalize)
|
||||
data[param] = value
|
||||
return data[param]
|
||||
|
||||
|
||||
def _parse_simple_parameter(line, data, cast_to=str, **kwargs):
|
||||
return _parse_and_set_simple_parameter_raw_value(line, data, cast_to, True)
|
||||
|
||||
|
||||
def _parse_i_frames_only(data, **kwargs):
|
||||
data["is_i_frames_only"] = True
|
||||
|
||||
|
||||
def _parse_is_independent_segments(data, **kwargs):
|
||||
data["is_independent_segments"] = True
|
||||
|
||||
|
||||
def _parse_endlist(data, **kwargs):
|
||||
data["is_endlist"] = True
|
||||
|
||||
|
||||
def _parse_cueout_cont(line, state, **kwargs):
|
||||
state["cue_out"] = True
|
||||
|
||||
elements = line.split(":", 1)
|
||||
if len(elements) != 2:
|
||||
return
|
||||
|
||||
# EXT-X-CUE-OUT-CONT:ElapsedTime=10,Duration=60,SCTE35=... style
|
||||
cue_info = _parse_attribute_list(
|
||||
protocol.ext_x_cue_out_cont,
|
||||
line,
|
||||
remove_quotes_parser("duration", "elapsedtime", "scte35"),
|
||||
)
|
||||
|
||||
# EXT-X-CUE-OUT-CONT:2.436/120 style
|
||||
progress = cue_info.get("")
|
||||
if progress:
|
||||
progress_parts = progress.split("/", 1)
|
||||
if len(progress_parts) == 1:
|
||||
state["current_cue_out_duration"] = progress_parts[0]
|
||||
else:
|
||||
state["current_cue_out_elapsedtime"] = progress_parts[0]
|
||||
state["current_cue_out_duration"] = progress_parts[1]
|
||||
|
||||
duration = cue_info.get("duration")
|
||||
if duration:
|
||||
state["current_cue_out_duration"] = duration
|
||||
|
||||
scte35 = cue_info.get("scte35")
|
||||
if duration:
|
||||
state["current_cue_out_scte35"] = scte35
|
||||
|
||||
elapsedtime = cue_info.get("elapsedtime")
|
||||
if elapsedtime:
|
||||
state["current_cue_out_elapsedtime"] = elapsedtime
|
||||
|
||||
|
||||
def _parse_cueout(line, state, **kwargs):
|
||||
state["cue_out_start"] = True
|
||||
state["cue_out"] = True
|
||||
if "DURATION" in line.upper():
|
||||
state["cue_out_explicitly_duration"] = True
|
||||
|
||||
elements = line.split(":", 1)
|
||||
if len(elements) != 2:
|
||||
return
|
||||
|
||||
cue_info = _parse_attribute_list(
|
||||
protocol.ext_x_cue_out,
|
||||
line,
|
||||
remove_quotes_parser("cue"),
|
||||
)
|
||||
cue_out_scte35 = cue_info.get("cue")
|
||||
cue_out_duration = cue_info.get("duration") or cue_info.get("")
|
||||
|
||||
current_cue_out_scte35 = state.get("current_cue_out_scte35")
|
||||
state["current_cue_out_scte35"] = cue_out_scte35 or current_cue_out_scte35
|
||||
state["current_cue_out_duration"] = cue_out_duration
|
||||
|
||||
|
||||
def _parse_server_control(line, data, **kwargs):
|
||||
attribute_parser = {
|
||||
"can_block_reload": str,
|
||||
"hold_back": lambda x: float(x),
|
||||
"part_hold_back": lambda x: float(x),
|
||||
"can_skip_until": lambda x: float(x),
|
||||
"can_skip_dateranges": str,
|
||||
}
|
||||
|
||||
data["server_control"] = _parse_attribute_list(
|
||||
protocol.ext_x_server_control, line, attribute_parser
|
||||
)
|
||||
|
||||
|
||||
def _parse_part_inf(line, data, **kwargs):
|
||||
attribute_parser = {"part_target": lambda x: float(x)}
|
||||
|
||||
data["part_inf"] = _parse_attribute_list(
|
||||
protocol.ext_x_part_inf, line, attribute_parser
|
||||
)
|
||||
|
||||
|
||||
def _parse_rendition_report(line, data, **kwargs):
|
||||
attribute_parser = remove_quotes_parser("uri")
|
||||
attribute_parser["last_msn"] = int
|
||||
attribute_parser["last_part"] = int
|
||||
|
||||
rendition_report = _parse_attribute_list(
|
||||
protocol.ext_x_rendition_report, line, attribute_parser
|
||||
)
|
||||
|
||||
data["rendition_reports"].append(rendition_report)
|
||||
|
||||
|
||||
def _parse_part(line, state, **kwargs):
|
||||
attribute_parser = remove_quotes_parser("uri")
|
||||
attribute_parser["duration"] = lambda x: float(x)
|
||||
attribute_parser["independent"] = str
|
||||
attribute_parser["gap"] = str
|
||||
attribute_parser["byterange"] = str
|
||||
|
||||
part = _parse_attribute_list(protocol.ext_x_part, line, attribute_parser)
|
||||
|
||||
# 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"])
|
||||
|
||||
part["dateranges"] = state.pop("dateranges", None)
|
||||
part["gap_tag"] = state.pop("gap", None)
|
||||
|
||||
if "segment" not in state:
|
||||
state["segment"] = {}
|
||||
segment = state["segment"]
|
||||
if "parts" not in segment:
|
||||
segment["parts"] = []
|
||||
|
||||
segment["parts"].append(part)
|
||||
|
||||
|
||||
def _parse_skip(line, data, **parse_kwargs):
|
||||
attribute_parser = remove_quotes_parser("recently_removed_dateranges")
|
||||
attribute_parser["skipped_segments"] = int
|
||||
|
||||
data["skip"] = _parse_attribute_list(protocol.ext_x_skip, line, attribute_parser)
|
||||
|
||||
|
||||
def _parse_session_data(line, data, **kwargs):
|
||||
quoted = remove_quotes_parser("data_id", "value", "uri", "language")
|
||||
session_data = _parse_attribute_list(protocol.ext_x_session_data, line, quoted)
|
||||
data["session_data"].append(session_data)
|
||||
|
||||
|
||||
def _parse_session_key(line, data, **kwargs):
|
||||
params = ATTRIBUTELISTPATTERN.split(
|
||||
line.replace(protocol.ext_x_session_key + ":", "")
|
||||
)[1::2]
|
||||
key = {}
|
||||
for param in params:
|
||||
name, value = param.split("=", 1)
|
||||
key[normalize_attribute(name)] = remove_quotes(value)
|
||||
data["session_keys"].append(key)
|
||||
|
||||
|
||||
def _parse_preload_hint(line, data, **kwargs):
|
||||
attribute_parser = remove_quotes_parser("uri")
|
||||
attribute_parser["type"] = str
|
||||
attribute_parser["byterange_start"] = int
|
||||
attribute_parser["byterange_length"] = int
|
||||
|
||||
data["preload_hint"] = _parse_attribute_list(
|
||||
protocol.ext_x_preload_hint, line, attribute_parser
|
||||
)
|
||||
|
||||
|
||||
def _parse_daterange(line, state, **kwargs):
|
||||
attribute_parser = remove_quotes_parser("id", "class", "start_date", "end_date")
|
||||
attribute_parser["duration"] = float
|
||||
attribute_parser["planned_duration"] = float
|
||||
attribute_parser["end_on_next"] = str
|
||||
attribute_parser["scte35_cmd"] = str
|
||||
attribute_parser["scte35_out"] = str
|
||||
attribute_parser["scte35_in"] = str
|
||||
|
||||
parsed = _parse_attribute_list(protocol.ext_x_daterange, line, attribute_parser)
|
||||
|
||||
if "dateranges" not in state:
|
||||
state["dateranges"] = []
|
||||
|
||||
state["dateranges"].append(parsed)
|
||||
|
||||
|
||||
def _parse_content_steering(line, data, **kwargs):
|
||||
attribute_parser = remove_quotes_parser("server_uri", "pathway_id")
|
||||
|
||||
data["content_steering"] = _parse_attribute_list(
|
||||
protocol.ext_x_content_steering, line, attribute_parser
|
||||
)
|
||||
|
||||
|
||||
def _parse_oatcls_scte35(line, state, **kwargs):
|
||||
scte35_cue = line.split(":", 1)[1]
|
||||
state["current_cue_out_oatcls_scte35"] = scte35_cue
|
||||
state["current_cue_out_scte35"] = scte35_cue
|
||||
|
||||
|
||||
def _parse_asset(line, state, **kwargs):
|
||||
# EXT-X-ASSET attribute values may or may not be quoted, and need to be URL-encoded.
|
||||
# They are preserved as-is here to prevent loss of information.
|
||||
state["asset_metadata"] = _parse_attribute_list(
|
||||
protocol.ext_x_asset, line, {}, default_parser=str
|
||||
)
|
||||
|
||||
|
||||
def string_to_lines(string):
|
||||
return string.strip().splitlines()
|
||||
|
||||
|
||||
def remove_quotes_parser(*attrs):
|
||||
return dict(zip(attrs, itertools.repeat(remove_quotes)))
|
||||
|
||||
|
||||
def remove_quotes(string):
|
||||
"""
|
||||
Remove quotes from string.
|
||||
|
||||
Ex.:
|
||||
"foo" -> foo
|
||||
'foo' -> foo
|
||||
'foo -> 'foo
|
||||
|
||||
"""
|
||||
quotes = ('"', "'")
|
||||
if string.startswith(quotes) and string.endswith(quotes):
|
||||
return string[1:-1]
|
||||
return string
|
||||
|
||||
|
||||
def normalize_attribute(attribute):
|
||||
return attribute.replace("-", "_").lower().strip()
|
||||
|
||||
|
||||
def get_segment_custom_value(state, key, default=None):
|
||||
"""
|
||||
Helper function for getting custom values for Segment
|
||||
Are useful with custom_tags_parser
|
||||
"""
|
||||
if "segment" not in state:
|
||||
return default
|
||||
if "custom_parser_values" not in state["segment"]:
|
||||
return default
|
||||
return state["segment"]["custom_parser_values"].get(key, default)
|
||||
|
||||
|
||||
def save_segment_custom_value(state, key, value):
|
||||
"""
|
||||
Helper function for saving custom values for Segment
|
||||
Are useful with custom_tags_parser
|
||||
"""
|
||||
if "segment" not in state:
|
||||
state["segment"] = {}
|
||||
|
||||
if "custom_parser_values" not in state["segment"]:
|
||||
state["segment"]["custom_parser_values"] = {}
|
||||
|
||||
state["segment"]["custom_parser_values"][key] = value
|
45
env/lib/python3.11/site-packages/m3u8/protocol.py
vendored
Normal file
45
env/lib/python3.11/site-packages/m3u8/protocol.py
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
# 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.
|
||||
|
||||
ext_m3u = "#EXTM3U"
|
||||
ext_x_targetduration = "#EXT-X-TARGETDURATION"
|
||||
ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE"
|
||||
ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE"
|
||||
ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME"
|
||||
ext_x_media = "#EXT-X-MEDIA"
|
||||
ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE"
|
||||
ext_x_key = "#EXT-X-KEY"
|
||||
ext_x_stream_inf = "#EXT-X-STREAM-INF"
|
||||
ext_x_version = "#EXT-X-VERSION"
|
||||
ext_x_allow_cache = "#EXT-X-ALLOW-CACHE"
|
||||
ext_x_endlist = "#EXT-X-ENDLIST"
|
||||
extinf = "#EXTINF"
|
||||
ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY"
|
||||
ext_x_asset = "#EXT-X-ASSET"
|
||||
ext_x_bitrate = "#EXT-X-BITRATE"
|
||||
ext_x_byterange = "#EXT-X-BYTERANGE"
|
||||
ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF"
|
||||
ext_x_discontinuity = "#EXT-X-DISCONTINUITY"
|
||||
ext_x_cue_out = "#EXT-X-CUE-OUT"
|
||||
ext_x_cue_out_cont = "#EXT-X-CUE-OUT-CONT"
|
||||
ext_x_cue_in = "#EXT-X-CUE-IN"
|
||||
ext_x_cue_span = "#EXT-X-CUE-SPAN"
|
||||
ext_oatcls_scte35 = "#EXT-OATCLS-SCTE35"
|
||||
ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS"
|
||||
ext_x_map = "#EXT-X-MAP"
|
||||
ext_x_start = "#EXT-X-START"
|
||||
ext_x_server_control = "#EXT-X-SERVER-CONTROL"
|
||||
ext_x_part_inf = "#EXT-X-PART-INF"
|
||||
ext_x_part = "#EXT-X-PART"
|
||||
ext_x_rendition_report = "#EXT-X-RENDITION-REPORT"
|
||||
ext_x_skip = "#EXT-X-SKIP"
|
||||
ext_x_session_data = "#EXT-X-SESSION-DATA"
|
||||
ext_x_session_key = "#EXT-X-SESSION-KEY"
|
||||
ext_x_preload_hint = "#EXT-X-PRELOAD-HINT"
|
||||
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"
|
39
env/lib/python3.11/site-packages/m3u8/version_matching.py
vendored
Normal file
39
env/lib/python3.11/site-packages/m3u8/version_matching.py
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
from typing import List
|
||||
|
||||
from m3u8 import protocol
|
||||
from m3u8.version_matching_rules import VersionMatchingError, available_rules
|
||||
|
||||
|
||||
def get_version(file_lines: List[str]):
|
||||
for line in file_lines:
|
||||
if line.startswith(protocol.ext_x_version):
|
||||
version = line.split(":")[1]
|
||||
return float(version)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if not validator.validate():
|
||||
errors.append(validator.get_error())
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def validate(file_lines: List[str]) -> List[VersionMatchingError]:
|
||||
found_version = get_version(file_lines)
|
||||
if found_version is None:
|
||||
return []
|
||||
|
||||
errors = []
|
||||
for number, line in enumerate(file_lines):
|
||||
errors_in_line = valid_in_all_rules(number, line, found_version)
|
||||
errors.extend(errors_in_line)
|
||||
|
||||
return errors
|
109
env/lib/python3.11/site-packages/m3u8/version_matching_rules.py
vendored
Normal file
109
env/lib/python3.11/site-packages/m3u8/version_matching_rules.py
vendored
Normal file
@ -0,0 +1,109 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Type
|
||||
|
||||
from m3u8 import protocol
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
|
||||
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."
|
||||
|
||||
def validate(self):
|
||||
if protocol.ext_x_key not in self.line:
|
||||
return True
|
||||
|
||||
if "IV" in self.line:
|
||||
return self.version >= 2
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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 protocol.extinf not in self.line:
|
||||
return True
|
||||
|
||||
chunks = self.line.replace(protocol.extinf + ":", "").split(",", 1)
|
||||
duration = chunks[0]
|
||||
|
||||
def is_number(value: str):
|
||||
try:
|
||||
float(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
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 (
|
||||
protocol.ext_x_byterange not in self.line
|
||||
and protocol.ext_i_frames_only not in self.line
|
||||
):
|
||||
return True
|
||||
|
||||
return self.version >= 4
|
||||
|
||||
|
||||
available_rules: List[Type[VersionMatchRuleBase]] = [
|
||||
ValidIVInEXTXKEY,
|
||||
ValidFloatingPointEXTINF,
|
||||
ValidEXTXBYTERANGEOrEXTXIFRAMESONLY,
|
||||
]
|
Reference in New Issue
Block a user