streamlink/src/streamlink/session.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

665 lines
25 KiB
Python
Raw Normal View History

import logging
2013-08-08 15:01:32 +02:00
import pkgutil
import warnings
2021-01-12 21:38:45 +01:00
from functools import lru_cache
from socket import AF_INET, AF_INET6
from typing import Any, Callable, ClassVar, Dict, Iterator, Mapping, Optional, Tuple, Type
import urllib3.util.connection as urllib3_util_connection
import urllib3.util.ssl_ as urllib3_util_ssl
from streamlink import __version__, plugins
from streamlink.exceptions import NoPluginError, PluginError, StreamlinkDeprecationWarning
from streamlink.logger import StreamlinkLogger
from streamlink.options import Options
from streamlink.plugin.api.http_session import HTTPSession
from streamlink.plugin.plugin import NO_PRIORITY, NORMAL_PRIORITY, Matcher, Plugin
from streamlink.utils.l10n import Localization
from streamlink.utils.module import load_module
from streamlink.utils.url import update_scheme
# Ensure that the Logger class returned is Streamslink's for using the API (for backwards compatibility)
logging.setLoggerClass(StreamlinkLogger)
log = logging.getLogger(__name__)
2022-05-21 00:38:18 +02:00
_original_allowed_gai_family = urllib3_util_connection.allowed_gai_family # type: ignore[attr-defined]
def _get_deprecation_stacklevel_offset():
"""Deal with stacklevels of both session.{g,s}et_option() and session.options.{g,s}et() calls"""
from inspect import currentframe
frame = currentframe().f_back.f_back
offset = 0
while frame:
if frame.f_code.co_filename == __file__ and frame.f_code.co_name in ("set_option", "get_option"):
offset += 1
break
frame = frame.f_back
return offset
class PythonDeprecatedWarning(UserWarning):
pass
class StreamlinkOptions(Options):
def __init__(self, session: "Streamlink", *args, **kwargs):
super().__init__(*args, **kwargs)
self.session = session
# ---- utils
@staticmethod
def _parse_key_equals_value_string(delimiter: str, value: str) -> Iterator[Tuple[str, str]]:
for keyval in value.split(delimiter):
try:
key, val = keyval.split("=", 1)
yield key.strip(), val.strip()
except ValueError:
continue
@staticmethod
def _deprecate_https_proxy(key: str) -> None:
if key == "https-proxy":
warnings.warn(
"The `https-proxy` option has been deprecated in favor of a single `http-proxy` option",
StreamlinkDeprecationWarning,
stacklevel=4 + _get_deprecation_stacklevel_offset(),
)
# ---- getters
def _get_http_proxy(self, key):
self._deprecate_https_proxy(key)
return self.session.http.proxies.get("https" if key == "https-proxy" else "http")
def _get_http_attr(self, key):
return getattr(self.session.http, self._OPTIONS_HTTP_ATTRS[key])
# ---- setters
def _set_interface(self, key, value):
for scheme, adapter in self.session.http.adapters.items():
if scheme not in ("http://", "https://"):
continue
if not value:
adapter.poolmanager.connection_pool_kw.pop("source_address")
else:
# https://docs.python.org/3/library/socket.html#socket.create_connection
adapter.poolmanager.connection_pool_kw.update(source_address=(value, 0))
self.set_explicit(key, None if not value else value)
def _set_ipv4_ipv6(self, key, value):
self.set_explicit(key, value)
if not value:
urllib3_util_connection.allowed_gai_family = _original_allowed_gai_family # type: ignore[attr-defined]
elif key == "ipv4":
self.set_explicit("ipv6", False)
urllib3_util_connection.allowed_gai_family = (lambda: AF_INET) # type: ignore[attr-defined]
else:
self.set_explicit("ipv4", False)
urllib3_util_connection.allowed_gai_family = (lambda: AF_INET6) # type: ignore[attr-defined]
def _set_http_proxy(self, key, value):
self.session.http.proxies["http"] \
= self.session.http.proxies["https"] \
= update_scheme("https://", value, force=False)
self._deprecate_https_proxy(key)
def _set_http_attr(self, key, value):
setattr(self.session.http, self._OPTIONS_HTTP_ATTRS[key], value)
def _set_http_disable_dh(self, key, value):
self.set_explicit(key, value)
default_ciphers = [
item
for item in urllib3_util_ssl.DEFAULT_CIPHERS.split(":") # type: ignore[attr-defined]
if item != "!DH"
]
if value:
default_ciphers.append("!DH")
urllib3_util_ssl.DEFAULT_CIPHERS = ":".join(default_ciphers) # type: ignore[attr-defined]
@staticmethod
def _factory_set_http_attr_key_equals_value(delimiter: str) -> Callable[["StreamlinkOptions", str, Any], None]:
def inner(self: "StreamlinkOptions", key: str, value: Any) -> None:
getattr(self.session.http, self._OPTIONS_HTTP_ATTRS[key]).update(
value if isinstance(value, dict) else dict(self._parse_key_equals_value_string(delimiter, value)),
)
return inner
@staticmethod
def _factory_set_deprecated(name: str, mapper: Callable[[Any], Any]) -> Callable[["StreamlinkOptions", str, Any], None]:
def inner(self: "StreamlinkOptions", key: str, value: Any) -> None:
self.set_explicit(name, mapper(value))
warnings.warn(
f"`{key}` has been deprecated in favor of the `{name}` option",
StreamlinkDeprecationWarning,
stacklevel=3 + _get_deprecation_stacklevel_offset(),
)
return inner
# bind explicitly with dummy context, to prevent `TypeError: 'staticmethod' object is not callable` on py<310
_factory_set_http_attr_key_equals_value = _factory_set_http_attr_key_equals_value.__get__(object)
_factory_set_deprecated = _factory_set_deprecated.__get__(object)
# ----
2023-02-23 22:06:26 +01:00
_OPTIONS_HTTP_ATTRS = {
"http-cookies": "cookies",
"http-headers": "headers",
"http-query-params": "params",
"http-ssl-cert": "cert",
"http-ssl-verify": "verify",
"http-trust-env": "trust_env",
"http-timeout": "timeout",
}
_MAP_GETTERS: ClassVar[Mapping[str, Callable[["StreamlinkOptions", str], Any]]] = {
"http-proxy": _get_http_proxy,
"https-proxy": _get_http_proxy,
"http-cookies": _get_http_attr,
"http-headers": _get_http_attr,
"http-query-params": _get_http_attr,
"http-ssl-cert": _get_http_attr,
"http-ssl-verify": _get_http_attr,
"http-trust-env": _get_http_attr,
"http-timeout": _get_http_attr,
}
_MAP_SETTERS: ClassVar[Mapping[str, Callable[["StreamlinkOptions", str, Any], None]]] = {
"interface": _set_interface,
"ipv4": _set_ipv4_ipv6,
"ipv6": _set_ipv4_ipv6,
"http-proxy": _set_http_proxy,
"https-proxy": _set_http_proxy,
"http-cookies": _factory_set_http_attr_key_equals_value(";"),
"http-headers": _factory_set_http_attr_key_equals_value(";"),
"http-query-params": _factory_set_http_attr_key_equals_value("&"),
"http-disable-dh": _set_http_disable_dh,
"http-ssl-cert": _set_http_attr,
"http-ssl-verify": _set_http_attr,
"http-trust-env": _set_http_attr,
"http-timeout": _set_http_attr,
"dash-segment-attempts": _factory_set_deprecated("stream-segment-attempts", int),
"hls-segment-attempts": _factory_set_deprecated("stream-segment-attempts", int),
"dash-segment-threads": _factory_set_deprecated("stream-segment-threads", int),
"hls-segment-threads": _factory_set_deprecated("stream-segment-threads", int),
"dash-segment-timeout": _factory_set_deprecated("stream-segment-timeout", float),
"hls-segment-timeout": _factory_set_deprecated("stream-segment-timeout", float),
"dash-timeout": _factory_set_deprecated("stream-timeout", float),
"hls-timeout": _factory_set_deprecated("stream-timeout", float),
"http-stream-timeout": _factory_set_deprecated("stream-timeout", float),
}
class Streamlink:
"""
The Streamlink session is used to load and resolve plugins, and to store options used by plugins and stream implementations.
"""
http: HTTPSession
"""
An instance of Streamlink's :class:`requests.Session` subclass.
Used for any kind of HTTP request made by plugin and stream implementations.
"""
def __init__(
self,
options: Optional[Dict[str, Any]] = None,
):
"""
:param options: Custom options
"""
self.http = HTTPSession()
self.options = StreamlinkOptions(self, {
"user-input-requester": None,
"locale": None,
"interface": None,
"ipv4": False,
"ipv6": False,
"ringbuffer-size": 1024 * 1024 * 16, # 16 MB
"mux-subtitles": False,
"stream-segment-attempts": 3,
"stream-segment-threads": 1,
"stream-segment-timeout": 10.0,
"stream-timeout": 60.0,
"hls-live-edge": 3,
"hls-live-restart": False,
"hls-start-offset": 0.0,
"hls-duration": None,
"hls-playlist-reload-attempts": 3,
"hls-playlist-reload-time": "default",
"hls-segment-stream-data": False,
"hls-segment-ignore-names": [],
"hls-segment-key-uri": None,
"hls-audio-select": [],
"dash-manifest-reload-attempts": 3,
"ffmpeg-ffmpeg": None,
"ffmpeg-no-validation": False,
"ffmpeg-verbose": False,
"ffmpeg-verbose-path": None,
"ffmpeg-fout": None,
"ffmpeg-video-transcode": None,
"ffmpeg-audio-transcode": None,
"ffmpeg-copyts": False,
"ffmpeg-start-at-zero": False,
})
if options:
self.options.update(options)
self.plugins: Dict[str, Type[Plugin]] = {}
self.load_builtin_plugins()
def set_option(self, key: str, value: Any) -> None:
"""
Sets general options used by plugins and streams originating from this session object.
2013-02-25 04:21:37 +01:00
:param key: key of the option
:param value: value to set the option to
**Available options**:
.. list-table::
:header-rows: 1
:width: 100%
* - key
- type
- default
- description
* - user-input-requester
- ``UserInputRequester | None``
- ``None``
- Instance of ``UserInputRequester`` to collect input from the user at runtime
* - locale
- ``str``
- *system locale*
- Locale setting, in the RFC 1766 format,
e.g. ``en_US`` or ``es_ES``
* - interface
- ``str | None``
- ``None``
- Network interface address
* - ipv4
- ``bool``
- ``False``
- Resolve address names to IPv4 only, overrides ``ipv6``
* - ipv6
- ``bool``
- ``False``
- Resolve address names to IPv6 only, overrides ``ipv4``
* - http-proxy
- ``str | None``
- ``None``
- Proxy address for all HTTP/HTTPS requests
* - https-proxy *(deprecated)*
- ``str | None``
- ``None``
- Proxy address for all HTTP/HTTPS requests
* - http-cookies
- ``dict[str, str] | str``
- ``{}``
- A ``dict`` or a semicolon ``;`` delimited ``str`` of cookies to add to each HTTP/HTTPS request,
e.g. ``foo=bar;baz=qux``
* - http-headers
- ``dict[str, str] | str``
- ``{}``
- A ``dict`` or a semicolon ``;`` delimited ``str`` of headers to add to each HTTP/HTTPS request,
e.g. ``foo=bar;baz=qux``
* - http-query-params
- ``dict[str, str] | str``
- ``{}``
- A ``dict`` or an ampersand ``&`` delimited ``str`` of query string parameters to add to each HTTP/HTTPS request,
e.g. ``foo=bar&baz=qux``
* - http-trust-env
- ``bool``
- ``True``
- Trust HTTP settings set in the environment,
such as environment variables (``HTTP_PROXY``, etc.) and ``~/.netrc`` authentication
* - http-ssl-verify
- ``bool``
- ``True``
- Verify TLS/SSL certificates
* - http-disable-dh
- ``bool``
- ``False``
- Disable TLS/SSL Diffie-Hellman key exchange
* - http-ssl-cert
- ``str | tuple | None``
- ``None``
- TLS/SSL certificate to use, can be either a .pem file (``str``) or a .crt/.key pair (``tuple``)
* - http-timeout
- ``float``
- ``20.0``
- General timeout used by all HTTP/HTTPS requests, except the ones covered by other options
* - ringbuffer-size
- ``int``
- ``16777216`` (16 MiB)
- The size of the internal ring buffer used by most stream types
* - mux-subtitles
- ``bool``
- ``False``
- Make supported plugins mux available subtitles into the output stream
* - stream-segment-attempts
- ``int``
- ``3``
- Number of segment download attempts in segmented streams
* - stream-segment-threads
- ``int``
- ``1``
- The size of the thread pool used to download segments in parallel
* - stream-segment-timeout
- ``float``
- ``10.0``
- Segment connect and read timeout
* - stream-timeout
- ``float``
- ``60.0``
- Timeout for reading data from stream
* - hls-live-edge
- ``int``
- ``3``
- Number of segments from the live position of the HLS stream to start reading
* - hls-live-restart
- ``bool``
- ``False``
- Skip to the beginning of a live HLS stream, or as far back as possible
* - hls-start-offset
- ``float``
- ``0.0``
- Number of seconds to skip from the beginning of the HLS stream,
interpreted as a negative offset for livestreams
* - hls-duration
- ``float | None``
- ``None``
- Limit the HLS stream playback duration, rounded to the nearest HLS segment
* - hls-playlist-reload-attempts
- ``int``
- ``3``
- Max number of HLS playlist reload attempts before giving up
* - hls-playlist-reload-time
- ``str | float``
- ``"default"``
- Override the HLS playlist reload time, either in seconds (``float``) or as a ``str`` keyword:
- ``segment``: duration of the last segment
- ``live-edge``: sum of segment durations of the ``hls-live-edge`` value minus one
- ``default``: the playlist's target duration
* - hls-segment-stream-data
- ``bool``
- ``False``
- Stream data of HLS segment downloads to the output instead of waiting for the full response
* - hls-segment-ignore-names
- ``List[str]``
- ``[]``
- List of HLS segment names without file endings which should get filtered out
* - hls-segment-key-uri
- ``str | None``
- ``None``
- Override the address of the encrypted HLS stream's key,
with support for the following string template variables:
``{url}``, ``{scheme}``, ``{netloc}``, ``{path}``, ``{query}``
* - hls-audio-select
- ``List[str]``
- ``[]``
- Select a specific audio source or sources when multiple audio sources are available,
by language code or name, or ``"*"`` (asterisk)
* - dash-manifest-reload-attempts
- ``int``
- ``3``
- Max number of DASH manifest reload attempts before giving up
* - hls-segment-attempts *(deprecated)*
- ``int``
- ``3``
- See ``stream-segment-attempts``
* - hls-segment-threads *(deprecated)*
- ``int``
- ``3``
- See ``stream-segment-threads``
* - hls-segment-timeout *(deprecated)*
- ``float``
- ``10.00``
- See ``stream-segment-timeout``
* - hls-timeout *(deprecated)*
- ``float``
- ``60.00``
- See ``stream-timeout``
* - dash-segment-attempts *(deprecated)*
- ``int``
- ``3``
- See ``stream-segment-attempts``
* - dash-segment-threads *(deprecated)*
- ``int``
- ``3``
- See ``stream-segment-threads``
* - dash-segment-timeout *(deprecated)*
- ``float``
- ``10.00``
- See ``stream-segment-timeout``
* - dash-timeout *(deprecated)*
- ``float``
- ``60.00``
- See ``stream-timeout``
* - http-stream-timeout *(deprecated)*
- ``float``
- ``60.00``
- See ``stream-timeout``
* - ffmpeg-ffmpeg
- ``str | None``
- ``None``
- Override for the ``ffmpeg``/``ffmpeg.exe`` binary path,
which by default gets looked up via the ``PATH`` env var
* - ffmpeg-no-validation
- ``bool``
- ``False``
- Disable FFmpeg validation and version logging
* - ffmpeg-verbose
- ``bool``
- ``False``
- Append FFmpeg's stderr stream to the Python's stderr stream
* - ffmpeg-verbose-path
- ``str | None``
- ``None``
- Write FFmpeg's stderr stream to the filesystem at the specified path
* - ffmpeg-fout
- ``str | None``
- ``None``
- Set the output format of muxed streams, e.g. ``"matroska"``
* - ffmpeg-video-transcode
- ``str | None``
- ``None``
- The codec to use if transcoding video when muxing streams, e.g. ``"h264"``
* - ffmpeg-audio-transcode
- ``str | None``
- ``None``
- The codec to use if transcoding video when muxing streams, e.g. ``"aac"``
* - ffmpeg-copyts
- ``bool``
- ``False``
- Don't shift input stream timestamps when muxing streams
* - ffmpeg-start-at-zero
- ``bool``
- ``False``
- When ``ffmpeg-copyts`` is ``True``, shift timestamps to zero
"""
self.options.set(key, value)
def get_option(self, key: str) -> Any:
"""
Returns the current value of the specified option.
2013-02-25 04:21:37 +01:00
:param key: key of the option
"""
return self.options.get(key)
2022-05-21 00:38:18 +02:00
def set_plugin_option(self, plugin: str, key: str, value: Any) -> None:
"""
Sets plugin specific options used by plugins originating from this session object.
2013-02-25 04:21:37 +01:00
: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:
2022-05-21 00:38:18 +02:00
plugincls = self.plugins[plugin]
plugincls.set_option(key, value)
2022-05-21 00:38:18 +02:00
def get_plugin_option(self, plugin: str, key: str) -> Optional[Any]:
"""
Returns the current value of the plugin specific option.
2013-02-25 04:21:37 +01:00
:param plugin: name of the plugin
:param key: key of the option
"""
if plugin in self.plugins:
2022-05-21 00:38:18 +02:00
plugincls = self.plugins[plugin]
return plugincls.get_option(key)
2023-03-24 14:22:33 +01:00
@lru_cache(maxsize=128) # noqa: B019
def resolve_url(
self,
url: str,
plugin: remove Plugin.bind() This changes the way how the Streamlink session and other objects like the plugin cache and logger are stored on each plugin. Previously, those objects were set as class attributes on every `Plugin` class via `Plugin.bind()` when loading plugins via the session's `load_plugins()` method that gets called on initialization. This meant that whenever a new Streamlink session was initialized, references to it (including a dict of every loaded plugin) were set on each `Plugin` class as a class attribute, and Python's garbage collector could not get rid of this memory when deleting the session instance that was created last. Removing `Plugin.bind()`, passing the session via the `Plugin.__init__` constructor, and setting the cache, logger, etc. on `Plugin` instances instead (only one gets initialized by `streamlink_cli`), removes those static references that prevent the garbage collector to work. Since the plugin "module" name now doesn't get set via `Plugin.bind()` anymore, it derives its name via `self.__class__.__module__` on its own, which means a change of the return type of `Streamlink.resolve_url()` is necessary in order to pass the plugin name to `streamlink_cli`, so that it can load config files and initialize plugin arguments, etc. Breaking changes: - Remove `Plugin.bind()` - Pass the `session` instance via the Plugin constructor and set the `module`, `cache` and `logger` on the plugin instance instead. Derive `module` from the actual module name. - Change the return type of `Session.resolve_url()` and include the resolved plugin name in the returned tuple Other changes: - Remove `pluginclass.bind()` call from `Session.load_plugins()` and use the loader's module name directly on the `Session.plugins` dict - Remove initialization check from `Plugin` cookie methods - Update streamlink_cli.main module according to breaking changes - Update tests respectively - Add explicit plugin initialization test - Update tests with plugin constructors and custom plugin names - Move testplugin override module, so that it shares the same module name as the main testplugin module. Rel `Session.load_plugins()` - Refactor most session tests and replace unneeded `resolve_url()` wrappers in favor of calling `session.streams()`
2022-08-25 10:55:38 +02:00
follow_redirect: bool = True,
) -> Tuple[str, Type[Plugin], str]:
"""
Attempts to find a plugin that can use this URL.
2013-08-08 15:01:32 +02:00
The default protocol (https) will be prefixed to the URL if not specified.
2013-02-25 04:21:37 +01:00
Return values of this method are cached via :meth:`functools.lru_cache`.
2013-02-25 04:21:37 +01:00
:param url: a URL to match against loaded plugins
:param follow_redirect: follow redirects
:raises NoPluginError: on plugin resolve failure
"""
url = update_scheme("https://", url, force=False)
matcher: Matcher
plugin: remove Plugin.bind() This changes the way how the Streamlink session and other objects like the plugin cache and logger are stored on each plugin. Previously, those objects were set as class attributes on every `Plugin` class via `Plugin.bind()` when loading plugins via the session's `load_plugins()` method that gets called on initialization. This meant that whenever a new Streamlink session was initialized, references to it (including a dict of every loaded plugin) were set on each `Plugin` class as a class attribute, and Python's garbage collector could not get rid of this memory when deleting the session instance that was created last. Removing `Plugin.bind()`, passing the session via the `Plugin.__init__` constructor, and setting the cache, logger, etc. on `Plugin` instances instead (only one gets initialized by `streamlink_cli`), removes those static references that prevent the garbage collector to work. Since the plugin "module" name now doesn't get set via `Plugin.bind()` anymore, it derives its name via `self.__class__.__module__` on its own, which means a change of the return type of `Streamlink.resolve_url()` is necessary in order to pass the plugin name to `streamlink_cli`, so that it can load config files and initialize plugin arguments, etc. Breaking changes: - Remove `Plugin.bind()` - Pass the `session` instance via the Plugin constructor and set the `module`, `cache` and `logger` on the plugin instance instead. Derive `module` from the actual module name. - Change the return type of `Session.resolve_url()` and include the resolved plugin name in the returned tuple Other changes: - Remove `pluginclass.bind()` call from `Session.load_plugins()` and use the loader's module name directly on the `Session.plugins` dict - Remove initialization check from `Plugin` cookie methods - Update streamlink_cli.main module according to breaking changes - Update tests respectively - Add explicit plugin initialization test - Update tests with plugin constructors and custom plugin names - Move testplugin override module, so that it shares the same module name as the main testplugin module. Rel `Session.load_plugins()` - Refactor most session tests and replace unneeded `resolve_url()` wrappers in favor of calling `session.streams()`
2022-08-25 10:55:38 +02:00
candidate: Optional[Tuple[str, 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:
plugin: remove Plugin.bind() This changes the way how the Streamlink session and other objects like the plugin cache and logger are stored on each plugin. Previously, those objects were set as class attributes on every `Plugin` class via `Plugin.bind()` when loading plugins via the session's `load_plugins()` method that gets called on initialization. This meant that whenever a new Streamlink session was initialized, references to it (including a dict of every loaded plugin) were set on each `Plugin` class as a class attribute, and Python's garbage collector could not get rid of this memory when deleting the session instance that was created last. Removing `Plugin.bind()`, passing the session via the `Plugin.__init__` constructor, and setting the cache, logger, etc. on `Plugin` instances instead (only one gets initialized by `streamlink_cli`), removes those static references that prevent the garbage collector to work. Since the plugin "module" name now doesn't get set via `Plugin.bind()` anymore, it derives its name via `self.__class__.__module__` on its own, which means a change of the return type of `Streamlink.resolve_url()` is necessary in order to pass the plugin name to `streamlink_cli`, so that it can load config files and initialize plugin arguments, etc. Breaking changes: - Remove `Plugin.bind()` - Pass the `session` instance via the Plugin constructor and set the `module`, `cache` and `logger` on the plugin instance instead. Derive `module` from the actual module name. - Change the return type of `Session.resolve_url()` and include the resolved plugin name in the returned tuple Other changes: - Remove `pluginclass.bind()` call from `Session.load_plugins()` and use the loader's module name directly on the `Session.plugins` dict - Remove initialization check from `Plugin` cookie methods - Update streamlink_cli.main module according to breaking changes - Update tests respectively - Add explicit plugin initialization test - Update tests with plugin constructors and custom plugin names - Move testplugin override module, so that it shares the same module name as the main testplugin module. Rel `Session.load_plugins()` - Refactor most session tests and replace unneeded `resolve_url()` wrappers in favor of calling `session.streams()`
2022-08-25 10:55:38 +02:00
candidate = name, 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:
warnings.warn(
f"Resolved plugin {name} with deprecated can_handle_url API",
StreamlinkDeprecationWarning,
stacklevel=1,
)
plugin: remove Plugin.bind() This changes the way how the Streamlink session and other objects like the plugin cache and logger are stored on each plugin. Previously, those objects were set as class attributes on every `Plugin` class via `Plugin.bind()` when loading plugins via the session's `load_plugins()` method that gets called on initialization. This meant that whenever a new Streamlink session was initialized, references to it (including a dict of every loaded plugin) were set on each `Plugin` class as a class attribute, and Python's garbage collector could not get rid of this memory when deleting the session instance that was created last. Removing `Plugin.bind()`, passing the session via the `Plugin.__init__` constructor, and setting the cache, logger, etc. on `Plugin` instances instead (only one gets initialized by `streamlink_cli`), removes those static references that prevent the garbage collector to work. Since the plugin "module" name now doesn't get set via `Plugin.bind()` anymore, it derives its name via `self.__class__.__module__` on its own, which means a change of the return type of `Streamlink.resolve_url()` is necessary in order to pass the plugin name to `streamlink_cli`, so that it can load config files and initialize plugin arguments, etc. Breaking changes: - Remove `Plugin.bind()` - Pass the `session` instance via the Plugin constructor and set the `module`, `cache` and `logger` on the plugin instance instead. Derive `module` from the actual module name. - Change the return type of `Session.resolve_url()` and include the resolved plugin name in the returned tuple Other changes: - Remove `pluginclass.bind()` call from `Session.load_plugins()` and use the loader's module name directly on the `Session.plugins` dict - Remove initialization check from `Plugin` cookie methods - Update streamlink_cli.main module according to breaking changes - Update tests respectively - Add explicit plugin initialization test - Update tests with plugin constructors and custom plugin names - Move testplugin override module, so that it shares the same module name as the main testplugin module. Rel `Session.load_plugins()` - Refactor most session tests and replace unneeded `resolve_url()` wrappers in favor of calling `session.streams()`
2022-08-25 10:55:38 +02:00
candidate = name, plugin
priority = prio
if candidate:
plugin: remove Plugin.bind() This changes the way how the Streamlink session and other objects like the plugin cache and logger are stored on each plugin. Previously, those objects were set as class attributes on every `Plugin` class via `Plugin.bind()` when loading plugins via the session's `load_plugins()` method that gets called on initialization. This meant that whenever a new Streamlink session was initialized, references to it (including a dict of every loaded plugin) were set on each `Plugin` class as a class attribute, and Python's garbage collector could not get rid of this memory when deleting the session instance that was created last. Removing `Plugin.bind()`, passing the session via the `Plugin.__init__` constructor, and setting the cache, logger, etc. on `Plugin` instances instead (only one gets initialized by `streamlink_cli`), removes those static references that prevent the garbage collector to work. Since the plugin "module" name now doesn't get set via `Plugin.bind()` anymore, it derives its name via `self.__class__.__module__` on its own, which means a change of the return type of `Streamlink.resolve_url()` is necessary in order to pass the plugin name to `streamlink_cli`, so that it can load config files and initialize plugin arguments, etc. Breaking changes: - Remove `Plugin.bind()` - Pass the `session` instance via the Plugin constructor and set the `module`, `cache` and `logger` on the plugin instance instead. Derive `module` from the actual module name. - Change the return type of `Session.resolve_url()` and include the resolved plugin name in the returned tuple Other changes: - Remove `pluginclass.bind()` call from `Session.load_plugins()` and use the loader's module name directly on the `Session.plugins` dict - Remove initialization check from `Plugin` cookie methods - Update streamlink_cli.main module according to breaking changes - Update tests respectively - Add explicit plugin initialization test - Update tests with plugin constructors and custom plugin names - Move testplugin override module, so that it shares the same module name as the main testplugin module. Rel `Session.load_plugins()` - Refactor most session tests and replace unneeded `resolve_url()` wrappers in favor of calling `session.streams()`
2022-08-25 10:55:38 +02:00
return candidate[0], candidate[1], url
if follow_redirect:
# Attempt to handle a redirect URL
try:
2022-05-21 00:38:18 +02:00
res = self.http.head(url, allow_redirects=True, acceptable_status=[501]) # type: ignore[call-arg]
# 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
plugin: remove Plugin.bind() This changes the way how the Streamlink session and other objects like the plugin cache and logger are stored on each plugin. Previously, those objects were set as class attributes on every `Plugin` class via `Plugin.bind()` when loading plugins via the session's `load_plugins()` method that gets called on initialization. This meant that whenever a new Streamlink session was initialized, references to it (including a dict of every loaded plugin) were set on each `Plugin` class as a class attribute, and Python's garbage collector could not get rid of this memory when deleting the session instance that was created last. Removing `Plugin.bind()`, passing the session via the `Plugin.__init__` constructor, and setting the cache, logger, etc. on `Plugin` instances instead (only one gets initialized by `streamlink_cli`), removes those static references that prevent the garbage collector to work. Since the plugin "module" name now doesn't get set via `Plugin.bind()` anymore, it derives its name via `self.__class__.__module__` on its own, which means a change of the return type of `Streamlink.resolve_url()` is necessary in order to pass the plugin name to `streamlink_cli`, so that it can load config files and initialize plugin arguments, etc. Breaking changes: - Remove `Plugin.bind()` - Pass the `session` instance via the Plugin constructor and set the `module`, `cache` and `logger` on the plugin instance instead. Derive `module` from the actual module name. - Change the return type of `Session.resolve_url()` and include the resolved plugin name in the returned tuple Other changes: - Remove `pluginclass.bind()` call from `Session.load_plugins()` and use the loader's module name directly on the `Session.plugins` dict - Remove initialization check from `Plugin` cookie methods - Update streamlink_cli.main module according to breaking changes - Update tests respectively - Add explicit plugin initialization test - Update tests with plugin constructors and custom plugin names - Move testplugin override module, so that it shares the same module name as the main testplugin module. Rel `Session.load_plugins()` - Refactor most session tests and replace unneeded `resolve_url()` wrappers in favor of calling `session.streams()`
2022-08-25 10:55:38 +02:00
def resolve_url_no_redirect(self, url: str) -> Tuple[str, 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.
:param url: a URL to match against loaded plugins
:raises NoPluginError: on plugin resolve failure
"""
return self.resolve_url(url, follow_redirect=False)
def streams(self, url: str, **params):
"""
Attempts to find a plugin and extracts streams from the *url* if a plugin was found.
2014-06-06 18:14:46 +02:00
:param url: a URL to match against loaded plugins
:param params: Additional keyword arguments passed to :meth:`streamlink.plugin.Plugin.streams`
:raises NoPluginError: on plugin resolve failure
:return: A :class:`dict` of stream names and :class:`streamlink.stream.Stream` instances
2014-06-06 18:14:46 +02:00
"""
plugin: remove Plugin.bind() This changes the way how the Streamlink session and other objects like the plugin cache and logger are stored on each plugin. Previously, those objects were set as class attributes on every `Plugin` class via `Plugin.bind()` when loading plugins via the session's `load_plugins()` method that gets called on initialization. This meant that whenever a new Streamlink session was initialized, references to it (including a dict of every loaded plugin) were set on each `Plugin` class as a class attribute, and Python's garbage collector could not get rid of this memory when deleting the session instance that was created last. Removing `Plugin.bind()`, passing the session via the `Plugin.__init__` constructor, and setting the cache, logger, etc. on `Plugin` instances instead (only one gets initialized by `streamlink_cli`), removes those static references that prevent the garbage collector to work. Since the plugin "module" name now doesn't get set via `Plugin.bind()` anymore, it derives its name via `self.__class__.__module__` on its own, which means a change of the return type of `Streamlink.resolve_url()` is necessary in order to pass the plugin name to `streamlink_cli`, so that it can load config files and initialize plugin arguments, etc. Breaking changes: - Remove `Plugin.bind()` - Pass the `session` instance via the Plugin constructor and set the `module`, `cache` and `logger` on the plugin instance instead. Derive `module` from the actual module name. - Change the return type of `Session.resolve_url()` and include the resolved plugin name in the returned tuple Other changes: - Remove `pluginclass.bind()` call from `Session.load_plugins()` and use the loader's module name directly on the `Session.plugins` dict - Remove initialization check from `Plugin` cookie methods - Update streamlink_cli.main module according to breaking changes - Update tests respectively - Add explicit plugin initialization test - Update tests with plugin constructors and custom plugin names - Move testplugin override module, so that it shares the same module name as the main testplugin module. Rel `Session.load_plugins()` - Refactor most session tests and replace unneeded `resolve_url()` wrappers in favor of calling `session.streams()`
2022-08-25 10:55:38 +02:00
pluginname, pluginclass, resolved_url = self.resolve_url(url)
plugin = pluginclass(self, resolved_url)
2014-06-06 18:14:46 +02:00
return plugin.streams(**params)
def get_plugins(self):
2013-02-25 04:21:37 +01:00
"""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.
2013-02-25 04:21:37 +01:00
:param path: full path to a directory where to look for plugins
:return: success
"""
success = False
2023-03-24 14:22:33 +01:00
for _loader, name, _ispkg in pkgutil.iter_modules([path]):
# set the full plugin module name
plugin: remove Plugin.bind() This changes the way how the Streamlink session and other objects like the plugin cache and logger are stored on each plugin. Previously, those objects were set as class attributes on every `Plugin` class via `Plugin.bind()` when loading plugins via the session's `load_plugins()` method that gets called on initialization. This meant that whenever a new Streamlink session was initialized, references to it (including a dict of every loaded plugin) were set on each `Plugin` class as a class attribute, and Python's garbage collector could not get rid of this memory when deleting the session instance that was created last. Removing `Plugin.bind()`, passing the session via the `Plugin.__init__` constructor, and setting the cache, logger, etc. on `Plugin` instances instead (only one gets initialized by `streamlink_cli`), removes those static references that prevent the garbage collector to work. Since the plugin "module" name now doesn't get set via `Plugin.bind()` anymore, it derives its name via `self.__class__.__module__` on its own, which means a change of the return type of `Streamlink.resolve_url()` is necessary in order to pass the plugin name to `streamlink_cli`, so that it can load config files and initialize plugin arguments, etc. Breaking changes: - Remove `Plugin.bind()` - Pass the `session` instance via the Plugin constructor and set the `module`, `cache` and `logger` on the plugin instance instead. Derive `module` from the actual module name. - Change the return type of `Session.resolve_url()` and include the resolved plugin name in the returned tuple Other changes: - Remove `pluginclass.bind()` call from `Session.load_plugins()` and use the loader's module name directly on the `Session.plugins` dict - Remove initialization check from `Plugin` cookie methods - Update streamlink_cli.main module according to breaking changes - Update tests respectively - Add explicit plugin initialization test - Update tests with plugin constructors and custom plugin names - Move testplugin override module, so that it shares the same module name as the main testplugin module. Rel `Session.load_plugins()` - Refactor most session tests and replace unneeded `resolve_url()` wrappers in favor of calling `session.streams()`
2022-08-25 10:55:38 +02:00
# use the "streamlink.plugins." prefix even for sideloaded plugins
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: remove Plugin.bind() This changes the way how the Streamlink session and other objects like the plugin cache and logger are stored on each plugin. Previously, those objects were set as class attributes on every `Plugin` class via `Plugin.bind()` when loading plugins via the session's `load_plugins()` method that gets called on initialization. This meant that whenever a new Streamlink session was initialized, references to it (including a dict of every loaded plugin) were set on each `Plugin` class as a class attribute, and Python's garbage collector could not get rid of this memory when deleting the session instance that was created last. Removing `Plugin.bind()`, passing the session via the `Plugin.__init__` constructor, and setting the cache, logger, etc. on `Plugin` instances instead (only one gets initialized by `streamlink_cli`), removes those static references that prevent the garbage collector to work. Since the plugin "module" name now doesn't get set via `Plugin.bind()` anymore, it derives its name via `self.__class__.__module__` on its own, which means a change of the return type of `Streamlink.resolve_url()` is necessary in order to pass the plugin name to `streamlink_cli`, so that it can load config files and initialize plugin arguments, etc. Breaking changes: - Remove `Plugin.bind()` - Pass the `session` instance via the Plugin constructor and set the `module`, `cache` and `logger` on the plugin instance instead. Derive `module` from the actual module name. - Change the return type of `Session.resolve_url()` and include the resolved plugin name in the returned tuple Other changes: - Remove `pluginclass.bind()` call from `Session.load_plugins()` and use the loader's module name directly on the `Session.plugins` dict - Remove initialization check from `Plugin` cookie methods - Update streamlink_cli.main module according to breaking changes - Update tests respectively - Add explicit plugin initialization test - Update tests with plugin constructors and custom plugin names - Move testplugin override module, so that it shares the same module name as the main testplugin module. Rel `Session.load_plugins()` - Refactor most session tests and replace unneeded `resolve_url()` wrappers in favor of calling `session.streams()`
2022-08-25 10:55:38 +02:00
if name in self.plugins:
log.debug(f"Plugin {name} is being overridden by {mod.__file__}")
self.plugins[name] = plugin
return success
@property
def version(self):
return __version__
@property
def localization(self):
return Localization(self.get_option("locale"))
__all__ = ["Streamlink"]