Repository: streamlink/streamlink

Issue:
plugins.lrt: stream is reported Forbidden (though plays if opened manually)
### Checklist

- [X] This is a plugin issue and not a different kind of issue
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed plugin issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

Latest stable release

### Description

When trying to open https://www.lrt.lt/mediateka/tiesiogiai/lrt-televizija or https://www.lrt.lt/mediateka/tiesiogiai/lrt-plius, an error is reported (see the log below).

However, if I try to manually pass the m3u8 URL mentioned in the error to `mpv`, like this (the URL taken from the log below, note the absence of the `%0A` at the end of it):

    mpv https://af5dcb595ac445ab94d7da3af2ebb360.dlvr1.net/lrt_hd/master.m3u8?RxKc3mPWTMxjM1SuDkHZeW1Fw3jEx0oqyryrSQODiHo-Bs31UZVEBEPkLtrdbPKVKrlorJgTLUnSwqks_5Y1QrSQRYfbtlWddOuLrpnY9-kuyM_3QE_yBbqwzhre

...then, after a few ffmpeg errors and warnings, it does open.

The error started to appear a few days ago, worked perfectly before that (so, probably, they changed something at their side).

Thanks.

### Debug log

```text
[cli][debug] OS:         Linux-5.15.0-76-generic-x86_64-with-glibc2.35
[cli][debug] Python:     3.11.3
[cli][debug] Streamlink: 5.5.1
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.5.7
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.2
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.18.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  urllib3: 2.0.2
[cli][debug]  websocket-client: 1.5.2
[cli][debug] Arguments:
[cli][debug]  url=https://www.lrt.lt/mediateka/tiesiogiai/lrt-televizija
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin lrt for URL https://www.lrt.lt/mediateka/tiesiogiai/lrt-televizija
[utils.l10n][debug] Language code: en_US
error: Unable to open URL: https://af5dcb595ac445ab94d7da3af2ebb360.dlvr1.net/lrt_hd/master.m3u8?RxKc3mPWTMxjM1SuDkHZeW1Fw3jEx0oqyryrSQODiHo-Bs31UZVEBEPkLtrdbPKVKrlorJgTLUnSwqks_5Y1QrSQRYfbtlWddOuLrpnY9-kuyM_3QE_yBbqwzhre
 (403 Client Error: Forbidden for url: https://af5dcb595ac445ab94d7da3af2ebb360.dlvr1.net/lrt_hd/master.m3u8?RxKc3mPWTMxjM1SuDkHZeW1Fw3jEx0oqyryrSQODiHo-Bs31UZVEBEPkLtrdbPKVKrlorJgTLUnSwqks_5Y1QrSQRYfbtlWddOuLrpnY9-kuyM_3QE_yBbqwzhre%0A)
```


Discussion:

