forked from known-as-bmf/plugin.video.arteplussept
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create m3u8 with multiple languages while playing content - WIP
- Loading branch information
1 parent
4e7ad77
commit 55674ed
Showing
13 changed files
with
2,462 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# 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. | ||
|
||
import sys | ||
import os | ||
|
||
from urllib.parse import urljoin, urlsplit | ||
|
||
from resources.lib.m3u8.httpclient import DefaultHTTPClient | ||
from resources.lib.m3u8.model import (M3U8, Segment, SegmentList, PartialSegment, | ||
PartialSegmentList, Key, Playlist, IFramePlaylist, | ||
Media, MediaList, PlaylistList, Start, | ||
RenditionReport, RenditionReportList, ServerControl, | ||
Skip, PartInformation, PreloadHint, DateRange, | ||
DateRangeList, ContentSteering) | ||
from resources.lib.m3u8.parser import parse, ParseError | ||
|
||
|
||
__all__ = ('M3U8', 'Segment', 'SegmentList', 'PartialSegment', | ||
'PartialSegmentList', 'Key', 'Playlist', 'IFramePlaylist', | ||
'Media', 'MediaList', 'PlaylistList', 'Start', 'RenditionReport', | ||
'RenditionReportList', 'ServerControl', 'Skip', 'PartInformation', | ||
'PreloadHint', 'DateRange', 'DateRangeList', 'ContentSteering', | ||
'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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
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(), '.') | ||
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
"""ISO 8601 date time string parsing | ||
Basic usage: | ||
>>> import iso8601 | ||
>>> iso8601.parse_date("2007-01-25T12:00:00Z") | ||
datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.Utc ...>) | ||
>>> | ||
""" | ||
|
||
import datetime | ||
import re | ||
import typing | ||
from decimal import Decimal | ||
|
||
__all__ = ["parse_date", "ParseError", "UTC", "FixedOffset"] | ||
|
||
# Adapted from http://delete.me.uk/2005/03/iso8601.html | ||
ISO8601_REGEX = re.compile( | ||
r""" | ||
(?P<year>[0-9]{4}) | ||
( | ||
( | ||
(-(?P<monthdash>[0-9]{1,2})) | ||
| | ||
(?P<month>[0-9]{2}) | ||
(?!$) # Don't allow YYYYMM | ||
) | ||
( | ||
( | ||
(-(?P<daydash>[0-9]{1,2})) | ||
| | ||
(?P<day>[0-9]{2}) | ||
) | ||
( | ||
( | ||
(?P<separator>[ T]) | ||
(?P<hour>[0-9]{2}) | ||
(:{0,1}(?P<minute>[0-9]{2})){0,1} | ||
( | ||
:{0,1}(?P<second>[0-9]{1,2}) | ||
([.,](?P<second_fraction>[0-9]+)){0,1} | ||
){0,1} | ||
(?P<timezone> | ||
Z | ||
| | ||
( | ||
(?P<tz_sign>[-+]) | ||
(?P<tz_hour>[0-9]{2}) | ||
:{0,1} | ||
(?P<tz_minute>[0-9]{2}){0,1} | ||
) | ||
){0,1} | ||
){0,1} | ||
) | ||
){0,1} # YYYY-MM | ||
){0,1} # YYYY only | ||
$ | ||
""", | ||
re.VERBOSE, | ||
) | ||
|
||
|
||
class ParseError(ValueError): | ||
"""Raised when there is a problem parsing a date string""" | ||
|
||
|
||
UTC = datetime.timezone.utc | ||
|
||
|
||
def FixedOffset( | ||
offset_hours: float, offset_minutes: float, name: str | ||
) -> datetime.timezone: | ||
return datetime.timezone( | ||
datetime.timedelta(hours=offset_hours, minutes=offset_minutes), name | ||
) | ||
|
||
|
||
def parse_timezone( | ||
matches: typing.Dict[str, str], | ||
default_timezone: typing.Optional[datetime.timezone] = UTC, | ||
) -> typing.Optional[datetime.timezone]: | ||
"""Parses ISO 8601 time zone specs into tzinfo offsets""" | ||
tz = matches.get("timezone", None) | ||
if tz == "Z": | ||
return UTC | ||
# This isn't strictly correct, but it's common to encounter dates without | ||
# timezones so I'll assume the default (which defaults to UTC). | ||
# Addresses issue 4. | ||
if tz is None: | ||
return default_timezone | ||
sign = matches.get("tz_sign", None) | ||
hours = int(matches.get("tz_hour", 0)) | ||
minutes = int(matches.get("tz_minute", 0)) | ||
description = f"{sign}{hours:02d}:{minutes:02d}" | ||
if sign == "-": | ||
hours = -hours | ||
minutes = -minutes | ||
return FixedOffset(hours, minutes, description) | ||
|
||
|
||
def parse_date( | ||
datestring: str, default_timezone: typing.Optional[datetime.timezone] = UTC | ||
) -> datetime.datetime: | ||
"""Parses ISO 8601 dates into datetime objects | ||
The timezone is parsed from the date string. However it is quite common to | ||
have dates without a timezone (not strictly correct). In this case the | ||
default timezone specified in default_timezone is used. This is UTC by | ||
default. | ||
:param datestring: The date to parse as a string | ||
:param default_timezone: A datetime tzinfo instance to use when no timezone | ||
is specified in the datestring. If this is set to | ||
None then a naive datetime object is returned. | ||
:returns: A datetime.datetime instance | ||
:raises: ParseError when there is a problem parsing the date or | ||
constructing the datetime instance. | ||
""" | ||
try: | ||
m = ISO8601_REGEX.match(datestring) | ||
except Exception as e: | ||
raise ParseError(e) | ||
|
||
if not m: | ||
raise ParseError(f"Unable to parse date string {datestring!r}") | ||
|
||
# Drop any Nones from the regex matches | ||
# TODO: check if there's a way to omit results in regexes | ||
groups: typing.Dict[str, str] = { | ||
k: v for k, v in m.groupdict().items() if v is not None | ||
} | ||
|
||
try: | ||
return datetime.datetime( | ||
year=int(groups.get("year", 0)), | ||
month=int(groups.get("month", groups.get("monthdash", 1))), | ||
day=int(groups.get("day", groups.get("daydash", 1))), | ||
hour=int(groups.get("hour", 0)), | ||
minute=int(groups.get("minute", 0)), | ||
second=int(groups.get("second", 0)), | ||
microsecond=int( | ||
Decimal(f"0.{groups.get('second_fraction', 0)}") * Decimal("1000000.0") | ||
), | ||
tzinfo=parse_timezone(groups, default_timezone=default_timezone), | ||
) | ||
except Exception as e: | ||
raise ParseError(e) | ||
|
||
|
||
def is_iso8601(datestring: str) -> bool: | ||
"""Check if a string matches an ISO 8601 format. | ||
:param datestring: The string to check for validity | ||
:returns: True if the string matches an ISO 8601 format, False otherwise | ||
""" | ||
try: | ||
m = ISO8601_REGEX.match(datestring) | ||
return bool(m) | ||
except Exception as e: | ||
raise ParseError(e) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from os.path import dirname | ||
from urllib.parse import urljoin, urlsplit | ||
|
||
|
||
class BasePathMixin(object): | ||
|
||
@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 = "%s/%s" % (newbase_path, self.uri) | ||
else: | ||
self.uri = self.uri.replace(self.base_path, newbase_path) | ||
|
||
|
||
class GroupedBasePathMixin(object): | ||
|
||
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) |
Oops, something went wrong.