Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package streamlink for openSUSE:Factory checked in at 2026-01-22 15:13:52 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/streamlink (Old) and /work/SRC/openSUSE:Factory/.streamlink.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "streamlink" Thu Jan 22 15:13:52 2026 rev:29 rq:1328446 version:8.1.2 Changes: -------- --- /work/SRC/openSUSE:Factory/streamlink/streamlink.changes 2025-12-18 18:32:29.580236866 +0100 +++ /work/SRC/openSUSE:Factory/.streamlink.new.1928/streamlink.changes 2026-01-22 15:15:47.886282625 +0100 @@ -1,0 +2,17 @@ +Tue Jan 20 23:19:02 UTC 2026 - Richard Rahl <[email protected]> + +- Update to version 8.1.2: + * Fixed: warnings when parsing HLS playlists with private-use language + subtags + * youtube: fixed live streams +- Update to version 8.1.1: + * Fixed: --stream-segmented-queue-deadline not being applied correctly to the + Streamlink session options + * Changed: --hls-segment-ignore-names to not hardcode .ts HLS segment file + name extensions + * dailymotion: fixed 403 HLS playlist responses + * pluto: fixed url matchers and ad detection + * soop: fixed CDN mapping for ld_cdn based regions + * Build: removed unneeded wheel dependency + +------------------------------------------------------------------- Old: ---- streamlink-8.1.0.tar.gz streamlink-8.1.0.tar.gz.asc New: ---- streamlink-8.1.2.tar.gz streamlink-8.1.2.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ streamlink.spec ++++++ --- /var/tmp/diff_new_pack.VI98JY/_old 2026-01-22 15:15:48.714317062 +0100 +++ /var/tmp/diff_new_pack.VI98JY/_new 2026-01-22 15:15:48.714317062 +0100 @@ -1,7 +1,7 @@ # # spec file for package streamlink # -# Copyright (c) 2025 SUSE LLC and contributors +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -24,7 +24,7 @@ %endif %{?sle15_python_module_pythons}%{!?sle15_python_module_pythons:%define pythons python3} Name: streamlink%{psuffix} -Version: 8.1.0 +Version: 8.1.2 Release: 0 Summary: Program to pipe streams from services into a video player License: Apache-2.0 AND BSD-2-Clause @@ -49,7 +49,6 @@ BuildRequires: %{python_module urllib3 >= 2.0.0} BuildRequires: %{python_module versioningit >= 2.0.0} BuildRequires: %{python_module websocket-client >= 1.2.1} -BuildRequires: %{python_module wheel} BuildRequires: fdupes %if "%{flavor}" == "test" ++++++ streamlink-8.1.0.tar.gz -> streamlink-8.1.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/CHANGELOG.md new/streamlink-8.1.2/CHANGELOG.md --- old/streamlink-8.1.0/CHANGELOG.md 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/CHANGELOG.md 2026-01-18 13:20:38.000000000 +0100 @@ -1,5 +1,27 @@ # Changelog +## streamlink 8.1.2 (2026-01-18) + +- Fixed: warnings when parsing HLS playlists with private-use language subtags ([#6780](https://github.com/streamlink/streamlink/pull/6780)) +- Updated plugins: + - youtube: fixed live streams ([#6777](https://github.com/streamlink/streamlink/pull/6777)) + +[Full changelog](https://github.com/streamlink/streamlink/compare/8.1.1...8.1.2) + + +## streamlink 8.1.1 (2026-01-17) + +- Fixed: `--stream-segmented-queue-deadline` not being applied correctly to the Streamlink session options ([#6758](https://github.com/streamlink/streamlink/pull/6758)) +- Changed: `--hls-segment-ignore-names` to not hardcode `.ts` HLS segment file name extensions ([#6747](https://github.com/streamlink/streamlink/pull/6747)) +- Updated plugins: + - dailymotion: fixed 403 HLS playlist responses ([#6773](https://github.com/streamlink/streamlink/pull/6773)) + - pluto: fixed url matchers and ad detection ([#6767](https://github.com/streamlink/streamlink/pull/6767)) + - soop: fixed CDN mapping for `ld_cdn` based regions ([#6749](https://github.com/streamlink/streamlink/pull/6749)) +- Build: removed unneeded `wheel` dependency from `build-system.requires` and the `build` dependency group ([#6754](https://github.com/streamlink/streamlink/pull/6754)) + +[Full changelog](https://github.com/streamlink/streamlink/compare/8.1.0...8.1.1) + + ## streamlink 8.1.0 (2025-12-14) - Deprecated: `--hls-segment-queue-threshold` in favor of `--stream-segmented-queue-deadline` ([#6734](https://github.com/streamlink/streamlink/pull/6734)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/LICENSE new/streamlink-8.1.2/LICENSE --- old/streamlink-8.1.0/LICENSE 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/LICENSE 2026-01-18 13:20:38.000000000 +0100 @@ -1,5 +1,5 @@ Copyright (c) 2011-2016, Christopher Rosell -Copyright (c) 2016-2025, Streamlink Team +Copyright (c) 2016-2026, Streamlink Team All rights reserved. Redistribution and use in source and binary forms, with or without diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/PKG-INFO new/streamlink-8.1.2/PKG-INFO --- old/streamlink-8.1.0/PKG-INFO 2025-12-14 19:07:27.963754200 +0100 +++ new/streamlink-8.1.2/PKG-INFO 2026-01-18 13:21:06.754069000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: streamlink -Version: 8.1.0 +Version: 8.1.2 Summary: Streamlink is a command-line utility that extracts streams from various services and pipes them into a video player of choice. Author: Streamlink Author-email: [email protected] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/docs/_build/man/streamlink.1 new/streamlink-8.1.2/docs/_build/man/streamlink.1 --- old/streamlink-8.1.0/docs/_build/man/streamlink.1 2025-12-14 19:07:23.000000000 +0100 +++ new/streamlink-8.1.2/docs/_build/man/streamlink.1 2026-01-18 13:21:01.000000000 +0100 @@ -27,7 +27,7 @@ .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "STREAMLINK" "1" "Dec 14, 2025" "8.1.0" "Streamlink" +.TH "STREAMLINK" "1" "Jan 18, 2026" "8.1.2" "Streamlink" .SH NAME streamlink \- extracts streams from various services and pipes them into a video player of choice .SH SYNOPSIS @@ -1529,6 +1529,6 @@ .SH AUTHOR Streamlink Contributors .SH COPYRIGHT -2025, Streamlink +2026, Streamlink .\" Generated by docutils manpage writer. . diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/docs/changelog.md new/streamlink-8.1.2/docs/changelog.md --- old/streamlink-8.1.0/docs/changelog.md 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/docs/changelog.md 2026-01-18 13:20:38.000000000 +0100 @@ -1,5 +1,27 @@ # Changelog +## streamlink 8.1.2 (2026-01-18) + +- Fixed: warnings when parsing HLS playlists with private-use language subtags ([#6780](https://github.com/streamlink/streamlink/pull/6780)) +- Updated plugins: + - youtube: fixed live streams ([#6777](https://github.com/streamlink/streamlink/pull/6777)) + +[Full changelog](https://github.com/streamlink/streamlink/compare/8.1.1...8.1.2) + + +## streamlink 8.1.1 (2026-01-17) + +- Fixed: `--stream-segmented-queue-deadline` not being applied correctly to the Streamlink session options ([#6758](https://github.com/streamlink/streamlink/pull/6758)) +- Changed: `--hls-segment-ignore-names` to not hardcode `.ts` HLS segment file name extensions ([#6747](https://github.com/streamlink/streamlink/pull/6747)) +- Updated plugins: + - dailymotion: fixed 403 HLS playlist responses ([#6773](https://github.com/streamlink/streamlink/pull/6773)) + - pluto: fixed url matchers and ad detection ([#6767](https://github.com/streamlink/streamlink/pull/6767)) + - soop: fixed CDN mapping for `ld_cdn` based regions ([#6749](https://github.com/streamlink/streamlink/pull/6749)) +- Build: removed unneeded `wheel` dependency from `build-system.requires` and the `build` dependency group ([#6754](https://github.com/streamlink/streamlink/pull/6754)) + +[Full changelog](https://github.com/streamlink/streamlink/compare/8.1.0...8.1.1) + + ## streamlink 8.1.0 (2025-12-14) - Deprecated: `--hls-segment-queue-threshold` in favor of `--stream-segmented-queue-deadline` ([#6734](https://github.com/streamlink/streamlink/pull/6734)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/docs/conf.py new/streamlink-8.1.2/docs/conf.py --- old/streamlink-8.1.0/docs/conf.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/docs/conf.py 2026-01-18 13:20:38.000000000 +0100 @@ -16,7 +16,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "Streamlink" -project_copyright = "2025, Streamlink" +project_copyright = "2026, Streamlink" author = "Streamlink" version = streamlink_version.split("+")[0] release = streamlink_version diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/pyproject.toml new/streamlink-8.1.2/pyproject.toml --- old/streamlink-8.1.0/pyproject.toml 2025-12-14 19:07:27.966754200 +0100 +++ new/streamlink-8.1.2/pyproject.toml 2026-01-18 13:21:06.757069000 +0100 @@ -1,7 +1,6 @@ [build-system] requires = [ "setuptools >=77", - "wheel", # The versioningit build-requirement gets removed from the source distribution, # as the version string is already built into it (see the onbuild versioningit hook): # "versioningit >=2.0.0,<4", @@ -106,12 +105,12 @@ "freezegun >=1.5.0", ] lint = [ - "ruff ==0.14.7", + "ruff ==0.14.11", ] typing = [ { include-group = "test" }, { include-group = "docs" }, - "mypy[faster-cache] ==1.19.0", + "mypy[faster-cache] ==1.19.1", "typing-extensions >=4.0.0", "lxml-stubs", "trio-typing", @@ -132,7 +131,6 @@ { include-group = "dev" }, "shtab", "build", - "wheel", ] script = [ "inflection", @@ -311,6 +309,7 @@ "A003", # builtin-attribute-shadowing "A005", # stdlib-module-shadowing "ASYNC109", # async-function-with-timeout + "B901", # return-in-generator "C408", # unnecessary-collection-call "ISC003", # explicit-string-concatenation "PLC1901", # compare-to-empty-string @@ -324,15 +323,17 @@ [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] +"build_backend/__init__.py" = ["RUF067"] "docs/**" = ["INP"] "script/**" = ["INP"] -"src/streamlink/__init__.py" = ["I"] +"src/streamlink/__init__.py" = ["I", "RUF067"] "src/streamlink/_version.py" = ["I"] "src/streamlink/plugins/*" = ["RUF012"] "src/streamlink/session/http_useragents.py" = ["E501"] "src/streamlink/webbrowser/cdp/devtools/*" = ["E303", "E501", "F401", "TC"] "src/streamlink_cli/main.py" = ["PLW0603"] "tests/**" = ["RUF012"] +"tests/**/__init__.py" = ["RUF067"] [tool.ruff.lint.isort] known-first-party = ["streamlink", "streamlink_cli"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/setup.py new/streamlink-8.1.2/setup.py --- old/streamlink-8.1.0/setup.py 2025-12-14 19:07:27.966754200 +0100 +++ new/streamlink-8.1.2/setup.py 2026-01-18 13:21:06.757069000 +0100 @@ -27,6 +27,11 @@ from pathlib import Path # noqa: E402 +from typing import TYPE_CHECKING # noqa: E402 + + +if TYPE_CHECKING: + from collections.abc import Sequence def is_wheel_for_windows(argv): @@ -50,7 +55,7 @@ # optional data files -data_files = [ +data_files: "list[tuple[str, Sequence[str]]]" = [ # shell completions: # requires pre-built completion files via shtab ("build" dependency group) # `./script/build-shell-completions.sh` @@ -85,5 +90,5 @@ cmdclass=get_cmdclasses(cmdclass), entry_points=entry_points, data_files=data_files, - version="8.1.0", + version="8.1.2", ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/__init__.py new/streamlink-8.1.2/src/streamlink/__init__.py --- old/streamlink-8.1.0/src/streamlink/__init__.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/__init__.py 2026-01-18 13:20:38.000000000 +0100 @@ -1,22 +1,22 @@ -"""Streamlink extracts streams from various services. +""" +Streamlink extracts streams from various services. The main compontent of Streamlink is a command-line utility that launches the streams in a video player. An API is also provided that allows direct access to stream data. -Full documentation is available at https://streamlink.github.io. - +Full documentation is available at https://streamlink.github.io/ """ from streamlink._version import __version__ +from streamlink.api import streams +from streamlink.exceptions import StreamlinkError, PluginError, NoStreamsError, NoPluginError, StreamError +from streamlink.session import Streamlink + __title__ = "streamlink" __license__ = "Simplified BSD" __author__ = "Streamlink" -__copyright__ = "Copyright 2025 Streamlink" +__copyright__ = "Copyright 2026 Streamlink" __credits__ = ["https://github.com/streamlink/streamlink/blob/master/AUTHORS"] - -from streamlink.api import streams -from streamlink.exceptions import StreamlinkError, PluginError, NoStreamsError, NoPluginError, StreamError -from streamlink.session import Streamlink diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/_version.py new/streamlink-8.1.2/src/streamlink/_version.py --- old/streamlink-8.1.0/src/streamlink/_version.py 2025-12-14 19:07:27.967754100 +0100 +++ new/streamlink-8.1.2/src/streamlink/_version.py 2026-01-18 13:21:06.757069000 +0100 @@ -1 +1 @@ -__version__ = "8.1.0" +__version__ = "8.1.2" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/plugins/dailymotion.py new/streamlink-8.1.2/src/streamlink/plugins/dailymotion.py --- old/streamlink-8.1.0/src/streamlink/plugins/dailymotion.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/plugins/dailymotion.py 2026-01-18 13:20:38.000000000 +0100 @@ -83,6 +83,11 @@ self.author = media["owner"]["username"] self.title = media["title"] + # required for preventing 403 responses when using a French IP address + self.session.http.headers.update({ + "priority": "u=1, i", + }) + for quality, streams in media["qualities"].items(): for stream in streams: if stream["type"] == "application/x-mpegURL": diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/plugins/pluto.py new/streamlink-8.1.2/src/streamlink/plugins/pluto.py --- old/streamlink-8.1.0/src/streamlink/plugins/pluto.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/plugins/pluto.py 2026-01-18 13:20:38.000000000 +0100 @@ -10,24 +10,53 @@ import logging import re -from urllib.parse import parse_qsl, urljoin +from dataclasses import dataclass +from typing import ClassVar +from urllib.parse import parse_qsl, urljoin, urlparse from uuid import uuid4 from streamlink.exceptions import PluginError from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import useragents, validate -from streamlink.stream.hls import HLSStream, HLSStreamReader, HLSStreamWriter +from streamlink.stream.hls import HLSSegment, HLSStream, HLSStreamReader, HLSStreamWriter, M3U8Parser from streamlink.utils.url import update_qsd log = logging.getLogger(__name__) -class PlutoHLSStreamWriter(HLSStreamWriter): - ad_re = re.compile(r"_ad/creative/|creative/\d+_ad/|dai\.google\.com|Pluto_TV_OandO/.*(Bumper|plutotv_filler)") +@dataclass +class PlutoHLSSegment(HLSSegment): + ad: bool = False + + _RE_AD: ClassVar[re.Pattern[str]] = re.compile( + r""" + _ad(?:/|%2F|_bumper) + | + plutotv_filler + """, + re.VERBOSE | re.IGNORECASE, + ) + + def __post_init__(self): + self.ad = self._is_ad() + + def _is_ad(self) -> bool: + parsed = urlparse(self.uri) + + if parsed.hostname and parsed.hostname.endswith("dai.google.com"): + return True - def should_filter_segment(self, segment): - return self.ad_re.search(segment.uri) is not None or super().should_filter_segment(segment) + return re.search(self._RE_AD, parsed.path or "") is not None + + +class PlutoM3U8Parser(M3U8Parser): + __segment__ = PlutoHLSSegment + + +class PlutoHLSStreamWriter(HLSStreamWriter): + def should_filter_segment(self, segment: PlutoHLSSegment): # type: ignore[override] + return segment.ad or super().should_filter_segment(segment) class PlutoHLSStreamReader(HLSStreamReader): @@ -37,24 +66,25 @@ class PlutoHLSStream(HLSStream): __shortname__ = "hls-pluto" __reader__ = PlutoHLSStreamReader + __parser__ = PlutoM3U8Parser @pluginmatcher( name="live", pattern=re.compile( - r"https?://(?:www\.)?pluto\.tv/(?:\w{2}/)?live-tv/(?P<id>[^/]+)/?$", + r"https?://(?:www\.)?pluto\.tv/(?:\w{2,}/)?live-tv/(?P<id>[^/?]+)", ), ) @pluginmatcher( name="series", pattern=re.compile( - r"https?://(?:www\.)?pluto\.tv/(?:\w{2}/)?on-demand/series/(?P<id_s>[^/]+)(?:/season/\d+)?/episode/(?P<id_e>[^/]+)/?$", + r"https?://(?:www\.)?pluto\.tv/(?:\w{2,}/)?on-demand/series/(?P<id_s>[^/]+)(?:/season/\d+)?/episode/(?P<id_e>[^/?]+)", ), ) @pluginmatcher( name="movies", pattern=re.compile( - r"https?://(?:www\.)?pluto\.tv/(?:\w{2}/)?on-demand/movies/(?P<id>[^/]+)/?$", + r"https?://(?:www\.)?pluto\.tv/(?:\w{2,}/)?on-demand/movies/(?P<id>[^/?]+)", ), ) class Pluto(Plugin): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/plugins/soop.py new/streamlink-8.1.2/src/streamlink/plugins/soop.py --- old/streamlink-8.1.0/src/streamlink/plugins/soop.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/plugins/soop.py 2026-01-18 13:20:38.000000000 +0100 @@ -95,6 +95,7 @@ CDN_TYPE_MAPPING = { "gs_cdn": "gs_cdn_pc_web", + "lg_cdn": "lg_cdn_pc_web", } CHANNEL_RESULT_OK = 1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/plugins/youtube.py new/streamlink-8.1.2/src/streamlink/plugins/youtube.py --- old/streamlink-8.1.0/src/streamlink/plugins/youtube.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/plugins/youtube.py 2026-01-18 13:20:38.000000000 +0100 @@ -10,7 +10,6 @@ $notes VOD content and protected videos are not supported """ -import json import logging import re from urllib.parse import urlparse, urlunparse @@ -327,28 +326,23 @@ except (PluginError, TypeError): return - try: - api_key = re.search(r'"INNERTUBE_API_KEY":\s*"([^"]+)"', res.text).group(1) - except AttributeError: + if m := re.search(r"""(?P<q1>["'])INNERTUBE_API_KEY(?P=q1)\s*:\s*(?P<q2>["'])(?P<data>.+?)(?P=q2)""", res.text): + api_key = m.group("data") + else: api_key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" - try: - client_version = re.search(r'"INNERTUBE_CLIENT_VERSION":\s*"([\d\.]+)"', res.text).group(1) - except AttributeError: - client_version = "1.20210616.1.0" - - res = self.session.http.post( + return self.session.http.post( "https://www.youtube.com/youtubei/v1/player", headers={"Content-Type": "application/json"}, params={"key": api_key}, - data=json.dumps({ + json={ "videoId": video_id, "contentCheckOk": True, "racyCheckOk": True, "context": { "client": { - "clientName": "WEB", - "clientVersion": client_version, + "clientName": "ANDROID", + "clientVersion": "19.45.36", "platform": "DESKTOP", "clientScreen": "EMBED", "clientFormFactor": "UNKNOWN_FORM_FACTOR", @@ -357,9 +351,11 @@ "user": {"lockedSafetyMode": "false"}, "request": {"useSsl": "true"}, }, - }), + }, + schema=validate.Schema( + validate.parse_json(), + ), ) - return parse_json(res.text) @staticmethod def _data_video_id(data): @@ -371,17 +367,6 @@ if videoId is not None: return videoId - def _data_status(self, data, errorlog=False): - if not data: - return False - status, reason = self._schema_playabilitystatus(data) - # assume that there's an error if reason is set (status will still be "OK" for some reason) - if status != "OK" or reason: - if errorlog: - log.error(f"Could not get video info - {status}: {reason}") - return False - return True - def _get_streams(self): res = self._get_res(self.url) @@ -394,18 +379,25 @@ self.url = self._url_canonical.format(video_id=video_id) res = self._get_res(self.url) - data = self._get_data_from_regex(res, self._re_ytInitialPlayerResponse, "initial player response") - if not self._data_status(data): - data = self._get_data_from_api(res) - if not self._data_status(data, True): - return + # TODO: clean up the validation schemas and how they're applied + + if not (data := self._get_data_from_api(res)): + return + status, reason = self._schema_playabilitystatus(data) + # assume that there's an error if reason is set (status will still be "OK" for some reason) + if status != "OK" or reason: + log.error(f"Could not get video info - {status}: {reason}") + return - self.id, self.author, self.category, self.title, is_live = self._schema_videodetails(data) + # the initial player response contains the category data, which the API response does not + init_player_response = self._get_data_from_regex(res, self._re_ytInitialPlayerResponse, "initial player response") + self.id, self.author, self.category, self.title, is_live = self._schema_videodetails(init_player_response) log.debug(f"Using video ID: {self.id}") if is_live: log.debug("This video is live.") + # TODO: remove parsing of non-HLS stuff, as we don't support this streams = {} hls_manifest, formats, adaptive_formats = self._schema_streamingdata(data) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/session/http_useragents.py new/streamlink-8.1.2/src/streamlink/session/http_useragents.py --- old/streamlink-8.1.0/src/streamlink/session/http_useragents.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/session/http_useragents.py 2026-01-18 13:20:38.000000000 +0100 @@ -1,11 +1,11 @@ -ANDROID = "Mozilla/5.0 (Linux; Android 16) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.172 Mobile Safari/537.36" -CHROME = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" +ANDROID = "Mozilla/5.0 (Linux; Android 16) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.7499.110 Mobile Safari/537.36" +CHROME = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" CHROME_OS = "Mozilla/5.0 (X11; CrOS x86_64 16181.61.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.198 Safari/537.36" -FIREFOX = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0" +FIREFOX = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0" IE_11 = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko" -IPHONE = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1" -OPERA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 OPR/124.0.0.0" -SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15" +IPHONE = "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1" +OPERA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 OPR/124.0.0.0" +SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15" # Backwards compatibility EDGE = CHROME diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/stream/dash/dash.py new/streamlink-8.1.2/src/streamlink/stream/dash/dash.py --- old/streamlink-8.1.0/src/streamlink/stream/dash/dash.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/stream/dash/dash.py 2026-01-18 13:20:38.000000000 +0100 @@ -33,6 +33,8 @@ class DASHStreamWriter(SegmentedStreamWriter[DASHSegment, Response]): + WRITE_CHUNK_SIZE: int = 8192 + reader: DASHStreamReader stream: DASHStream @@ -69,8 +71,8 @@ except StreamError as err: log.error(f"{self.reader.mime_type} segment {name}: failed ({err})") - def write(self, segment, res, chunk_size=8192): - for chunk in res.iter_content(chunk_size): + def write(self, segment: DASHSegment, result: Response, *data): + for chunk in result.iter_content(self.WRITE_CHUNK_SIZE): if self.closed: log.warning(f"{self.reader.mime_type} segment {segment.name}: aborted") return diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/stream/hls/hls.py new/streamlink-8.1.2/src/streamlink/stream/hls/hls.py --- old/streamlink-8.1.0/src/streamlink/stream/hls/hls.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/stream/hls/hls.py 2026-01-18 13:20:38.000000000 +0100 @@ -96,8 +96,7 @@ ignore_names = {*options.get("hls-segment-ignore-names")} if ignore_names: segments = "|".join(map(re.escape, ignore_names)) - # noinspection RegExpUnnecessaryNonCapturingGroup - self.ignore_names = re.compile(rf"(?:{segments})\.ts", re.IGNORECASE) + self.ignore_names = re.compile(segments, re.IGNORECASE) @staticmethod def num_to_iv(n: int) -> bytes: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/stream/hls/segment.py new/streamlink-8.1.2/src/streamlink/stream/hls/segment.py --- old/streamlink-8.1.0/src/streamlink/stream/hls/segment.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/stream/hls/segment.py 2026-01-18 13:20:38.000000000 +0100 @@ -18,6 +18,7 @@ _MEDIA_LANGUAGE_CODES_RESERVED_LOCAL = re.compile(r"^q[a-t][a-z]$") +_MEDIA_LANGUAGE_CODES_PRIVATE_USE_SUBTAGS = re.compile(r"^[a-z]{2,3}-x-\S+$") class Resolution(NamedTuple): @@ -88,7 +89,11 @@ self._parse_language() def _parse_language(self): - if self.language is None or _MEDIA_LANGUAGE_CODES_RESERVED_LOCAL.match(self.language): + if ( + self.language is None + or _MEDIA_LANGUAGE_CODES_RESERVED_LOCAL.match(self.language) + or _MEDIA_LANGUAGE_CODES_PRIVATE_USE_SUBTAGS.match(self.language) + ): return try: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/validate/_validate.py new/streamlink-8.1.2/src/streamlink/validate/_validate.py --- old/streamlink-8.1.0/src/streamlink/validate/_validate.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/validate/_validate.py 2026-01-18 13:20:38.000000000 +0100 @@ -131,7 +131,7 @@ if not schema(value): raise ValidationError( "{callable} is not true", - callable=f"{schema.__name__}({value!r})", + callable=f"{getattr(schema, '__name__', schema.__class__.__name__)}({value!r})", schema=abc.Callable, ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink/validate/_validators.py new/streamlink-8.1.2/src/streamlink/validate/_validators.py --- old/streamlink-8.1.0/src/streamlink/validate/_validators.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink/validate/_validators.py 2026-01-18 13:20:38.000000000 +0100 @@ -21,10 +21,12 @@ if TYPE_CHECKING: from collections.abc import Callable, Mapping + from _typeshed import SupportsLen + # String related validators -_validator_length_ops: Mapping[str, tuple[Callable, str]] = { +_validator_length_ops: Mapping[Literal["lt", "le", "eq", "ge", "gt"], tuple[Callable[[int, int], bool], str]] = { "lt": (operator.lt, "Length must be <{number}, but value is {value}"), "le": (operator.le, "Length must be <={number}, but value is {value}"), "eq": (operator.eq, "Length must be =={number}, but value is {value}"), @@ -62,8 +64,8 @@ schema.validate([1, 2, 3]) # raises ValidationError """ - def length(value): - func, msg = _validator_length_ops.get(op, "ge") + def length(value: SupportsLen): + func, msg = _validator_length_ops.get(op) or _validator_length_ops["ge"] if not func(len(value), number): raise ValidationError( msg, @@ -608,7 +610,9 @@ :raise ValidationError: On parsing error """ - return TransformSchema(_parse_json, *args, **kwargs, exception=ValidationError, schema=None) + kwargs.update(exception=ValidationError, schema=None) + + return TransformSchema(_parse_json, *args, **kwargs) def validator_parse_html(*args, **kwargs) -> TransformSchema: @@ -628,7 +632,9 @@ :raise ValidationError: On parsing error """ - return TransformSchema(_parse_html, *args, **kwargs, exception=ValidationError, schema=None) + kwargs.update(exception=ValidationError, schema=None) + + return TransformSchema(_parse_html, *args, **kwargs) def validator_parse_xml(*args, **kwargs) -> TransformSchema: @@ -648,7 +654,9 @@ :raise ValidationError: On parsing error """ - return TransformSchema(_parse_xml, *args, **kwargs, exception=ValidationError, schema=None) + kwargs.update(exception=ValidationError, schema=None) + + return TransformSchema(_parse_xml, *args, **kwargs) def validator_parse_qsd(*args, **kwargs) -> TransformSchema: @@ -670,6 +678,9 @@ def parser(*_args, **_kwargs): validate(AnySchema(str, bytes), _args[0]) - return _parse_qsd(*_args, **_kwargs, exception=ValidationError, schema=None) + + _kwargs.update(exception=ValidationError, schema=None) + + return _parse_qsd(*_args, **_kwargs) return TransformSchema(parser, *args, **kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink.egg-info/PKG-INFO new/streamlink-8.1.2/src/streamlink.egg-info/PKG-INFO --- old/streamlink-8.1.0/src/streamlink.egg-info/PKG-INFO 2025-12-14 19:07:27.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink.egg-info/PKG-INFO 2026-01-18 13:21:06.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: streamlink -Version: 8.1.0 +Version: 8.1.2 Summary: Streamlink is a command-line utility that extracts streams from various services and pipes them into a video player of choice. Author: Streamlink Author-email: [email protected] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink_cli/__init__.py new/streamlink-8.1.2/src/streamlink_cli/__init__.py --- old/streamlink-8.1.0/src/streamlink_cli/__init__.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink_cli/__init__.py 2026-01-18 13:20:38.000000000 +0100 @@ -1,3 +1,5 @@ +# ruff: noqa: RUF067 + from signal import SIGINT, SIGTERM, signal from sys import exit # noqa: A004 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink_cli/argparser.py new/streamlink-8.1.2/src/streamlink_cli/argparser.py --- old/streamlink-8.1.0/src/streamlink_cli/argparser.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink_cli/argparser.py 2026-01-18 13:20:38.000000000 +0100 @@ -1533,19 +1533,20 @@ ("http_timeout", "http-timeout", None), # stream transport arguments ("hls_duration", "hls-duration", None), # deprecated options must come first + ("hls_segment_queue_threshold", "hls-segment-queue-threshold", None), # deprecated options must come first ("ringbuffer_size", "ringbuffer-size", None), ("mux_subtitles", "mux-subtitles", None), ("stream_segment_attempts", "stream-segment-attempts", None), ("stream_segment_threads", "stream-segment-threads", None), ("stream_segment_timeout", "stream-segment-timeout", None), ("stream_segmented_duration", "stream-segmented-duration", None), + ("stream_segmented_queue_deadline", "stream-segmented-queue-deadline", None), ("stream_timeout", "stream-timeout", None), ("hls_live_edge", "hls-live-edge", None), ("hls_live_restart", "hls-live-restart", None), ("hls_start_offset", "hls-start-offset", None), ("hls_playlist_reload_attempts", "hls-playlist-reload-attempts", None), ("hls_playlist_reload_time", "hls-playlist-reload-time", None), - ("hls_segment_queue_threshold", "hls-segment-queue-threshold", None), ("hls_segment_stream_data", "hls-segment-stream-data", None), ("hls_segment_ignore_names", "hls-segment-ignore-names", None), ("hls_segment_key_uri", "hls-segment-key-uri", None), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/src/streamlink_cli/utils/__init__.py new/streamlink-8.1.2/src/streamlink_cli/utils/__init__.py --- old/streamlink-8.1.0/src/streamlink_cli/utils/__init__.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/src/streamlink_cli/utils/__init__.py 2026-01-18 13:20:38.000000000 +0100 @@ -1,3 +1,5 @@ +# ruff: noqa: RUF067 + import json from datetime import datetime as _datetime diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/tests/cli/test_argparser.py new/streamlink-8.1.2/tests/cli/test_argparser.py --- old/streamlink-8.1.0/tests/cli/test_argparser.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/tests/cli/test_argparser.py 2026-01-18 13:20:38.000000000 +0100 @@ -297,6 +297,50 @@ assert session.options.get_explicit("option") == expected [email protected]( + ("argv", "option", "value", "deprecations"), + [ + pytest.param( + [ + "--stream-segmented-duration=123", + "--hls-duration=321", + ], + "stream-segmented-duration", + 123, + ["`hls-duration` has been deprecated in favor of the `stream-segmented-duration` option"], + id="hls-duration", + ), + pytest.param( + [ + "--stream-segmented-queue-deadline=0.0", + "--hls-segment-queue-threshold=5.0", + ], + "stream-segmented-queue-deadline", + 0.0, + ["`hls-segment-queue-threshold` has been deprecated in favor of the `stream-segmented-queue-deadline` option"], + id="hls-segment-queue-threshold", + ), + ], +) +def test_setup_session_options_deprecations( + parser: ArgumentParser, + session: Streamlink, + argv: list[str], + option: str, + value: Any, + deprecations: list[str], +): + """ + Test all deprecated ArgumentParser arguments that are mapped to Streamlink session options + and test whether the order is correct, so deprecated arguments are always overridden. + """ + args = parser.parse_args(argv) + with pytest.warns(SDW) as warnings: + setup_session_options(session, args) + assert [str(dep.message) for dep in warnings.list] == deprecations + assert session.options.get_explicit(option) == value + + def test_cli_main_setup_session_options(monkeypatch: pytest.MonkeyPatch, parser: ArgumentParser, session: Streamlink): class StopTest(Exception): pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/tests/plugins/test_pluto.py new/streamlink-8.1.2/tests/plugins/test_pluto.py --- old/streamlink-8.1.0/tests/plugins/test_pluto.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/tests/plugins/test_pluto.py 2026-01-18 13:20:38.000000000 +0100 @@ -38,7 +38,4 @@ should_not_match = [ "https://pluto.tv/live-tv", - "https://pluto.tv/en/live-tv/61409f8d6feb30000766b675/details", - "https://pluto.tv/en/on-demand/series/5e00cd538e67b0dcb2cf3bcd/details/season/1", - "https://pluto.tv/en/on-demand/movies/600545d1813b2d001b686fa9/details", ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/tests/resources/hls/test_media_language_special.m3u8 new/streamlink-8.1.2/tests/resources/hls/test_media_language_special.m3u8 --- old/streamlink-8.1.0/tests/resources/hls/test_media_language_special.m3u8 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/tests/resources/hls/test_media_language_special.m3u8 2026-01-18 13:20:38.000000000 +0100 @@ -2,6 +2,7 @@ #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Reserved",LANGUAGE="qaa",AUTOSELECT=NO,URI="qaa.m3u8" +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Private use",LANGUAGE="en-x-foo",AUTOSELECT=NO,URI="en-x-foo.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Does not exist",LANGUAGE="INVALID",AUTOSELECT=NO,URI="invalid.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Undetermined",LANGUAGE="und",AUTOSELECT=NO,URI="und.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Not selected",LANGUAGE="qqq",AUTOSELECT=NO,URI="qqq.m3u8" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/tests/stream/hls/test_hls.py new/streamlink-8.1.2/tests/stream/hls/test_hls.py --- old/streamlink-8.1.0/tests/stream/hls/test_hls.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/tests/stream/hls/test_hls.py 2026-01-18 13:20:38.000000000 +0100 @@ -1398,29 +1398,58 @@ ] @pytest.mark.parametrize( - ("session", "_playlist"), + ("session", "_playlist", "expected"), [ pytest.param( - {"hls-audio-select": ["und", "qaa", "invalid"]}, + {"hls-audio-select": ["und", "qaa", "en-x-foo", "invalid"]}, {"playlist": "hls/test_media_language_special.m3u8"}, - id="special-reserved-invalid", + [ + "http://mocked/path/playlist.m3u8", + "http://mocked/path/qaa.m3u8", + "http://mocked/path/en-x-foo.m3u8", + "http://mocked/path/invalid.m3u8", + "http://mocked/path/und.m3u8", + ], + id="special-reserved-private-invalid", ), pytest.param( - {"hls-audio-select": ["Undetermined", "Reserved", "does NOT exist"]}, + {"hls-audio-select": ["Undetermined", "Reserved", "Private use", "does NOT exist"]}, {"playlist": "hls/test_media_language_special.m3u8"}, + [ + "http://mocked/path/playlist.m3u8", + "http://mocked/path/qaa.m3u8", + "http://mocked/path/en-x-foo.m3u8", + "http://mocked/path/invalid.m3u8", + "http://mocked/path/und.m3u8", + ], id="name-attribute", ), + pytest.param( + {"hls-audio-select": ["*"]}, + {"playlist": "hls/test_media_language_special.m3u8"}, + [ + "http://mocked/path/playlist.m3u8", + "http://mocked/path/qaa.m3u8", + "http://mocked/path/en-x-foo.m3u8", + "http://mocked/path/invalid.m3u8", + "http://mocked/path/und.m3u8", + "http://mocked/path/qqq.m3u8", + ], + id="all", + ), ], indirect=["session", "_playlist"], ) - def test_parse_media_language(self, caplog: pytest.LogCaptureFixture, session: Streamlink, stream: MuxedHLSStream): + def test_parse_media_language( + self, + caplog: pytest.LogCaptureFixture, + session: Streamlink, + stream: MuxedHLSStream, + _playlist: None, + expected: list[str], + ): assert isinstance(stream, MuxedHLSStream) - assert [substream.url for substream in stream.substreams] == [ - "http://mocked/path/playlist.m3u8", - "http://mocked/path/qaa.m3u8", - "http://mocked/path/invalid.m3u8", - "http://mocked/path/und.m3u8", - ] + assert [substream.url for substream in stream.substreams] == expected assert [ record.message for record in caplog.get_records(when="setup") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/streamlink-8.1.0/tests/test_validate.py new/streamlink-8.1.2/tests/test_validate.py --- old/streamlink-8.1.0/tests/test_validate.py 2025-12-14 19:07:02.000000000 +0100 +++ new/streamlink-8.1.2/tests/test_validate.py 2026-01-18 13:20:38.000000000 +0100 @@ -295,9 +295,14 @@ def subject(v): return v is not None + class Subject: + def __call__(self, v): + return v is not None + def test_success(self): value = object() assert validate.validate(self.subject, value) is value + assert validate.validate(self.Subject(), value) is value def test_failure(self): with pytest.raises(ValidationError) as cm: @@ -310,6 +315,16 @@ """, ) + with pytest.raises(ValidationError) as cm: + validate.validate(self.Subject(), None) + assert_validationerror( + cm.value, + """ + ValidationError(Callable): + Subject(None) is not true + """, + ) + class TestPattern: @pytest.mark.parametrize( @@ -626,7 +641,7 @@ with pytest.raises(ValidationError) as cm: # noinspection PyTypeChecker validate.validate( - validate.transform("not a callable"), + validate.transform("not a callable"), # type: ignore "foo", ) assert_validationerror( @@ -986,6 +1001,10 @@ ((3,), [1, 2, 3]), ((3,), "abcd"), ((3,), [1, 2, 3, 4]), + ((3, "invalid"), "abc"), + ((3, "invalid"), [1, 2, 3]), + ((3, "invalid"), "abcd"), + ((3, "invalid"), [1, 2, 3, 4]), ((3, "lt"), "ab"), ((3, "lt"), [1, 2]), ((3, "le"), "ab"), @@ -1010,6 +1029,8 @@ [ ((3,), "ab", "Length must be >=3, but value is 2"), ((3,), [1, 2], "Length must be >=3, but value is 2"), + ((3, "invalid"), "ab", "Length must be >=3, but value is 2"), + ((3, "invalid"), [1, 2], "Length must be >=3, but value is 2"), ((3, "lt"), "abc", "Length must be <3, but value is 3"), ((3, "lt"), [1, 2, 3], "Length must be <3, but value is 3"), ((3, "le"), "abcd", "Length must be <=3, but value is 4"),
