Beispiel #1
0
 def __init__(self, options=None):
     self.http = HTTPSession()
     self.options = Options({
         "interface": None,
         "ipv4": False,
         "ipv6": False,
         "hls-live-edge": 3,
         "hls-segment-ignore-names": [],
         "hls-playlist-reload-attempts": 3,
         "hls-playlist-reload-time":
         "default",  # default, duration, segment, average
         "hls-start-offset": 0,
         "hls-duration": None,
         "hls-live-restart": False,
         "ringbuffer-size": 1024 * 1024 * 16,  # 16 MB
         "stream-segment-attempts": 3,
         "stream-segment-threads": 1,
         "stream-segment-timeout": 10.0,
         "stream-timeout": 60.0,
         "ffmpeg-ffmpeg": None,
         "ffmpeg-fout": None,
         "ffmpeg-video-transcode": None,
         "ffmpeg-audio-transcode": None,
         "ffmpeg-copyts": False,
         "ffmpeg-start-at-zero": False,
         "mux-subtitles": False,
         "locale": None,
         "user-input-requester": None,
         "chunk-size": 8192
     })
     if options:
         self.options.update(options)
     self.plugins: Dict[str, Type[Plugin]] = OrderedDict({})
     self.load_builtin_plugins()
Beispiel #2
0
class TestOptions(unittest.TestCase):
    def setUp(self):
        self.options = Options({
            "a_default": "default"
        })

    def test_options(self):
        self.assertEqual(self.options.get("a_default"), "default")
        self.assertEqual(self.options.get("non_existing"), None)

        self.options.set("a_option", "option")
        self.assertEqual(self.options.get("a_option"), "option")
Beispiel #3
0
    def __init__(self, options=None):
        self.http = api.HTTPSession()
        self.options = Options({
            "interface": None,
            "ipv4": False,
            "ipv6": False,
            "hds-live-edge": 10.0,
            "hds-segment-attempts": 3,
            "hds-segment-threads": 1,
            "hds-segment-timeout": 10.0,
            "hds-timeout": 60.0,
            "hls-live-edge": 3,
            "hls-segment-attempts": 3,
            "hls-segment-ignore-names": [],
            "hls-segment-threads": 1,
            "hls-segment-timeout": 10.0,
            "hls-segment-stream-data": False,
            "hls-timeout": 60.0,
            "hls-playlist-reload-attempts": 3,
            "hls-playlist-reload-time": "default",
            "hls-start-offset": 0,
            "hls-duration": None,
            "http-stream-timeout": 60.0,
            "hls-token-period": 60.0,
            "ringbuffer-size": 1024 * 1024 * 16,  # 16 MB
            "rtmp-timeout": 60.0,
            "rtmp-rtmpdump": is_win32 and "rtmpdump.exe" or "rtmpdump",
            "rtmp-proxy": None,
            "stream-segment-attempts": 3,
            "stream-segment-threads": 1,
            "stream-segment-timeout": 10.0,
            "stream-timeout": 60.0,
            "subprocess-errorlog": False,
            "subprocess-errorlog-path": None,
            "ffmpeg-ffmpeg": None,
            "ffmpeg-fout": None,
            "ffmpeg-video-transcode": None,
            "ffmpeg-audio-transcode": None,
            "ffmpeg-copyts": False,
            "ffmpeg-start-at-zero": False,
            "mux-subtitles": False,
            "locale": None,
            "user-input-requester": None
        })
        # felix add update headers cookies
        self.cache = Cache(filename="StreamlinkSession.json")

        if options:
            self.options.update(options)
        self.plugins = OrderedDict({})
        self.load_builtin_plugins()
