2018-05-30 03:15:11 +02:00
|
|
|
import logging
|
2013-08-08 15:01:32 +02:00
|
|
|
import pkgutil
|
2023-01-06 22:54:16 +01:00
|
|
|
import warnings
|
2021-01-12 21:38:45 +01:00
|
|
|
from functools import lru_cache
|
2021-01-09 18:45:04 +01:00
|
|
|
from socket import AF_INET, AF_INET6
|
2023-01-07 20:50:16 +01:00
|
|
|
from typing import Any, Callable, ClassVar, Dict, Iterator, Mapping, Optional, Tuple, Type
|
2018-05-30 03:15:11 +02:00
|
|
|
|
2022-05-11 14:37:09 +02:00
|
|
|
import urllib3.util.connection as urllib3_util_connection
|
|
|
|
import urllib3.util.ssl_ as urllib3_util_ssl
|
2017-08-03 16:46:46 +02:00
|
|
|
|
2020-10-26 14:46:17 +01:00
|
|
|
from streamlink import __version__, plugins
|
2023-01-12 18:58:54 +01:00
|
|
|
from streamlink.exceptions import NoPluginError, PluginError, StreamlinkDeprecationWarning
|
2020-10-13 14:02:59 +02:00
|
|
|
from streamlink.logger import StreamlinkLogger
|
2020-10-26 14:46:17 +01:00
|
|
|
from streamlink.options import Options
|
2021-11-06 12:33:53 +01:00
|
|
|
from streamlink.plugin.api.http_session import HTTPSession
|
2023-02-09 15:44:12 +01:00
|
|
|
from streamlink.plugin.plugin import NO_PRIORITY, NORMAL_PRIORITY, Matcher, Plugin
|
2017-01-20 14:02:14 +01:00
|
|
|
from streamlink.utils.l10n import Localization
|
2021-09-17 16:19:46 +02:00
|
|
|
from streamlink.utils.module import load_module
|
2021-09-14 12:01:25 +02:00
|
|
|
from streamlink.utils.url import update_scheme
|
2013-02-08 02:00:44 +01:00
|
|
|
|
2023-02-09 15:44:12 +01:00
|
|
|
|
2018-05-30 03:15:11 +02:00
|
|
|
# Ensure that the Logger class returned is Streamslink's for using the API (for backwards compatibility)
|
|
|
|
logging.setLoggerClass(StreamlinkLogger)
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2013-05-10 00:57:43 +02:00
|
|
|
|
2022-05-21 00:38:18 +02:00
|
|
|
_original_allowed_gai_family = urllib3_util_connection.allowed_gai_family # type: ignore[attr-defined]
|
2022-05-11 14:37:09 +02:00
|
|
|
|
|
|
|
|
2023-03-24 15:33:48 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2020-04-19 21:02:43 +02:00
|
|
|
class PythonDeprecatedWarning(UserWarning):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2023-01-07 20:50:16 +01:00
|
|
|
class StreamlinkOptions(Options):
|
|
|
|
def __init__(self, session: "Streamlink", *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.session = session
|
|
|
|
|
|
|
|
# ---- utils
|
|
|
|
|
|
|
|
@staticmethod
|
2023-02-17 17:44:51 +01:00
|
|
|
def _parse_key_equals_value_string(delimiter: str, value: str) -> Iterator[Tuple[str, str]]:
|
|
|
|
for keyval in value.split(delimiter):
|
2023-01-07 20:50:16 +01:00
|
|
|
try:
|
|
|
|
key, val = keyval.split("=", 1)
|
|
|
|
yield key.strip(), val.strip()
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
|
2023-03-24 15:33:48 +01:00
|
|
|
@staticmethod
|
|
|
|
def _deprecate_https_proxy(key: str) -> None:
|
2023-01-07 20:50:16 +01:00
|
|
|
if key == "https-proxy":
|
2023-01-06 22:54:16 +01:00
|
|
|
warnings.warn(
|
|
|
|
"The `https-proxy` option has been deprecated in favor of a single `http-proxy` option",
|
2023-01-12 18:58:54 +01:00
|
|
|
StreamlinkDeprecationWarning,
|
2023-03-24 15:33:48 +01:00
|
|
|
stacklevel=4 + _get_deprecation_stacklevel_offset(),
|
2023-01-06 22:54:16 +01:00
|
|
|
)
|
2023-03-24 15:33:48 +01:00
|
|
|
|
|
|
|
# ---- getters
|
|
|
|
|
|
|
|
def _get_http_proxy(self, key):
|
|
|
|
self._deprecate_https_proxy(key)
|
2023-01-07 20:50:16 +01:00
|
|
|
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)
|
2023-03-24 15:33:48 +01:00
|
|
|
self._deprecate_https_proxy(key)
|
2023-01-07 20:50:16 +01:00
|
|
|
|
|
|
|
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]
|
|
|
|
|
2023-02-17 17:44:51 +01:00
|
|
|
@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
|
|
|
|
|
2023-01-07 20:50:16 +01:00
|
|
|
@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))
|
2023-01-06 22:54:16 +01:00
|
|
|
warnings.warn(
|
|
|
|
f"`{key}` has been deprecated in favor of the `{name}` option",
|
2023-01-12 18:58:54 +01:00
|
|
|
StreamlinkDeprecationWarning,
|
2023-03-24 15:33:48 +01:00
|
|
|
stacklevel=3 + _get_deprecation_stacklevel_offset(),
|
2023-01-06 22:54:16 +01:00
|
|
|
)
|
2023-01-07 20:50:16 +01:00
|
|
|
|
|
|
|
return inner
|
|
|
|
|
|
|
|
# bind explicitly with dummy context, to prevent `TypeError: 'staticmethod' object is not callable` on py<310
|
2023-02-17 17:44:51 +01:00
|
|
|
_factory_set_http_attr_key_equals_value = _factory_set_http_attr_key_equals_value.__get__(object)
|
2023-01-07 20:50:16 +01:00
|
|
|
_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",
|
|
|
|
}
|
|
|
|
|
2023-01-07 20:50:16 +01:00
|
|
|
_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,
|
2023-02-17 17:44:51 +01:00
|
|
|
"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("&"),
|
2023-01-07 20:50:16 +01:00
|
|
|
"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),
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-09-19 21:46:06 +02:00
|
|
|
class Streamlink:
|
2022-04-03 20:58:13 +02:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
"""
|
2013-02-08 02:00:44 +01:00
|
|
|
|
2021-11-06 12:33:53 +01:00
|
|
|
self.http = HTTPSession()
|
2023-01-07 20:50:16 +01:00
|
|
|
self.options = StreamlinkOptions(self, {
|
2023-02-17 17:21:37 +01:00
|
|
|
"user-input-requester": None,
|
|
|
|
"locale": None,
|
2021-01-09 18:45:04 +01:00
|
|
|
"interface": None,
|
|
|
|
"ipv4": False,
|
|
|
|
"ipv6": False,
|
2017-01-16 18:57:12 +01:00
|
|
|
"ringbuffer-size": 1024 * 1024 * 16, # 16 MB
|
2023-02-17 17:21:37 +01:00
|
|
|
"mux-subtitles": False,
|
2014-07-27 15:55:10 +02:00
|
|
|
"stream-segment-attempts": 3,
|
|
|
|
"stream-segment-threads": 1,
|
|
|
|
"stream-segment-timeout": 10.0,
|
|
|
|
"stream-timeout": 60.0,
|
2023-02-17 17:21:37 +01:00
|
|
|
"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": [],
|
2023-02-28 19:14:06 +01:00
|
|
|
"dash-manifest-reload-attempts": 3,
|
2017-01-12 13:36:30 +01:00
|
|
|
"ffmpeg-ffmpeg": None,
|
2022-10-02 14:43:26 +02:00
|
|
|
"ffmpeg-no-validation": False,
|
2023-02-17 17:21:37 +01:00
|
|
|
"ffmpeg-verbose": False,
|
|
|
|
"ffmpeg-verbose-path": None,
|
2020-11-23 18:29:26 +01:00
|
|
|
"ffmpeg-fout": None,
|
|
|
|
"ffmpeg-video-transcode": None,
|
|
|
|
"ffmpeg-audio-transcode": None,
|
2020-12-16 17:52:10 +01:00
|
|
|
"ffmpeg-copyts": False,
|
2020-12-15 16:57:50 +01:00
|
|
|
"ffmpeg-start-at-zero": False,
|
2013-02-08 02:00:44 +01:00
|
|
|
})
|
2018-06-22 01:15:21 +02:00
|
|
|
if options:
|
|
|
|
self.options.update(options)
|
2022-04-17 14:07:46 +02:00
|
|
|
self.plugins: Dict[str, Type[Plugin]] = {}
|
2013-02-08 02:00:44 +01:00
|
|
|
self.load_builtin_plugins()
|
|
|
|
|
2023-01-07 20:50:16 +01:00
|
|
|
def set_option(self, key: str, value: Any) -> None:
|
2022-04-03 20:58:13 +02:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2014-04-17 00:52:49 +02:00
|
|
|
**Available options**:
|
|
|
|
|
2023-02-17 17:21:37 +01:00
|
|
|
.. 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``
|
|
|
|
- ``{}``
|
2023-02-17 17:44:51 +01:00
|
|
|
- A ``dict`` or an ampersand ``&`` delimited ``str`` of query string parameters to add to each HTTP/HTTPS request,
|
2023-02-17 19:12:27 +01:00
|
|
|
e.g. ``foo=bar&baz=qux``
|
2023-02-17 17:21:37 +01:00
|
|
|
* - 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``
|
2023-02-17 19:12:27 +01:00
|
|
|
- Skip to the beginning of a live HLS stream, or as far back as possible
|
2023-02-17 17:21:37 +01:00
|
|
|
* - hls-start-offset
|
|
|
|
- ``float``
|
|
|
|
- ``0.0``
|
|
|
|
- Number of seconds to skip from the beginning of the HLS stream,
|
2023-02-17 19:12:27 +01:00
|
|
|
interpreted as a negative offset for livestreams
|
2023-02-17 17:21:37 +01:00
|
|
|
* - hls-duration
|
|
|
|
- ``float | None``
|
|
|
|
- ``None``
|
|
|
|
- Limit the HLS stream playback duration, rounded to the nearest HLS segment
|
|
|
|
* - hls-playlist-reload-attempts
|
|
|
|
- ``int``
|
|
|
|
- ``3``
|
2023-02-28 19:14:06 +01:00
|
|
|
- Max number of HLS playlist reload attempts before giving up
|
2023-02-17 17:21:37 +01:00
|
|
|
* - 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
|
2023-02-17 19:12:27 +01:00
|
|
|
- ``live-edge``: sum of segment durations of the ``hls-live-edge`` value minus one
|
2023-02-17 17:21:37 +01:00
|
|
|
- ``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)
|
2023-02-28 19:14:06 +01:00
|
|
|
* - dash-manifest-reload-attempts
|
|
|
|
- ``int``
|
|
|
|
- ``3``
|
|
|
|
- Max number of DASH manifest reload attempts before giving up
|
2023-02-17 17:21:37 +01:00
|
|
|
* - 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
|
2023-02-17 19:12:27 +01:00
|
|
|
- ``str | None``
|
2023-02-17 17:21:37 +01:00
|
|
|
- ``None``
|
|
|
|
- Write FFmpeg's stderr stream to the filesystem at the specified path
|
|
|
|
* - ffmpeg-fout
|
2023-02-17 19:12:27 +01:00
|
|
|
- ``str | None``
|
2023-02-17 17:21:37 +01:00
|
|
|
- ``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
|
2014-04-17 00:52:49 +02:00
|
|
|
"""
|
2014-04-18 18:21:43 +02:00
|
|
|
|
2023-01-07 20:50:16 +01:00
|
|
|
self.options.set(key, value)
|
session: deprecate options for spec. stream types
Having multiple session options and CLI arguments for different stream
types which are already covered by generic options/arguments is not only
redundant, but also confusing.
A distinction between different stream types does only make sense when
multiple different stream types are available and the user needs to
explicitly set different values for each of them, but since it's not
always clear which stream type is returned when a stream is selected
via the "best" stream name synonym for example, this makes it even more
confusing. And it's redundant as well, since only one stream can be
selected anyway.
- deprecate `{dash,hds,hls}-segment-attempts`
in favor of `stream-segment-attempts`
- deprecate `{dash,hds,hls}-segment-threads`
in favor of `stream-segment-threads`
- deprecate `{dash,hds,hls}-segment-timeout`
in favor of `stream-segment-timeout`
- deprecate `{dash,hds,hls,rtmp,http-stream}-timeout`
in favor of `stream-timeout` (dash/http-stream were never used)
- fix `HTTPStream` and use `stream-timeout` instead of `http-timeout`
- suppress deprecated CLI arguments
- update help texts of generic CLI arguments
- fix docs and add entry to deprecations page
2021-07-30 22:34:22 +02:00
|
|
|
|
2023-01-07 20:50:16 +01:00
|
|
|
def get_option(self, key: str) -> Any:
|
2022-04-03 20:58:13 +02:00
|
|
|
"""
|
|
|
|
Returns the current value of the specified option.
|
2013-02-25 04:21:37 +01:00
|
|
|
|
|
|
|
:param key: key of the option
|
|
|
|
"""
|
|
|
|
|
2023-01-07 20:50:16 +01:00
|
|
|
return self.options.get(key)
|
2013-02-08 02:00:44 +01:00
|
|
|
|
2022-05-21 00:38:18 +02:00
|
|
|
def set_plugin_option(self, plugin: str, key: str, value: Any) -> None:
|
2022-04-03 20:58:13 +02:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
|
2013-02-08 02:00:44 +01:00
|
|
|
if plugin in self.plugins:
|
2022-05-21 00:38:18 +02:00
|
|
|
plugincls = self.plugins[plugin]
|
|
|
|
plugincls.set_option(key, value)
|
2013-02-08 02:00:44 +01:00
|
|
|
|
2022-05-21 00:38:18 +02:00
|
|
|
def get_plugin_option(self, plugin: str, key: str) -> Optional[Any]:
|
2022-04-03 20:58:13 +02:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
|
2013-02-08 02:00:44 +01:00
|
|
|
if plugin in self.plugins:
|
2022-05-21 00:38:18 +02:00
|
|
|
plugincls = self.plugins[plugin]
|
|
|
|
return plugincls.get_option(key)
|
2013-02-08 02:00:44 +01:00
|
|
|
|
2023-03-24 14:22:33 +01:00
|
|
|
@lru_cache(maxsize=128) # noqa: B019
|
2022-04-03 20:58:13 +02:00
|
|
|
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]:
|
2022-04-03 20:58:13 +02:00
|
|
|
"""
|
|
|
|
Attempts to find a plugin that can use this URL.
|
2013-08-08 15:01:32 +02:00
|
|
|
|
2022-04-03 20:58:13 +02:00
|
|
|
The default protocol (https) will be prefixed to the URL if not specified.
|
2013-02-25 04:21:37 +01:00
|
|
|
|
2022-04-03 20:58:13 +02: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
|
2017-08-03 14:45:00 +02:00
|
|
|
:param follow_redirect: follow redirects
|
2022-04-03 20:58:13 +02:00
|
|
|
:raises NoPluginError: on plugin resolve failure
|
2013-02-08 02:00:44 +01:00
|
|
|
"""
|
2022-04-03 20:58:13 +02:00
|
|
|
|
2021-10-03 14:35:46 +02:00
|
|
|
url = update_scheme("https://", url, force=False)
|
2013-02-08 02:00:44 +01:00
|
|
|
|
2021-06-22 22:25:35 +02:00
|
|
|
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
|
2021-06-22 22:25:35 +02:00
|
|
|
priority = NO_PRIORITY
|
2013-02-08 02:00:44 +01:00
|
|
|
for name, plugin in self.plugins.items():
|
2021-06-22 22:25:35 +02:00
|
|
|
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
|
2021-06-22 22:25:35 +02:00
|
|
|
priority = matcher.priority
|
2021-06-23 09:17:07 +02:00
|
|
|
# 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:
|
2023-01-06 22:54:16 +01:00
|
|
|
warnings.warn(
|
|
|
|
f"Resolved plugin {name} with deprecated can_handle_url API",
|
2023-01-12 18:58:54 +01:00
|
|
|
StreamlinkDeprecationWarning,
|
2023-03-24 15:33:48 +01:00
|
|
|
stacklevel=1,
|
2023-01-06 22:54:16 +01: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
|
|
|
candidate = name, plugin
|
2021-06-23 09:17:07 +02:00
|
|
|
priority = prio
|
2021-06-22 22:25:35 +02:00
|
|
|
|
|
|
|
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
|
2015-03-24 21:53:42 +01:00
|
|
|
|
2017-08-03 14:45:00 +02:00
|
|
|
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]
|
2015-03-24 21:53:42 +01:00
|
|
|
|
2017-08-03 14:45:00 +02:00
|
|
|
# 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
|
2014-03-15 21:40:18 +01:00
|
|
|
|
2013-02-08 02:00:44 +01:00
|
|
|
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]:
|
2022-04-03 20:58:13 +02:00
|
|
|
"""
|
|
|
|
Attempts to find a plugin that can use this URL.
|
2017-01-10 20:16:05 +01:00
|
|
|
|
2022-04-03 20:58:13 +02:00
|
|
|
The default protocol (https) will be prefixed to the URL if not specified.
|
2017-01-10 20:16:05 +01:00
|
|
|
|
|
|
|
:param url: a URL to match against loaded plugins
|
2022-04-03 20:58:13 +02:00
|
|
|
:raises NoPluginError: on plugin resolve failure
|
2017-01-10 20:16:05 +01:00
|
|
|
"""
|
2022-04-03 20:58:13 +02:00
|
|
|
|
2017-08-03 14:45:00 +02:00
|
|
|
return self.resolve_url(url, follow_redirect=False)
|
2017-01-10 20:16:05 +01:00
|
|
|
|
2021-11-10 10:52:12 +01:00
|
|
|
def streams(self, url: str, **params):
|
2022-04-03 20:58:13 +02:00
|
|
|
"""
|
|
|
|
Attempts to find a plugin and extracts streams from the *url* if a plugin was found.
|
2014-06-06 18:14:46 +02:00
|
|
|
|
2022-04-03 20:58:13 +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)
|
2021-11-10 10:52:12 +01:00
|
|
|
|
2014-06-06 18:14:46 +02:00
|
|
|
return plugin.streams(**params)
|
|
|
|
|
2013-02-08 02:00:44 +01:00
|
|
|
def get_plugins(self):
|
2013-02-25 04:21:37 +01:00
|
|
|
"""Returns the loaded plugins for the session."""
|
|
|
|
|
2013-02-08 02:00:44 +01:00
|
|
|
return self.plugins
|
|
|
|
|
|
|
|
def load_builtin_plugins(self):
|
|
|
|
self.load_plugins(plugins.__path__[0])
|
|
|
|
|
2021-06-05 22:17:42 +02:00
|
|
|
def load_plugins(self, path: str) -> bool:
|
2022-04-03 20:58:13 +02:00
|
|
|
"""
|
|
|
|
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
|
2021-06-05 22:17:42 +02:00
|
|
|
:return: success
|
2013-02-08 02:00:44 +01:00
|
|
|
"""
|
2022-04-03 20:58:13 +02:00
|
|
|
|
2021-06-05 22:17:42 +02:00
|
|
|
success = False
|
2023-03-24 14:22:33 +01:00
|
|
|
for _loader, name, _ispkg in pkgutil.iter_modules([path]):
|
2018-06-01 00:16:52 +02:00
|
|
|
# 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
|
2020-11-26 01:14:39 +01:00
|
|
|
module_name = f"streamlink.plugins.{name}"
|
2013-05-10 00:57:43 +02:00
|
|
|
try:
|
2020-11-26 01:14:39 +01:00
|
|
|
mod = load_module(module_name, path)
|
|
|
|
except ImportError:
|
|
|
|
log.exception(f"Failed to load plugin {name} from {path}\n")
|
2013-05-10 00:57:43 +02:00
|
|
|
continue
|
2013-02-08 02:00:44 +01:00
|
|
|
|
2020-11-26 01:14:39 +01:00
|
|
|
if not hasattr(mod, "__plugin__") or not issubclass(mod.__plugin__, Plugin):
|
|
|
|
continue
|
2021-06-05 22:17:42 +02:00
|
|
|
success = True
|
2020-11-26 01:14:39 +01:00
|
|
|
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
|
2013-02-08 02:00:44 +01:00
|
|
|
|
2021-06-05 22:17:42 +02:00
|
|
|
return success
|
|
|
|
|
2013-02-08 02:00:44 +01:00
|
|
|
@property
|
|
|
|
def version(self):
|
|
|
|
return __version__
|
|
|
|
|
2017-01-20 14:02:14 +01:00
|
|
|
@property
|
|
|
|
def localization(self):
|
|
|
|
return Localization(self.get_option("locale"))
|
|
|
|
|
2017-01-12 15:54:55 +01:00
|
|
|
|
2016-09-19 21:46:06 +02:00
|
|
|
__all__ = ["Streamlink"]
|