Patch:
diff --git a/src/streamlink/plugins/lrt.py b/src/streamlink/plugins/lrt.py
index 23d5bc08..b5fc49a8 100644
--- a/src/streamlink/plugins/lrt.py
+++ b/src/streamlink/plugins/lrt.py
@@ -4,34 +4,42 @@ $url lrt.lt
 $type live
 """
 
-import logging
 import re
 
 from streamlink.plugin import Plugin, pluginmatcher
+from streamlink.plugin.api import validate
 from streamlink.stream.hls import HLSStream
 
 
-log = logging.getLogger(__name__)
-
-
 @pluginmatcher(re.compile(
     r"https?://(?:www\.)?lrt\.lt/mediateka/tiesiogiai/",
 ))
 class LRT(Plugin):
-    _video_id_re = re.compile(r"""var\svideo_id\s*=\s*["'](?P<video_id>\w+)["']""")
-    API_URL = "https://www.lrt.lt/servisai/stream_url/live/get_live_url.php?channel={0}"
-
     def _get_streams(self):
-        page = self.session.http.get(self.url)
-        m = self._video_id_re.search(page.text)
-        if m:
-            video_id = m.group("video_id")
-            data = self.session.http.get(self.API_URL.format(video_id)).json()
-            hls_url = data["response"]["data"]["content"]
-
-            yield from HLSStream.parse_variant_playlist(self.session, hls_url).items()
-        else:
-            log.debug("No match for video_id regex")
+        token_url = self.session.http.get(self.url, schema=validate.Schema(
+            re.compile(r"""var\s+tokenURL\s*=\s*(?P<q>["'])(?P<url>https://\S+)(?P=q)"""),
+            validate.none_or_all(validate.get("url")),
+        ))
+        if not token_url:
+            return
+
+        hls_url = self.session.http.get(token_url, schema=validate.Schema(
+            validate.parse_json(),
+            {
+                "response": {
+                    "data": {
+                        "content": validate.all(
+                            str,
+                            validate.transform(lambda url: url.strip()),
+                            validate.url(path=validate.endswith(".m3u8")),
+                        ),
+                    },
+                },
+            },
+            validate.get(("response", "data", "content")),
+        ))
+
+        return HLSStream.parse_variant_playlist(self.session, hls_url)
 
 
 __plugin__ = LRT
diff --git a/src/streamlink/plugins/rtvs.py b/src/streamlink/plugins/rtvs.py
index a0bc6be0..c3d7f4f1 100644
--- a/src/streamlink/plugins/rtvs.py
+++ b/src/streamlink/plugins/rtvs.py
@@ -6,47 +6,52 @@ $region Slovakia
 """
 
 import re
+from urllib.parse import urlparse
 
 from streamlink.plugin import Plugin, pluginmatcher
 from streamlink.plugin.api import validate
 from streamlink.stream.hls import HLSStream
-from streamlink.utils.parse import parse_json
 
 
 @pluginmatcher(re.compile(
-    r"https?://www\.rtvs\.sk/televizia/live-[\w-]+",
+    r"https?://www\.rtvs\.sk/televizia/(?:live-|sport)",
 ))
 class Rtvs(Plugin):
-    _re_channel_id = re.compile(r"'stream':\s*'live-(\d+)'")
-
     def _get_streams(self):
-        res = self.session.http.get(self.url)
-        m = self._re_channel_id.search(res.text)
-        if not m:
+        channel = self.session.http.get(self.url, schema=validate.Schema(
+            validate.parse_html(),
+            validate.xml_xpath_string(".//iframe[@id='player_live']//@src"),
+            validate.url(path=validate.startswith("/embed/live/")),
+            validate.transform(lambda embed: urlparse(embed).path[len("/embed/live/"):]),
+        ))
+        if not channel:
             return
 
-        res = self.session.http.get(
+        videos = self.session.http.get(
             "https://www.rtvs.sk/json/live5f.json",
             params={
-                "c": m.group(1),
+                "c": channel,
                 "b": "mozilla",
                 "p": "win",
                 "f": "0",
                 "d": "1",
             },
+            schema=validate.Schema(
+                validate.parse_json(),
+                {
+                    "clip": {
+                        "sources": [{
+                            "src": validate.url(),
+                            "type": str,
+                        }],
+                    },
+                },
+                validate.get(("clip", "sources")),
+                validate.filter(lambda n: n["type"] == "application/x-mpegurl"),
+            ),
         )
-        videos = parse_json(res.text, schema=validate.Schema({
-            "clip": {
-                "sources": [{
-                    "src": validate.url(),
-                    "type": str,
-                }],
-            }},
-            validate.get(("clip", "sources")),
-            validate.filter(lambda n: n["type"] == "application/x-mpegurl"),
-        ))
         for video in videos:
-            yield from HLSStream.parse_variant_playlist(self.session, video["src"]).items()
+            return HLSStream.parse_variant_playlist(self.session, video["src"])
 
 
 __plugin__ = Rtvs


Test patch:
diff --git a/tests/plugins/test_rtvs.py b/tests/plugins/test_rtvs.py
index d20c3a4f..b757d19d 100644
--- a/tests/plugins/test_rtvs.py
+++ b/tests/plugins/test_rtvs.py
@@ -6,13 +6,17 @@ class TestPluginCanHandleUrlRtvs(PluginCanHandleUrl):
     __plugin__ = Rtvs
 
     should_match = [
-        "http://www.rtvs.sk/televizia/live-1",
-        "http://www.rtvs.sk/televizia/live-2",
-        "http://www.rtvs.sk/televizia/live-o",
+        "https://www.rtvs.sk/televizia/live-1",
+        "https://www.rtvs.sk/televizia/live-2",
         "https://www.rtvs.sk/televizia/live-3",
+        "https://www.rtvs.sk/televizia/live-o",
         "https://www.rtvs.sk/televizia/live-rtvs",
+        "https://www.rtvs.sk/televizia/live-nr-sr",
+        "https://www.rtvs.sk/televizia/sport",
     ]
 
     should_not_match = [
         "http://www.rtvs.sk/",
+        "http://www.rtvs.sk/televizia/archiv",
+        "http://www.rtvs.sk/televizia/program",
     ]