Beispiel #4
0
class TestPlugin(Plugin):
    arguments = PluginArguments(
        PluginArgument("bool", action="store_true"),
        PluginArgument("password", metavar="PASSWORD", sensitive=True))

    options = Options({"a_option": "default"})

    @classmethod
    def can_handle_url(self, url):
        return "test.se" in url

    def get_title(self):
        return "Test Title"

    def get_author(self):
        return "Tѥst Āuƭhǿr"

    def get_category(self):
        return None

    def _get_streams(self):
        if "empty" in self.url:
            return

        if "UnsortableStreamNames" in self.url:

            def gen():
                for i in range(3):
                    yield "vod", HTTPStream(self.session,
                                            "http://test.se/stream")

            return gen()

        if "NoStreamsError" in self.url:
            raise NoStreamsError(self.url)

        streams = {}
        streams["test"] = TestStream(self.session)
        streams["rtmp"] = RTMPStream(self.session, dict(rtmp="rtmp://test.se"))
        streams["hls"] = HLSStream(self.session,
                                   "http://test.se/playlist.m3u8")
        streams["http"] = HTTPStream(self.session, "http://test.se/stream")
        streams["akamaihd"] = AkamaiHDStream(self.session,
                                             "http://test.se/stream")

        streams["240p"] = HTTPStream(self.session, "http://test.se/stream")
        streams["360p"] = HTTPStream(self.session, "http://test.se/stream")
        streams["1080p"] = HTTPStream(self.session, "http://test.se/stream")

        streams["350k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["800k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["1500k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["3000k"] = HTTPStream(self.session, "http://test.se/stream")

        streams["480p"] = [
            HTTPStream(self.session, "http://test.se/stream"),
            RTMPStream(self.session, dict(rtmp="rtmp://test.se"))
        ]

        return streams
Beispiel #5
0
class TestPlugin(Plugin):
    options = Options({"a_option": "default"})

    @classmethod
    def can_handle_url(self, url):
        return "test.se" in url

    def _get_streams(self):
        streams = {}
        streams["rtmp"] = RTMPStream(self.session, dict(rtmp="rtmp://test.se"))
        streams["hls"] = HLSStream(self.session,
                                   "http://test.se/playlist.m3u8")
        streams["http"] = HTTPStream(self.session, "http://test.se/stream")
        streams["akamaihd"] = AkamaiHDStream(self.session,
                                             "http://test.se/stream")

        streams["240p"] = HTTPStream(self.session, "http://test.se/stream")
        streams["360p"] = HTTPStream(self.session, "http://test.se/stream")
        streams["1080p"] = HTTPStream(self.session, "http://test.se/stream")

        streams["350k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["800k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["1500k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["3000k"] = HTTPStream(self.session, "http://test.se/stream")

        streams["480p"] = [
            HTTPStream(self.session, "http://test.se/stream"),
            RTMPStream(self.session, dict(rtmp="rtmp://test.se"))
        ]

        streams.update(testplugin_support.get_streams(self.session))

        return streams
Beispiel #6
0
 def __init__(self, options=None):
     self.http = api.HTTPSession()
     self.options = Options({
         "hds-live-edge": 10.0,
         "hds-segment-attempts": 3,
         "hds-segment-threads": 1,
         "hds-segment-timeout": 10.0,
         "hds-timeout": 60.0,
         "hls-live-edge": 3,
         "hls-segment-attempts": 3,
         "hls-segment-ignore-names": [],
         "hls-segment-threads": 1,
         "hls-segment-timeout": 10.0,
         "hls-segment-stream-data": False,
         "hls-timeout": 60.0,
         "hls-playlist-reload-attempts": 3,
         "hls-playlist-reload-time":
         "default",  # default, duration, segment, average
         "hls-start-offset": 0,
         "hls-duration": None,
         "http-stream-timeout": 60.0,
         "ringbuffer-size": 1024 * 1024 * 16,  # 16 MB
         "rtmp-timeout": 60.0,
         "rtmp-rtmpdump": is_win32 and "rtmpdump.exe" or "rtmpdump",
         "rtmp-proxy": None,
         "stream-segment-attempts": 3,
         "stream-segment-threads": 1,
         "stream-segment-timeout": 10.0,
         "stream-timeout": 60.0,
         "subprocess-errorlog": False,
         "subprocess-errorlog-path": None,
         "ffmpeg-ffmpeg": None,
         "ffmpeg-fout": None,
         "ffmpeg-video-transcode": None,
         "ffmpeg-audio-transcode": None,
         "ffmpeg-copyts": False,
         "ffmpeg-start-at-zero": False,
         "mux-subtitles": False,
         "locale": None,
         "user-input-requester": None,
         "chunk-size-ts": 8192,
         "chunk-size-hls": 8192
     })
     if options:
         self.options.update(options)
     self.plugins = OrderedDict({})
     self.load_builtin_plugins()
Beispiel #7
0
class TestPlugin(Plugin):
    options = Options({"a_option": "default"})

    @classmethod
    def can_handle_url(self, url):
        return "test.se" in url

    def get_title(self):
        return "Test Title"

    def get_author(self):
        return "Tѥst Āuƭhǿr"

    def get_category(self):
        return None

    def _get_streams(self):
        if "empty" in self.url:
            return

        if "UnsortableStreamNames" in self.url:

            def gen():
                for i in range(3):
                    yield "vod", HTTPStream(self.session,
                                            "http://test.se/stream")

            return gen()

        if "NoStreamsError" in self.url:
            raise NoStreamsError(self.url)

        streams = {}
        streams["test"] = TestStream(self.session)
        streams["rtmp"] = RTMPStream(self.session, dict(rtmp="rtmp://test.se"))
        streams["hls"] = HLSStream(self.session,
                                   "http://test.se/playlist.m3u8")
        streams["http"] = HTTPStream(self.session, "http://test.se/stream")
        streams["akamaihd"] = AkamaiHDStream(self.session,
                                             "http://test.se/stream")

        streams["240p"] = HTTPStream(self.session, "http://test.se/stream")
        streams["360p"] = HTTPStream(self.session, "http://test.se/stream")
        streams["1080p"] = HTTPStream(self.session, "http://test.se/stream")

        streams["350k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["800k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["1500k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["3000k"] = HTTPStream(self.session, "http://test.se/stream")

        streams["480p"] = [
            HTTPStream(self.session, "http://test.se/stream"),
            RTMPStream(self.session, dict(rtmp="rtmp://test.se"))
        ]

        streams.update(testplugin_support.get_streams(self.session))

        return streams
Beispiel #8
0
class TestPlugin(Plugin):
    arguments = PluginArguments(
        PluginArgument(
            "bool",
            action="store_true"
        ),
        PluginArgument(
            "password",
            metavar="PASSWORD",
            sensitive=True
        )
    )

    options = Options({
        "a_option": "default"
    })

    id = "test-id-1234-5678"
    author = "Tѥst Āuƭhǿr"
    category = None
    title = "Test Title"

    def _get_streams(self):
        if "empty" in self.url:
            return

        if "UnsortableStreamNames" in self.url:
            def gen():
                for i in range(3):
                    yield "vod", HTTPStream(self.session, "http://test.se/stream")

            return gen()

        if "NoStreamsError" in self.url:
            raise NoStreamsError(self.url)

        streams = {}
        streams["test"] = TestStream(self.session)
        streams["hls"] = HLSStream(self.session, "http://test.se/playlist.m3u8")
        streams["http"] = HTTPStream(self.session, "http://test.se/stream")

        streams["240p"] = HTTPStream(self.session, "http://test.se/stream")
        streams["360p"] = HTTPStream(self.session, "http://test.se/stream")
        streams["1080p"] = HTTPStream(self.session, "http://test.se/stream")

        streams["350k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["800k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["1500k"] = HTTPStream(self.session, "http://test.se/stream")
        streams["3000k"] = HTTPStream(self.session, "http://test.se/stream")

        streams["480p"] = [
            HTTPStream(self.session, "http://test.se/stream"),
            HLSStream(self.session, "http://test.se/playlist.m3u8")
        ]

        return streams
Beispiel #9
0
class TestOptions(unittest.TestCase):
    def setUp(self):
        self.options = Options({
            "a_default": "default",
            "another-default": "default2"
        })

    def test_options(self):
        self.assertEqual(self.options.get("a_default"), "default")
        self.assertEqual(self.options.get("non_existing"), None)

        self.options.set("a_option", "option")
        self.assertEqual(self.options.get("a_option"), "option")

    def test_options_name_normalised(self):
        self.assertEqual(self.options.get("a_default"), "default")
        self.assertEqual(self.options.get("a-default"), "default")
        self.assertEqual(self.options.get("another-default"), "default2")
        self.assertEqual(self.options.get("another_default"), "default2")
Beispiel #10
0
class TestOptions(unittest.TestCase):
    def setUp(self):
        self.options = Options({
            "a_default": "default",
            "another-default": "default2"
        })

    def test_options(self):
        self.assertEqual(self.options.get("a_default"), "default")
        self.assertEqual(self.options.get("non_existing"), None)

        self.options.set("a_option", "option")
        self.assertEqual(self.options.get("a_option"), "option")

    def test_options_name_normalised(self):
        self.assertEqual(self.options.get("a_default"), "default")
        self.assertEqual(self.options.get("a-default"), "default")
        self.assertEqual(self.options.get("another-default"), "default2")
        self.assertEqual(self.options.get("another_default"), "default2")
Beispiel #11
0
class Plugin(object):
    """A plugin can retrieve stream information from the URL specified.

    :param url: URL that the plugin will operate on
    """

    cache = None
    logger = None
    module = "unknown"
    options = Options()
    arguments = Arguments()
    session = None
    _user_input_requester = None

    @classmethod
    def bind(cls, session, module, user_input_requester=None):
        cls.cache = Cache(filename="plugin-cache.json", key_prefix=module)
        cls.logger = logging.getLogger("streamlink.plugin." + module)
        cls.module = module
        cls.session = session
        if user_input_requester is not None:
            if isinstance(user_input_requester, UserInputRequester):
                cls._user_input_requester = user_input_requester
            else:
                raise RuntimeError(
                    "user-input-requester must be an instance of UserInputRequester"
                )

    def __init__(self, url):
        self.url = url
        try:
            self.load_cookies()
        except RuntimeError:
            pass  # unbound cannot load

    @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 get_argument(cls, key):
        return cls.arguments.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

    @classmethod
    def broken(cls, issue=None):
        def func(*args, **kwargs):
            msg = (
                "This plugin has been marked as broken. This is likely due to "
                "changes to the service preventing a working implementation. ")

            if issue:
                msg += "More info: https://github.com/streamlink/streamlink/issues/{0}".format(
                    issue)

            raise PluginError(msg)

        def decorator(*args, **kwargs):
            return func

        return decorator

    @classmethod
    def priority(cls, url):
        """
        Return the plugin priority for a given URL, by default it returns
        NORMAL priority.
        :return: priority level
        """
        return NORMAL_PRIORITY

    def streams(self, stream_types=None, sorting_excludes=None):
        """Attempts to extract available streams.

        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.

        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.

        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>".

        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.

        """

        try:
            ostreams = self._get_streams()
            if isinstance(ostreams, dict):
                ostreams = ostreams.items()

            # Flatten the iterator to a list so we can reuse it.
            if ostreams:
                ostreams = list(ostreams)
        except NoStreamsError:
            return {}
        except (IOError, OSError, ValueError) as err:
            raise PluginError(err)

        if not ostreams:
            return {}

        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),
                                key=partial(stream_type_priority,
                                            stream_types))

        streams = {}
        for name, stream in sorted_streams:
            stream_type = type(stream).shortname()

            # Use * as wildcard to match other stream types
            if "*" not in stream_types and stream_type not in stream_types:
                continue

            # drop _alt from any stream names
            if name.endswith("_alt"):
                name = name[:-len("_alt")]

            existing = streams.get(name)
            if existing:
                existing_stream_type = type(existing).shortname()
                if existing_stream_type != stream_type:
                    name = "{0}_{1}".format(name, stream_type)

                if name in streams:
                    name = "{0}_alt".format(name)
                    num_alts = len(
                        list(
                            filter(lambda n: n.startswith(name),
                                   streams.keys())))

                    # We shouldn't need more than 2 alt streams
                    if num_alts >= 2:
                        continue
                    elif num_alts > 0:
                        name = "{0}{1}".format(name, num_alts + 1)

            # Validate stream name and discard the stream if it's bad.
            match = re.match("([A-z0-9_+]+)", name)
            if match:
                name = match.group(1)
            else:
                self.logger.debug(
                    "The stream '{0}' has been ignored "
                    "since it is badly named.", name)
                continue

            # Force lowercase name and replace space with underscore.
            streams[name.lower()] = stream

        # Create the best/worst synonmys
        def stream_weight_only(s):
            return (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))

        final_sorted_streams = OrderedDict()

        for stream_name in sorted(streams, key=stream_weight_only):
            final_sorted_streams[stream_name] = streams[stream_name]

        if len(sorted_streams) > 0:
            best = sorted_streams[-1]
            worst = sorted_streams[0]
            final_sorted_streams["worst"] = streams[worst]
            final_sorted_streams["best"] = streams[best]

        return final_sorted_streams

    def _get_streams(self):
        raise NotImplementedError

    def get_title(self):
        return None

    def get_author(self):
        return None

    def get_category(self):
        return None

    def save_cookies(self,
                     cookie_filter=None,
                     default_expires=60 * 60 * 24 * 7):
        """
        Store the cookies from ``http`` in the plugin cache until they expire. The cookies can be filtered
        by supplying a filter method. eg. ``lambda c: "auth" in c.name``. If no expiry date is given in the
        cookie then the ``default_expires`` value will be used.

        :param cookie_filter: a function to filter the cookies
        :type cookie_filter: function
        :param default_expires: time (in seconds) until cookies with no expiry will expire
        :type default_expires: int
        :return: list of the saved cookie names
        """
        if not self.session or not self.cache:
            raise RuntimeError("Cannot cache cookies in unbound plugin")

        cookie_filter = cookie_filter or (lambda c: True)
        saved = []

        for cookie in filter(cookie_filter, self.session.http.cookies):
            cookie_dict = {}
            for attr in ("version", "name", "value", "port", "domain", "path",
                         "secure", "expires", "discard", "comment",
                         "comment_url", "rfc2109"):
                cookie_dict[attr] = getattr(cookie, attr, None)
            cookie_dict["rest"] = getattr(cookie, "rest",
                                          getattr(cookie, "_rest", None))

            expires = default_expires
            if cookie_dict['expires']:
                expires = int(cookie_dict['expires'] - time.time())
            key = "__cookie:{0}:{1}:{2}:{3}".format(
                cookie.name, cookie.domain,
                cookie.port_specified and cookie.port or "80",
                cookie.path_specified and cookie.path or "*")
            self.cache.set(key, cookie_dict, expires)
            saved.append(cookie.name)

        if saved:
            self.logger.debug("Saved cookies: {0}".format(", ".join(saved)))
        return saved

    def load_cookies(self):
        """
        Load any stored cookies for the plugin that have not expired.

        :return: list of the restored cookie names
        """
        if not self.session or not self.cache:
            raise RuntimeError(
                "Cannot loaded cached cookies in unbound plugin")

        restored = []

        for key, value in self.cache.get_all().items():
            if key.startswith("__cookie"):
                cookie = requests.cookies.create_cookie(**value)
                self.session.http.cookies.set_cookie(cookie)
                restored.append(cookie.name)

        if restored:
            self.logger.debug("Restored cookies: {0}".format(
                ", ".join(restored)))
        return restored

    def clear_cookies(self, cookie_filter=None):
        """
        Removes all of the saved cookies for this Plugin. To filter the cookies that are deleted
        specify the ``cookie_filter`` argument (see :func:`save_cookies`).

        :param cookie_filter: a function to filter the cookies
        :type cookie_filter: function
        :return: list of the removed cookie names
        """
        if not self.session or not self.cache:
            raise RuntimeError(
                "Cannot loaded cached cookies in unbound plugin")

        cookie_filter = cookie_filter or (lambda c: True)
        removed = []

        for key, value in sorted(self.cache.get_all().items(),
                                 key=operator.itemgetter(0),
                                 reverse=True):
            if key.startswith("__cookie"):
                cookie = requests.cookies.create_cookie(**value)
                if cookie_filter(cookie):
                    del self.session.http.cookies[cookie.name]
                    self.cache.set(key, None, 0)
                    removed.append(key)

        return removed

    def input_ask(self, prompt):
        if self._user_input_requester:
            try:
                return self._user_input_requester.ask(prompt)
            except IOError as e:
                raise FatalPluginError("User input error: {0}".format(e))
            except NotImplementedError:  # ignore this and raise a FatalPluginError
                pass
        raise FatalPluginError(
            "This plugin requires user input, however it is not supported on this platform"
        )

    def input_ask_password(self, prompt):
        if self._user_input_requester:
            try:
                return self._user_input_requester.ask_password(prompt)
            except IOError as e:
                raise FatalPluginError("User input error: {0}".format(e))
            except NotImplementedError:  # ignore this and raise a FatalPluginError
                pass
        raise FatalPluginError(
            "This plugin requires user input, however it is not supported on this platform"
        )
Beispiel #12
0
 def setUp(self):
     self.options = Options({
         "a_default": "default"
     })
Beispiel #13
0
class Streamlink:
    """A Streamlink session is used to keep track of plugins,
       options and log settings."""
    def __init__(self, options=None):
        self.http = api.HTTPSession()
        self.options = Options({
            "hds-live-edge": 10.0,
            "hds-segment-attempts": 3,
            "hds-segment-threads": 1,
            "hds-segment-timeout": 10.0,
            "hds-timeout": 60.0,
            "hls-live-edge": 3,
            "hls-segment-attempts": 3,
            "hls-segment-threads": 1,
            "hls-segment-timeout": 10.0,
            "hls-segment-stream-data": False,
            "hls-timeout": 60.0,
            "hls-playlist-reload-attempts": 3,
            "hls-playlist-reload-time": "default",
            "hls-start-offset": 0,
            "hls-duration": None,
            "http-stream-timeout": 60.0,
            "ringbuffer-size": 1024 * 1024 * 16,  # 16 MB
            "rtmp-timeout": 60.0,
            "rtmp-rtmpdump": is_win32 and "rtmpdump.exe" or "rtmpdump",
            "rtmp-proxy": None,
            "stream-segment-attempts": 3,
            "stream-segment-threads": 1,
            "stream-segment-timeout": 10.0,
            "stream-timeout": 60.0,
            "subprocess-errorlog": False,
            "subprocess-errorlog-path": None,
            "ffmpeg-ffmpeg": None,
            "ffmpeg-fout": None,
            "ffmpeg-video-transcode": None,
            "ffmpeg-audio-transcode": None,
            "ffmpeg-start-at-zero": None,
            "mux-subtitles": False,
            "locale": None,
            "user-input-requester": None
        })
        if options:
            self.options.update(options)
        self.plugins = OrderedDict({})
        self.load_builtin_plugins()

    def set_option(self, key, value):
        """Sets general options used by plugins and streams originating
        from this session object.

        :param key: key of the option
        :param value: value to set the option to


        **Available options**:

        ======================== =========================================
        hds-live-edge            ( float) Specify the time live HDS
                                 streams will start from the edge of
                                 stream, default: ``10.0``

        hds-segment-attempts     (int) How many attempts should be done
                                 to download each HDS segment, default: ``3``

        hds-segment-threads      (int) The size of the thread pool used
                                 to download segments, default: ``1``

        hds-segment-timeout      (float) HDS segment connect and read
                                 timeout, default: ``10.0``

        hds-timeout              (float) Timeout for reading data from
                                 HDS streams, default: ``60.0``

        hls-live-edge            (int) How many segments from the end
                                 to start live streams on, default: ``3``

        hls-segment-attempts     (int) How many attempts should be done
                                 to download each HLS segment, default: ``3``

        hls-segment-threads      (int) The size of the thread pool used
                                 to download segments, default: ``1``

        hls-segment-stream-data  (bool) Stream HLS segment downloads,
                                 default: ``False``

        hls-segment-timeout      (float) HLS segment connect and read
                                 timeout, default: ``10.0``

        hls-timeout              (float) Timeout for reading data from
                                 HLS streams, default: ``60.0``

        http-proxy               (str) Specify a HTTP proxy to use for
                                 all HTTP requests

        https-proxy              (str) Specify a HTTPS proxy to use for
                                 all HTTPS requests

        http-cookies             (dict or str) A dict or a semi-colon (;)
                                 delimited str of cookies to add to each
                                 HTTP request, e.g. ``foo=bar;baz=qux``

        http-headers             (dict or str) A dict or semi-colon (;)
                                 delimited str of headers to add to each
                                 HTTP request, e.g. ``foo=bar;baz=qux``

        http-query-params        (dict or str) A dict or a ampersand (&)
                                 delimited string of query parameters to
                                 add to each HTTP request,
                                 e.g. ``foo=bar&baz=qux``

        http-trust-env           (bool) Trust HTTP settings set in the
                                 environment, such as environment
                                 variables (HTTP_PROXY, etc) and
                                 ~/.netrc authentication

        http-ssl-verify          (bool) Verify SSL certificates,
                                 default: ``True``

        http-ssl-cert            (str or tuple) SSL certificate to use,
                                 can be either a .pem file (str) or a
                                 .crt/.key pair (tuple)

        http-timeout             (float) General timeout used by all HTTP
                                 requests except the ones covered by
                                 other options, default: ``20.0``

        http-stream-timeout      (float) Timeout for reading data from
                                 HTTP streams, default: ``60.0``

        subprocess-errorlog      (bool) Log errors from subprocesses to
                                 a file located in the temp directory

        subprocess-errorlog-path (str) Log errors from subprocesses to
                                 a specific file

        ringbuffer-size          (int) The size of the internal ring
                                 buffer used by most stream types,
                                 default: ``16777216`` (16MB)

        rtmp-proxy               (str) Specify a proxy (SOCKS) that RTMP
                                 streams will use

        rtmp-rtmpdump            (str) Specify the location of the
                                 rtmpdump executable used by RTMP streams,
                                 e.g. ``/usr/local/bin/rtmpdump``

        rtmp-timeout             (float) Timeout for reading data from
                                 RTMP streams, default: ``60.0``

        ffmpeg-ffmpeg            (str) Specify the location of the
                                 ffmpeg executable use by Muxing streams
                                 e.g. ``/usr/local/bin/ffmpeg``

        ffmpeg-verbose           (bool) Log stderr from ffmpeg to the
                                 console

        ffmpeg-verbose-path      (str) Specify the location of the
                                 ffmpeg stderr log file

        ffmpeg-fout              (str) The output file format
                                 when muxing with ffmpeg
                                 e.g. ``matroska``

        ffmpeg-video-transcode   (str) The codec to use if transcoding
                                 video when muxing with ffmpeg
                                 e.g. ``h264``

        ffmpeg-audio-transcode   (str) The codec to use if transcoding
                                 audio when muxing with ffmpeg
                                 e.g. ``aac``

        ffmpeg-start-at-zero     (bool) When used with ffmpeg and copyts,
                                 shift input timestamps so they start at zero

        mux-subtitles            (bool) Mux available subtitles into the
                                 output stream.

        stream-segment-attempts  (int) How many attempts should be done
                                 to download each segment, default: ``3``.
                                 General option used by streams not
                                 covered by other options.

        stream-segment-threads   (int) The size of the thread pool used
                                 to download segments, default: ``1``.
                                 General option used by streams not
                                 covered by other options.

        stream-segment-timeout   (float) Segment connect and read
                                 timeout, default: ``10.0``.
                                 General option used by streams not
                                 covered by other options.

        stream-timeout           (float) Timeout for reading data from
                                 stream, default: ``60.0``.
                                 General option used by streams not
                                 covered by other options.

        locale                   (str) Locale setting, in the RFC 1766 format
                                 eg. en_US or es_ES
                                 default: ``system locale``.

        user-input-requester     (UserInputRequester) instance of UserInputRequester
                                 to collect input from the user at runtime. Must be
                                 set before the plugins are loaded.
                                 default: ``UserInputRequester``.
        ======================== =========================================

        """

        if key == "http-proxy":
            self.http.proxies["http"] = update_scheme("http://", value)
            if "https" not in self.http.proxies:
                self.http.proxies["https"] = update_scheme("http://", value)

        elif key == "https-proxy":
            self.http.proxies["https"] = update_scheme("https://", value)
        elif key == "http-cookies":
            if isinstance(value, dict):
                self.http.cookies.update(value)
            else:
                self.http.parse_cookies(value)
        elif key == "http-headers":
            if isinstance(value, dict):
                self.http.headers.update(value)
            else:
                self.http.parse_headers(value)
        elif key == "http-query-params":
            if isinstance(value, dict):
                self.http.params.update(value)
            else:
                self.http.parse_query_params(value)
        elif key == "http-trust-env":
            self.http.trust_env = value
        elif key == "http-ssl-verify":
            self.http.verify = value
        elif key == "http-disable-dh":
            if value:
                requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':!DH'
                try:
                    requests.packages.urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST = \
                        requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS.encode("ascii")
                except AttributeError:
                    # no ssl to disable the cipher on
                    pass
        elif key == "http-ssl-cert":
            self.http.cert = value
        elif key == "http-timeout":
            self.http.timeout = value
        else:
            self.options.set(key, value)

    def get_option(self, key):
        """Returns current value of specified option.

        :param key: key of the option

        """

        if key == "http-proxy":
            return self.http.proxies.get("http")
        elif key == "https-proxy":
            return self.http.proxies.get("https")
        elif key == "http-cookies":
            return self.http.cookies
        elif key == "http-headers":
            return self.http.headers
        elif key == "http-query-params":
            return self.http.params
        elif key == "http-trust-env":
            return self.http.trust_env
        elif key == "http-ssl-verify":
            return self.http.verify
        elif key == "http-ssl-cert":
            return self.http.cert
        elif key == "http-timeout":
            return self.http.timeout
        else:
            return self.options.get(key)

    def set_plugin_option(self, plugin, key, value):
        """Sets plugin specific options used by plugins originating
        from this session object.

        :param plugin: name of the plugin
        :param key: key of the option
        :param value: value to set the option to

        """

        if plugin in self.plugins:
            plugin = self.plugins[plugin]
            plugin.set_option(key, value)

    def get_plugin_option(self, plugin, key):
        """Returns current value of plugin specific option.

        :param plugin: name of the plugin
        :param key: key of the option

        """

        if plugin in self.plugins:
            plugin = self.plugins[plugin]
            return plugin.get_option(key)

    @memoize
    def resolve_url(self, url, follow_redirect=True):
        """Attempts to find a plugin that can use this URL.

        The default protocol (http) will be prefixed to the URL if
        not specified.

        Raises :exc:`NoPluginError` on failure.

        :param url: a URL to match against loaded plugins
        :param follow_redirect: follow redirects

        """
        url = update_scheme("http://", url)

        available_plugins = []
        for name, plugin in self.plugins.items():
            if plugin.can_handle_url(url):
                available_plugins.append(plugin)

        available_plugins.sort(key=lambda x: x.priority(url), reverse=True)
        if available_plugins:
            return available_plugins[0](url)

        if follow_redirect:
            # Attempt to handle a redirect URL
            try:
                res = self.http.head(url,
                                     allow_redirects=True,
                                     acceptable_status=[501])

                # Fall back to GET request if server doesn't handle HEAD.
                if res.status_code == 501:
                    res = self.http.get(url, stream=True)

                if res.url != url:
                    return self.resolve_url(res.url,
                                            follow_redirect=follow_redirect)
            except PluginError:
                pass

        raise NoPluginError

    def resolve_url_no_redirect(self, url):
        """Attempts to find a plugin that can use this URL.

        The default protocol (http) will be prefixed to the URL if
        not specified.

        Raises :exc:`NoPluginError` on failure.

        :param url: a URL to match against loaded plugins

        """
        return self.resolve_url(url, follow_redirect=False)

    def streams(self, url, **params):
        """Attempts to find a plugin and extract streams from the *url*.

        *params* are passed to :func:`Plugin.streams`.

        Raises :exc:`NoPluginError` if no plugin is found.
        """

        plugin = self.resolve_url(url)
        return plugin.streams(**params)

    def get_plugins(self):
        """Returns the loaded plugins for the session."""

        return self.plugins

    def load_builtin_plugins(self):
        self.load_plugins(plugins.__path__[0])

    def load_plugins(self, path):
        """Attempt to load plugins from the path specified.

        :param path: full path to a directory where to look for plugins

        """
        for loader, name, ispkg in pkgutil.iter_modules([path]):
            file, pathname, desc = imp.find_module(name, [path])
            # set the full plugin module name
            module_name = "streamlink.plugin.{0}".format(name)

            try:
                self.load_plugin(module_name, file, pathname, desc)
            except Exception:
                sys.stderr.write("Failed to load plugin {0}:\n".format(name))
                print_small_exception("load_plugin")

                continue

    def load_plugin(self, name, file, pathname, desc):
        # Set the global http session for this plugin
        user_input_requester = self.get_option("user-input-requester")
        api.http = self.http

        module = imp.load_module(name, file, pathname, desc)

        if hasattr(module, "__plugin__"):
            module_name = getattr(module, "__name__")
            plugin_name = module_name.split(".")[
                -1]  # get the plugin part of the module name

            plugin = getattr(module, "__plugin__")
            plugin.bind(self, plugin_name, user_input_requester)

            if plugin.module in self.plugins:
                log.debug("Plugin {0} is being overridden by {1}".format(
                    plugin.module, pathname))

            self.plugins[plugin.module] = plugin

        if file:
            file.close()

    @property
    def version(self):
        return __version__

    @property
    def localization(self):
        return Localization(self.get_option("locale"))
Beispiel #14
0
 def setUp(self):
     self.options = Options({
         "a_default": "default",
         "another-default": "default2"
     })
Beispiel #15
0
 def setUp(self):
     self.options = Options({
         "a_default": "default",
         "another-default": "default2"
     })
Beispiel #16
0
class Streamlink:
    """A Streamlink session is used to keep track of plugins,
       options and log settings."""
    def __init__(self, options=None):
        self.http = HTTPSession()
        self.options = Options({
            "interface": None,
            "ipv4": False,
            "ipv6": False,
            "hls-live-edge": 3,
            "hls-segment-ignore-names": [],
            "hls-playlist-reload-attempts": 3,
            "hls-playlist-reload-time":
            "default",  # default, duration, segment, average
            "hls-start-offset": 0,
            "hls-duration": None,
            "hls-live-restart": False,
            "ringbuffer-size": 1024 * 1024 * 16,  # 16 MB
            "stream-segment-attempts": 3,
            "stream-segment-threads": 1,
            "stream-segment-timeout": 10.0,
            "stream-timeout": 60.0,
            "ffmpeg-ffmpeg": None,
            "ffmpeg-fout": None,
            "ffmpeg-video-transcode": None,
            "ffmpeg-audio-transcode": None,
            "ffmpeg-copyts": False,
            "ffmpeg-start-at-zero": False,
            "mux-subtitles": False,
            "locale": None,
            "user-input-requester": None,
            "chunk-size": 8192
        })
        if options:
            self.options.update(options)
        self.plugins: Dict[str, Type[Plugin]] = OrderedDict({})
        self.load_builtin_plugins()

    def set_option(self, key, value):
        """Sets general options used by plugins and streams originating
        from this session object.

        :param key: key of the option
        :param value: value to set the option to


        **Available options**:

        ======================== =========================================
        interface                (str) Set the network interface,
                                 default: ``None``
        ipv4                     (bool) Resolve address names to IPv4 only.
                                 This option overrides ipv6, default: ``False``
        ipv6                     (bool) Resolve address names to IPv6 only.
                                 This option overrides ipv4, default: ``False``

        hls-live-edge            (int) How many segments from the end
                                 to start live streams on, default: ``3``

        hls-segment-ignore-names (str[]) List of segment names without
                                 file endings which should get filtered out,
                                 default: ``[]``

        hls-segment-stream-data  (bool) Stream HLS segment downloads,
                                 default: ``False``

        http-proxy               (str) Specify a HTTP proxy to use for
                                 all HTTP requests

        https-proxy              (str) Specify a HTTPS proxy to use for
                                 all HTTPS requests

        http-cookies             (dict or str) A dict or a semi-colon (;)
                                 delimited str of cookies to add to each
                                 HTTP request, e.g. ``foo=bar;baz=qux``

        http-headers             (dict or str) A dict or semi-colon (;)
                                 delimited str of headers to add to each
                                 HTTP request, e.g. ``foo=bar;baz=qux``

        http-query-params        (dict or str) A dict or a ampersand (&)
                                 delimited string of query parameters to
                                 add to each HTTP request,
                                 e.g. ``foo=bar&baz=qux``

        http-trust-env           (bool) Trust HTTP settings set in the
                                 environment, such as environment
                                 variables (HTTP_PROXY, etc) and
                                 ~/.netrc authentication

        http-ssl-verify          (bool) Verify SSL certificates,
                                 default: ``True``

        http-ssl-cert            (str or tuple) SSL certificate to use,
                                 can be either a .pem file (str) or a
                                 .crt/.key pair (tuple)

        http-timeout             (float) General timeout used by all HTTP
                                 requests except the ones covered by
                                 other options, default: ``20.0``

        ringbuffer-size          (int) The size of the internal ring
                                 buffer used by most stream types,
                                 default: ``16777216`` (16MB)

        ffmpeg-ffmpeg            (str) Specify the location of the
                                 ffmpeg executable use by Muxing streams
                                 e.g. ``/usr/local/bin/ffmpeg``

        ffmpeg-verbose           (bool) Log stderr from ffmpeg to the
                                 console

        ffmpeg-verbose-path      (str) Specify the location of the
                                 ffmpeg stderr log file

        ffmpeg-fout              (str) The output file format
                                 when muxing with ffmpeg
                                 e.g. ``matroska``

        ffmpeg-video-transcode   (str) The codec to use if transcoding
                                 video when muxing with ffmpeg
                                 e.g. ``h264``

        ffmpeg-audio-transcode   (str) The codec to use if transcoding
                                 audio when muxing with ffmpeg
                                 e.g. ``aac``

        ffmpeg-copyts            (bool) When used with ffmpeg, do not shift input timestamps.

        ffmpeg-start-at-zero     (bool) When used with ffmpeg and copyts,
                                 shift input timestamps so they start at zero
                                 default: ``False``

        mux-subtitles            (bool) Mux available subtitles into the
                                 output stream.

        stream-segment-attempts  (int) How many attempts should be done
                                 to download each segment, default: ``3``.

        stream-segment-threads   (int) The size of the thread pool used
                                 to download segments, default: ``1``.

        stream-segment-timeout   (float) Segment connect and read
                                 timeout, default: ``10.0``.

        stream-timeout           (float) Timeout for reading data from
                                 stream, default: ``60.0``.

        locale                   (str) Locale setting, in the RFC 1766 format
                                 eg. en_US or es_ES
                                 default: ``system locale``.

        user-input-requester     (UserInputRequester) instance of UserInputRequester
                                 to collect input from the user at runtime. Must be
                                 set before the plugins are loaded.
                                 default: ``UserInputRequester``.
        ======================== =========================================

        """

        if key == "interface":
            for scheme, adapter in self.http.adapters.items():
                if scheme not in ("http://", "https://"):
                    continue
                if not value:
                    adapter.poolmanager.connection_pool_kw.pop(
                        "source_address")
                else:
                    adapter.poolmanager.connection_pool_kw.update(
                        # https://docs.python.org/3/library/socket.html#socket.create_connection
                        source_address=(value, 0))
            self.options.set(key, None if not value else value)

        elif key == "ipv4" or key == "ipv6":
            self.options.set(key, value)
            if value:
                self.options.set("ipv6" if key == "ipv4" else "ipv4", False)
                urllib3_connection.allowed_gai_family = \
                    (lambda: AF_INET) if key == "ipv4" else (lambda: AF_INET6)
            else:
                urllib3_connection.allowed_gai_family = allowed_gai_family

        elif key in ("http-proxy", "https-proxy"):
            self.http.proxies["http"] = update_scheme("https://",
                                                      value,
                                                      force=False)
            self.http.proxies["https"] = self.http.proxies["http"]
            if key == "https-proxy":
                log.info(
                    "The https-proxy option has been deprecated in favour of a single http-proxy option"
                )

        elif key == "http-cookies":
            if isinstance(value, dict):
                self.http.cookies.update(value)
            else:
                self.http.parse_cookies(value)
        elif key == "http-headers":
            if isinstance(value, dict):
                self.http.headers.update(value)
            else:
                self.http.parse_headers(value)
        elif key == "http-query-params":
            if isinstance(value, dict):
                self.http.params.update(value)
            else:
                self.http.parse_query_params(value)
        elif key == "http-trust-env":
            self.http.trust_env = value
        elif key == "http-ssl-verify":
            self.http.verify = value
        elif key == "http-disable-dh":
            if value:
                requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':!DH'
                try:
                    requests.packages.urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST = \
                        requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS.encode("ascii")
                except AttributeError:
                    # no ssl to disable the cipher on
                    pass
        elif key == "http-ssl-cert":
            self.http.cert = value
        elif key == "http-timeout":
            self.http.timeout = value

        # deprecated: {dash,hls}-segment-attempts
        elif key in ("dash-segment-attempts", "hls-segment-attempts"):
            self.options.set("stream-segment-attempts", int(value))
        # deprecated: {dash,hls}-segment-threads
        elif key in ("dash-segment-threads", "hls-segment-threads"):
            self.options.set("stream-segment-threads", int(value))
        # deprecated: {dash,hls}-segment-timeout
        elif key in ("dash-segment-timeout", "hls-segment-timeout"):
            self.options.set("stream-segment-timeout", float(value))
        # deprecated: {hls,dash,http-stream}-timeout
        elif key in ("dash-timeout", "hls-timeout", "http-stream-timeout"):
            self.options.set("stream-timeout", float(value))

        else:
            self.options.set(key, value)

    def get_option(self, key):
        """Returns current value of specified option.

        :param key: key of the option

        """
        if key == "http-proxy":
            return self.http.proxies.get("http")
        elif key == "https-proxy":
            return self.http.proxies.get("https")
        elif key == "http-cookies":
            return self.http.cookies
        elif key == "http-headers":
            return self.http.headers
        elif key == "http-query-params":
            return self.http.params
        elif key == "http-trust-env":
            return self.http.trust_env
        elif key == "http-ssl-verify":
            return self.http.verify
        elif key == "http-ssl-cert":
            return self.http.cert
        elif key == "http-timeout":
            return self.http.timeout
        else:
            return self.options.get(key)

    def set_plugin_option(self, plugin, key, value):
        """Sets plugin specific options used by plugins originating
        from this session object.

        :param plugin: name of the plugin
        :param key: key of the option
        :param value: value to set the option to

        """

        if plugin in self.plugins:
            plugin = self.plugins[plugin]
            plugin.set_option(key, value)

    def get_plugin_option(self, plugin, key):
        """Returns current value of plugin specific option.

        :param plugin: name of the plugin
        :param key: key of the option

        """

        if plugin in self.plugins:
            plugin = self.plugins[plugin]
            return plugin.get_option(key)

    @lru_cache(maxsize=128)
    def resolve_url(self,
                    url: str,
                    follow_redirect: bool = True) -> Tuple[Type[Plugin], str]:
        """Attempts to find a plugin that can use this URL.

        The default protocol (https) will be prefixed to the URL if
        not specified.

        Raises :exc:`NoPluginError` on failure.

        :param url: a URL to match against loaded plugins
        :param follow_redirect: follow redirects

        """
        url = update_scheme("https://", url, force=False)

        matcher: Matcher
        candidate: Optional[Type[Plugin]] = None
        priority = NO_PRIORITY
        for name, plugin in self.plugins.items():
            if plugin.matchers:
                for matcher in plugin.matchers:
                    if matcher.priority > priority and matcher.pattern.match(
                            url) is not None:
                        candidate = plugin
                        priority = matcher.priority
            # TODO: remove deprecated plugin resolver
            elif hasattr(plugin, "can_handle_url") and callable(
                    plugin.can_handle_url) and plugin.can_handle_url(url):
                prio = plugin.priority(url) if hasattr(
                    plugin, "priority") and callable(
                        plugin.priority) else NORMAL_PRIORITY
                if prio > priority:
                    log.info(
                        f"Resolved plugin {name} with deprecated can_handle_url API"
                    )
                    candidate = plugin
                    priority = prio

        if candidate:
            return candidate, url

        if follow_redirect:
            # Attempt to handle a redirect URL
            try:
                # noinspection PyArgumentList
                res = self.http.head(url,
                                     allow_redirects=True,
                                     acceptable_status=[501])

                # Fall back to GET request if server doesn't handle HEAD.
                if res.status_code == 501:
                    res = self.http.get(url, stream=True)

                if res.url != url:
                    return self.resolve_url(res.url,
                                            follow_redirect=follow_redirect)
            except PluginError:
                pass

        raise NoPluginError

    def resolve_url_no_redirect(self, url: str) -> Tuple[Type[Plugin], str]:
        """Attempts to find a plugin that can use this URL.

        The default protocol (https) will be prefixed to the URL if
        not specified.

        Raises :exc:`NoPluginError` on failure.

        :param url: a URL to match against loaded plugins

        """
        return self.resolve_url(url, follow_redirect=False)

    def streams(self, url: str, **params):
        """Attempts to find a plugin and extract streams from the *url*.

        *params* are passed to :func:`Plugin.streams`.

        Raises :exc:`NoPluginError` if no plugin is found.
        """

        pluginclass, resolved_url = self.resolve_url(url)
        plugin = pluginclass(resolved_url)

        return plugin.streams(**params)

    def get_plugins(self):
        """Returns the loaded plugins for the session."""

        return self.plugins

    def load_builtin_plugins(self):
        self.load_plugins(plugins.__path__[0])

    def load_plugins(self, path: str) -> bool:
        """Attempt to load plugins from the path specified.

        :param path: full path to a directory where to look for plugins
        :return: success
        """
        success = False
        user_input_requester = self.get_option("user-input-requester")
        for loader, name, ispkg in pkgutil.iter_modules([path]):
            # set the full plugin module name
            module_name = f"streamlink.plugins.{name}"
            try:
                mod = load_module(module_name, path)
            except ImportError:
                log.exception(f"Failed to load plugin {name} from {path}\n")
                continue

            if not hasattr(mod, "__plugin__") or not issubclass(
                    mod.__plugin__, Plugin):
                continue
            success = True
            plugin = mod.__plugin__
            plugin.bind(self, name, user_input_requester)
            if plugin.module in self.plugins:
                log.debug(
                    f"Plugin {plugin.module} is being overridden by {mod.__file__}"
                )
            self.plugins[plugin.module] = plugin

        return success

    @property
    def version(self):
        return __version__

    @property
    def localization(self):
        return Localization(self.get_option("locale"))
Beispiel #17
0
class Plugin:
    """
    Plugin base class for retrieving streams and metadata from the URL specified.
    """

    matchers: ClassVar[List[Matcher]] = None
    """
    The list of plugin matchers (URL pattern + priority).
    Use the :meth:`streamlink.plugin.pluginmatcher` decorator for initializing this list.
    """

    matches: Sequence[Optional[Match]]
    """A tuple of :class:`re.Match` results of all defined matchers"""

    matcher: Pattern
    """A reference to the compiled :class:`re.Pattern` of the first matching matcher"""

    match: Match
    """A reference to the :class:`re.Match` result of the first matching matcher"""

    # plugin metadata attributes
    id: Optional[str] = None
    """Metadata 'id' attribute: unique stream ID, etc."""
    title: Optional[str] = None
    """Metadata 'title' attribute: the stream's short descriptive title"""
    author: Optional[str] = None
    """Metadata 'author' attribute: the channel or broadcaster name, etc."""
    category: Optional[str] = None
    """Metadata 'category' attribute: name of a game being played, a music genre, etc."""

    cache = None
    logger = None
    module = "unknown"
    options = Options()
    arguments = Arguments()
    session = None
    _url: str = None
    _user_input_requester = None

    @classmethod
    def bind(cls, session, module, user_input_requester=None):
        cls.cache = Cache(filename="plugin-cache.json",
                          key_prefix=module)
        cls.logger = logging.getLogger("streamlink.plugins." + module)
        cls.module = module
        cls.session = session
        if user_input_requester is not None:
            if isinstance(user_input_requester, UserInputRequester):
                cls._user_input_requester = user_input_requester
            else:
                raise RuntimeError("user-input-requester must be an instance of UserInputRequester")

    @property
    def url(self) -> str:
        """
        The plugin's input URL.
        Setting a new value will automatically update the :attr:`matches`, :attr:`matcher` and :attr:`match` data.
        """

        return self._url

    @url.setter
    def url(self, value: str):
        self._url = value

        matches = [(pattern, pattern.match(value)) for pattern, priority in self.matchers or []]
        self.matches = tuple(m for p, m in matches)
        self.matcher, self.match = next(((p, m) for p, m in matches if m is not None), (None, None))

    def __init__(self, url: str):
        """
        :param str url: URL that the plugin will operate on
        """

        self.url = url

        try:
            self.load_cookies()
        except RuntimeError:
            pass  # unbound cannot load

    @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 get_argument(cls, key):
        return cls.arguments.get(key)

    @classmethod
    def stream_weight(cls, stream):
        return stream_weight(stream)

    @classmethod
    def default_stream_types(cls, streams):
        stream_types = ["hls", "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

    @classmethod
    def broken(cls, issue=None):
        def func(*args, **kwargs):
            msg = (
                "This plugin has been marked as broken. This is likely due to "
                "changes to the service preventing a working implementation. "
            )

            if issue:
                msg += "More info: https://github.com/streamlink/streamlink/issues/{0}".format(issue)

            raise PluginError(msg)

        def decorator(*args, **kwargs):
            return func

        return decorator

    def streams(self, stream_types=None, sorting_excludes=None):
        """
        Attempts to extract available streams.

        Returns a :class:`dict` containing the streams, where the key is
        the name of the stream (most commonly the quality name), with the value
        being a :class:`Stream` instance.

        The result can contain the synonyms **best** and **worst** which
        point to the streams which are likely to be of highest and
        lowest quality respectively.

        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>".

        The synonyms can be fine-tuned with the *sorting_excludes*
        parameter, which can be one 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 :meth:`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
        :returns: A :class:`dict` of stream names and :class:`streamlink.stream.Stream` instances
        """

        try:
            ostreams = self._get_streams()
            if isinstance(ostreams, dict):
                ostreams = ostreams.items()

            # Flatten the iterator to a list so we can reuse it.
            if ostreams:
                ostreams = list(ostreams)
        except NoStreamsError:
            return {}
        except (OSError, ValueError) as err:
            raise PluginError(err)

        if not ostreams:
            return {}

        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),
                                key=partial(stream_type_priority,
                                            stream_types))

        streams = {}
        for name, stream in sorted_streams:
            stream_type = type(stream).shortname()

            # Use * as wildcard to match other stream types
            if "*" not in stream_types and stream_type not in stream_types:
                continue

            # drop _alt from any stream names
            if name.endswith("_alt"):
                name = name[:-len("_alt")]

            existing = streams.get(name)
            if existing:
                existing_stream_type = type(existing).shortname()
                if existing_stream_type != stream_type:
                    name = "{0}_{1}".format(name, stream_type)

                if name in streams:
                    name = "{0}_alt".format(name)
                    num_alts = len(list(filter(lambda n: n.startswith(name), streams.keys())))

                    # We shouldn't need more than 2 alt streams
                    if num_alts >= 2:
                        continue
                    elif num_alts > 0:
                        name = "{0}{1}".format(name, num_alts + 1)

            # Validate stream name and discard the stream if it's bad.
            match = re.match("([A-z0-9_+]+)", name)
            if match:
                name = match.group(1)
            else:
                self.logger.debug(f"The stream '{name}' has been ignored since it is badly named.")
                continue

            # Force lowercase name and replace space with underscore.
            streams[name.lower()] = stream

        # Create the best/worst synonyms
        def stream_weight_only(s):
            return (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)
        unfiltered_sorted_streams = sorted_streams

        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))

        final_sorted_streams = OrderedDict()

        for stream_name in sorted(streams, key=stream_weight_only):
            final_sorted_streams[stream_name] = streams[stream_name]

        if len(sorted_streams) > 0:
            best = sorted_streams[-1]
            worst = sorted_streams[0]
            final_sorted_streams["worst"] = streams[worst]
            final_sorted_streams["best"] = streams[best]
        elif len(unfiltered_sorted_streams) > 0:
            best = unfiltered_sorted_streams[-1]
            worst = unfiltered_sorted_streams[0]
            final_sorted_streams["worst-unfiltered"] = streams[worst]
            final_sorted_streams["best-unfiltered"] = streams[best]

        return final_sorted_streams

    def _get_streams(self):
        """
        Implement the stream and metadata retrieval here.

        Needs to return either a dict of :class:`streamlink.stream.Stream` instances mapped by stream name, or needs to act
        as a generator which yields tuples of stream names and :class:`streamlink.stream.Stream` instances.
        """

        raise NotImplementedError

    def get_metadata(self) -> Dict[str, Optional[str]]:
        return dict(
            id=self.get_id(),
            author=self.get_author(),
            category=self.get_category(),
            title=self.get_title()
        )

    def get_id(self) -> Optional[str]:
        return None if self.id is None else str(self.id).strip()

    def get_title(self) -> Optional[str]:
        return None if self.title is None else str(self.title).strip()

    def get_author(self) -> Optional[str]:
        return None if self.author is None else str(self.author).strip()

    def get_category(self) -> Optional[str]:
        return None if self.category is None else str(self.category).strip()

    def save_cookies(
        self,
        cookie_filter: Optional[Callable] = None,
        default_expires: int = 60 * 60 * 24 * 7
    ) -> List[str]:
        """
        Store the cookies from :attr:`session.http` in the plugin cache until they expire. The cookies can be filtered
        by supplying a filter method. e.g. ``lambda c: "auth" in c.name``. If no expiry date is given in the
        cookie then the ``default_expires`` value will be used.

        :param cookie_filter: a function to filter the cookies
        :param default_expires: time (in seconds) until cookies with no expiry will expire
        :return: list of the saved cookie names
        """

        if not self.session or not self.cache:
            raise RuntimeError("Cannot cache cookies in unbound plugin")

        cookie_filter = cookie_filter or (lambda c: True)
        saved = []

        for cookie in filter(cookie_filter, self.session.http.cookies):
            cookie_dict = {}
            for attr in ("version", "name", "value", "port", "domain", "path", "secure", "expires", "discard",
                         "comment", "comment_url", "rfc2109"):
                cookie_dict[attr] = getattr(cookie, attr, None)
            cookie_dict["rest"] = getattr(cookie, "rest", getattr(cookie, "_rest", None))

            expires = default_expires
            if cookie_dict['expires']:
                expires = int(cookie_dict['expires'] - time.time())
            key = "__cookie:{0}:{1}:{2}:{3}".format(cookie.name,
                                                    cookie.domain,
                                                    cookie.port_specified and cookie.port or "80",
                                                    cookie.path_specified and cookie.path or "*")
            self.cache.set(key, cookie_dict, expires)
            saved.append(cookie.name)

        if saved:
            self.logger.debug("Saved cookies: {0}".format(", ".join(saved)))
        return saved

    def load_cookies(self) -> List[str]:
        """
        Load any stored cookies for the plugin that have not expired.

        :return: list of the restored cookie names
        """

        if not self.session or not self.cache:
            raise RuntimeError("Cannot load cached cookies in unbound plugin")

        restored = []

        for key, value in self.cache.get_all().items():
            if key.startswith("__cookie"):
                cookie = requests.cookies.create_cookie(**value)
                self.session.http.cookies.set_cookie(cookie)
                restored.append(cookie.name)

        if restored:
            self.logger.debug("Restored cookies: {0}".format(", ".join(restored)))
        return restored

    def clear_cookies(self, cookie_filter: Optional[Callable] = None) -> List[str]:
        """
        Removes all saved cookies for this plugin. To filter the cookies that are deleted
        specify the ``cookie_filter`` argument (see :func:`save_cookies`).

        :param cookie_filter: a function to filter the cookies
        :type cookie_filter: function
        :return: list of the removed cookie names
        """

        if not self.session or not self.cache:
            raise RuntimeError("Cannot clear cached cookies in unbound plugin")

        cookie_filter = cookie_filter or (lambda c: True)
        removed = []

        for key, value in sorted(self.cache.get_all().items(), key=operator.itemgetter(0), reverse=True):
            if key.startswith("__cookie"):
                cookie = requests.cookies.create_cookie(**value)
                if cookie_filter(cookie):
                    del self.session.http.cookies[cookie.name]
                    self.cache.set(key, None, 0)
                    removed.append(key)

        return removed

    def input_ask(self, prompt):
        if self._user_input_requester:
            try:
                return self._user_input_requester.ask(prompt)
            except OSError as e:
                raise FatalPluginError("User input error: {0}".format(e))
            except NotImplementedError:  # ignore this and raise a FatalPluginError
                pass
        raise FatalPluginError("This plugin requires user input, however it is not supported on this platform")

    def input_ask_password(self, prompt):
        if self._user_input_requester:
            try:
                return self._user_input_requester.ask_password(prompt)
            except OSError as e:
                raise FatalPluginError("User input error: {0}".format(e))
            except NotImplementedError:  # ignore this and raise a FatalPluginError
                pass
        raise FatalPluginError("This plugin requires user input, however it is not supported on this platform")