streamlink/src/livestreamer/plugin/plugin.py

277 lines
7.5 KiB
Python
Raw Normal View History

import operator
2013-08-08 15:01:32 +02:00
import re
from functools import partial
from ..cache import Cache
from ..exceptions import PluginError, NoStreamsError
from ..options import Options
2013-08-01 14:04:20 +02:00
QUALITY_WEIGTHS_EXTRA = {
"other": {
"live": 1080,
"source": 1080,
"high": 720,
"medium": 480,
"low": 240,
},
"tv": {
"hd": 1080,
"sd": 576,
},
"quality": {
"ehq": 720,
"hq": 576,
"sq": 360,
},
"mobile": {
"mobile_high": 330,
"mobile_medium": 260,
"mobile_low": 170,
}
}
FILTER_OPERATORS = {
"<": operator.lt,
"<=": operator.le,
">": operator.gt,
">=": operator.ge,
}
def stream_weight(stream):
for group, weights in QUALITY_WEIGTHS_EXTRA.items():
if stream in weights:
return weights[stream], group
match = re.match("^(\d+)([k]|[p])?([\+])?$", stream)
if match:
if match.group(2) == "k":
bitrate = int(match.group(1))
# These calculations are very rough
if bitrate > 2000:
weight = bitrate / 3.4
elif bitrate > 1000:
weight = bitrate / 2.6
else:
weight = bitrate / 1.7
return weight, "bitrate"
elif match.group(2) == "p":
2013-03-22 02:13:06 +01:00
weight = int(match.group(1))
if match.group(3) == "+":
weight += 1
return weight, "pixels"
return 0, "none"
def iterate_streams(streams):
for name, stream in streams.items():
if isinstance(stream, list):
for sub_stream in stream:
yield (name, sub_stream)
else:
yield (name, stream)
2013-08-08 15:01:32 +02:00
def stream_type_priority(stream_types, stream):
2013-08-08 15:01:32 +02:00
stream_type = type(stream[1]).shortname()
try:
prio = stream_types.index(stream_type)
except ValueError:
prio = 99
return prio
def stream_sorting_filter(expr, stream_weight):
match = re.match(r"(?P<op><=|>=|<|>)?(?P<value>[\w\+]+)", expr)
if not match:
raise PluginError("Invalid filter expression: {0}".format(expr))
op, value = match.group("op", "value")
op = FILTER_OPERATORS.get(op, operator.eq)
filter_weight, filter_group = stream_weight(value)
def func(quality):
weight, group = stream_weight(quality)
if group == filter_group:
return not op(weight, filter_weight)
return True
return func
2013-08-08 15:01:32 +02:00
class Plugin(object):
2013-08-08 15:01:32 +02:00
"""A plugin can retrieve stream information from the URL specified.
2013-02-25 04:21:37 +01:00
:param url: URL that the plugin will operate on
"""
cache = None
logger = None
module = "unknown"
options = Options()
session = None
@classmethod
def bind(cls, session, module):
cls.cache = Cache(filename="plugin-cache.json",
key_prefix=module)
cls.logger = session.logger.new_module("plugin." + module)
cls.module = module
cls.session = session
def __init__(self, url):
self.url = url
@classmethod
def can_handle_url(cls, url):
raise NotImplementedError
@classmethod
def set_option(cls, key, value):
cls.options.set(key, value)
@classmethod
def get_option(cls, key):
return cls.options.get(key)
@classmethod
def stream_weight(cls, stream):
return stream_weight(stream)
@classmethod
def default_stream_types(cls, streams):
stream_types = ["rtmp", "hls", "hds", "http"]
for name, stream in iterate_streams(streams):
stream_type = type(stream).shortname()
if stream_type not in stream_types:
stream_types.append(stream_type)
return stream_types
def get_streams(self, stream_types=None, sorting_excludes=None):
2013-08-08 15:01:32 +02:00
"""Attempts to extract available streams.
2013-08-08 15:01:32 +02:00
Returns a :class:`dict` containing the streams, where the key is
the name of the stream, most commonly the quality and the value
is a :class:`Stream` object.
2013-02-25 04:21:37 +01:00
2013-08-08 15:01:32 +02:00
The result can contain the synonyms **best** and **worst** which
points to the streams which are likely to be of highest and
lowest quality respectively.
2013-02-25 04:21:37 +01:00
2013-08-08 15:01:32 +02:00
If multiple streams with the same name are found, the order of
streams specified in *stream_types* will determine which stream
gets to keep the name while the rest will be renamed to
"<name>_<stream type>".
2013-02-25 04:21:37 +01:00
The synonyms can be fine tuned with the *sorting_excludes*
parameter. This can be either of these types:
- A list of filter expressions in the format
*[operator]<value>*. For example the filter ">480p" will
exclude streams ranked higher than "480p" from the list
used in the synonyms ranking. Valid operators are >, >=, <
and <=. If no operator is specified then equality will be
tested.
- A function that is passed to filter() with a list of
stream names as input.
:param stream_types: A list of stream types to return.
:param sorting_excludes: Specify which streams to exclude from
the best/worst synonyms.
.. versionchanged:: 1.4.2
Added *priority* parameter.
2013-09-29 22:01:31 +02:00
.. versionchanged:: 1.5.0
2013-08-08 15:01:32 +02:00
Renamed *priority* to *stream_types* and changed behaviour
slightly.
2013-09-29 22:01:31 +02:00
.. versionchanged:: 1.5.0
Added *sorting_excludes* parameter.
2013-09-29 22:01:31 +02:00
.. versionchanged:: 1.6.0
*sorting_excludes* can now be a list of filter expressions
or a function that is passed to filter().
"""
try:
ostreams = self._get_streams()
except NoStreamsError:
return {}
if not ostreams:
return {}
streams = {}
if stream_types is None:
stream_types = self.default_stream_types(ostreams)
# Add streams depending on stream type and priorities
sorted_streams = sorted(iterate_streams(ostreams),
2013-08-08 15:01:32 +02:00
key=partial(stream_type_priority,
stream_types))
for name, stream in sorted_streams:
2013-08-01 14:04:20 +02:00
stream_type = type(stream).shortname()
if stream_type not in stream_types:
continue
if name in streams:
name = "{0}_{1}".format(name, stream_type)
# Force lowercase name and replace space with underscore.
streams[name.lower().replace(" ", "_")] = stream
# Create the best/worst synonmys
stream_weight_only = lambda s: (self.stream_weight(s)[0] or
(len(streams) == 1 and 1))
stream_names = filter(stream_weight_only, streams.keys())
sorted_streams = sorted(stream_names, key=stream_weight_only)
if isinstance(sorting_excludes, list):
for expr in sorting_excludes:
filter_func = stream_sorting_filter(expr, self.stream_weight)
sorted_streams = list(filter(filter_func, sorted_streams))
elif callable(sorting_excludes):
sorted_streams = list(filter(sorting_excludes, sorted_streams))
if len(sorted_streams) > 0:
best = sorted_streams[-1]
worst = sorted_streams[0]
streams["best"] = streams[best]
streams["worst"] = streams[worst]
return streams
def _get_streams(self):
raise NotImplementedError
__all__ = ["Plugin"]