We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
plugins.rtve: ZTNR.translate() runs endlessly
### 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

RTVE plugin is not resolving any streams and stuck after message "Found matching plugin rtve for URL". Upon debugging in Python the following while block seems to cause an endless loop: https://github.com/streamlink/streamlink/blob/master/src/streamlink/plugins/rtve.py#L111

Thanks for the good work!
 
Cheers.

### Debug log

```text
bin\streamlink.exe -l debug https://rtve.es/play/videos/directo/canales-lineales/24h
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.10.7
[cli][debug] Streamlink: 5.0.0
[cli][debug] Dependencies:
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.1
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.15.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.28.1
[cli][debug]  websocket-client: 1.4.1
[cli][debug] Arguments:
[cli][debug]  url=https://rtve.es/play/videos/directo/canales-lineales/24h
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin rtve for URL https://rtve.es/play/videos/directo/canales-lineales/24h
```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/streamlink/plugins/rtve.py`
Content:
```
1 """
2 $description Live TV channels and video on-demand service from RTVE, a Spanish public, state-owned broadcaster.
3 $url rtve.es
4 $type live, vod
5 $region Spain
6 """
7 
8 import logging
9 import re
10 from base64 import b64decode
11 from io import BytesIO
12 from typing import Iterator, Sequence, Tuple
13 from urllib.parse import urlparse
14 
15 from streamlink.plugin import Plugin, pluginargument, pluginmatcher
16 from streamlink.plugin.api import validate
17 from streamlink.stream.ffmpegmux import MuxedStream
18 from streamlink.stream.hls import HLSStream
19 from streamlink.stream.http import HTTPStream
20 from streamlink.utils.url import update_scheme
21 
22 log = logging.getLogger(__name__)
23 
24 
25 class Base64Reader:
26     def __init__(self, data: str):
27         stream = BytesIO(b64decode(data))
28 
29         def _iterate():
30             while True:
31                 chunk = stream.read(1)
32                 if len(chunk) == 0:  # pragma: no cover
33                     return
34                 yield ord(chunk)
35 
36         self._iterator: Iterator[int] = _iterate()
37 
38     def read(self, num: int) -> Sequence[int]:
39         res = []
40         for _ in range(num):
41             item = next(self._iterator, None)
42             if item is None:  # pragma: no cover
43                 break
44             res.append(item)
45         return res
46 
47     def skip(self, num: int) -> None:
48         self.read(num)
49 
50     def read_chars(self, num: int) -> str:
51         return "".join(chr(item) for item in self.read(num))
52 
53     def read_int(self) -> int:
54         a, b, c, d = self.read(4)
55         return a << 24 | b << 16 | c << 8 | d
56 
57     def read_chunk(self) -> Tuple[str, Sequence[int]]:
58         size = self.read_int()
59         chunktype = self.read_chars(4)
60         chunkdata = self.read(size)
61         if len(chunkdata) != size:  # pragma: no cover
62             raise ValueError("Invalid chunk length")
63         self.skip(4)
64         return chunktype, chunkdata
65 
66 
67 class ZTNR:
68     @staticmethod
69     def _get_alphabet(text: str) -> str:
70         res = []
71         j = 0
72         k = 0
73         for char in text:
74             if k > 0:
75                 k -= 1
76             else:
77                 res.append(char)
78                 j = (j + 1) % 4
79                 k = j
80         return "".join(res)
81 
82     @staticmethod
83     def _get_url(text: str, alphabet: str) -> str:
84         res = []
85         j = 0
86         n = 0
87         k = 3
88         cont = 0
89         for char in text:
90             if j == 0:
91                 n = int(char) * 10
92                 j = 1
93             elif k > 0:
94                 k -= 1
95             else:
96                 res.append(alphabet[n + int(char)])
97                 j = 0
98                 k = cont % 4
99                 cont += 1
100         return "".join(res)
101 
102     @classmethod
103     def _get_source(cls, alphabet: str, data: str) -> str:
104         return cls._get_url(data, cls._get_alphabet(alphabet))
105 
106     @classmethod
107     def translate(cls, data: str) -> Iterator[Tuple[str, str]]:
108         reader = Base64Reader(data.replace("\n", ""))
109         reader.skip(8)
110         chunk_type, chunk_data = reader.read_chunk()
111         while chunk_type != "IEND":
112             if chunk_type == "tEXt":
113                 content = "".join(chr(item) for item in chunk_data if item > 0)
114                 if "#" not in content or "%%" not in content:  # pragma: no cover
115                     continue
116                 alphabet, content = content.split("#", 1)
117                 quality, content = content.split("%%", 1)
118                 yield quality, cls._get_source(alphabet, content)
119             chunk_type, chunk_data = reader.read_chunk()
120 
121 
122 @pluginmatcher(re.compile(
123     r"https?://(?:www\.)?rtve\.es/play/videos/.+"
124 ))
125 @pluginargument(
126     "mux-subtitles",
127     is_global=True,
128 )
129 class Rtve(Plugin):
130     URL_M3U8 = "https://ztnr.rtve.es/ztnr/{id}.m3u8"
131     URL_VIDEOS = "https://ztnr.rtve.es/ztnr/movil/thumbnail/rtveplayw/videos/{id}.png?q=v2"
132     URL_SUBTITLES = "https://www.rtve.es/api/videos/{id}/subtitulos.json"
133 
134     def _get_streams(self):
135         self.id = self.session.http.get(self.url, schema=validate.Schema(
136             re.compile(r"\bdata-setup='({.+?})'", re.DOTALL),
137             validate.none_or_all(
138                 validate.get(1),
139                 validate.parse_json(),
140                 {
141                     "idAsset": validate.any(int, validate.all(str, validate.transform(int))),
142                 },
143                 validate.get("idAsset"),
144             ),
145         ))
146         if not self.id:
147             return
148 
149         # check obfuscated stream URLs via self.URL_VIDEOS and ZTNR.translate() first
150         # self.URL_M3U8 appears to be valid for all streams, but doesn't provide any content in same cases
151         urls = self.session.http.get(
152             self.URL_VIDEOS.format(id=self.id),
153             schema=validate.Schema(
154                 validate.transform(ZTNR.translate),
155                 validate.transform(list),
156                 [(str, validate.url())],
157             ),
158         )
159 
160         # then fall back to self.URL_M3U8
161         if not urls:
162             url = self.URL_M3U8.format(id=self.id)
163         else:
164             url = next((url for _, url in urls if urlparse(url).path.endswith(".m3u8")), None)
165             if not url:
166                 url = next((url for _, url in urls if urlparse(url).path.endswith(".mp4")), None)
167                 if url:
168                     yield "vod", HTTPStream(self.session, url)
169                 return
170 
171         streams = HLSStream.parse_variant_playlist(self.session, url).items()
172 
173         if self.options.get("mux-subtitles"):
174             subs = self.session.http.get(
175                 self.URL_SUBTITLES.format(id=self.id),
176                 schema=validate.Schema(
177                     validate.parse_json(),
178                     {
179                         "page": {
180                             "items": [{
181                                 "lang": str,
182                                 "src": validate.url(),
183                             }],
184                         },
185                     },
186                     validate.get(("page", "items")),
187                 ),
188             )
189             if subs:
190                 subtitles = {
191                     s["lang"]: HTTPStream(self.session, update_scheme("https://", s["src"], force=True))
192                     for s in subs
193                 }
194                 for quality, stream in streams:
195                     yield quality, MuxedStream(self.session, stream, subtitles=subtitles)
196                 return
197 
198         yield from streams
199 
200 
201 __plugin__ = Rtve
202
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/streamlink/plugins/rtve.py b/src/streamlink/plugins/rtve.py
--- a/src/streamlink/plugins/rtve.py
+++ b/src/streamlink/plugins/rtve.py
@@ -12,7 +12,7 @@
 from typing import Iterator, Sequence, Tuple
 from urllib.parse import urlparse
 
-from streamlink.plugin import Plugin, pluginargument, pluginmatcher
+from streamlink.plugin import Plugin, PluginError, pluginargument, pluginmatcher
 from streamlink.plugin.api import validate
 from streamlink.stream.ffmpegmux import MuxedStream
 from streamlink.stream.hls import HLSStream
@@ -29,7 +29,7 @@
         def _iterate():
             while True:
                 chunk = stream.read(1)
-                if len(chunk) == 0:  # pragma: no cover
+                if len(chunk) == 0:
                     return
                 yield ord(chunk)
 
@@ -39,7 +39,7 @@
         res = []
         for _ in range(num):
             item = next(self._iterator, None)
-            if item is None:  # pragma: no cover
+            if item is None:
                 break
             res.append(item)
         return res
@@ -63,6 +63,14 @@
         self.skip(4)
         return chunktype, chunkdata
 
+    def __iter__(self):
+        self.skip(8)
+        while True:
+            try:
+                yield self.read_chunk()
+            except ValueError:
+                return
+
 
 class ZTNR:
     @staticmethod
@@ -106,17 +114,16 @@
     @classmethod
     def translate(cls, data: str) -> Iterator[Tuple[str, str]]:
         reader = Base64Reader(data.replace("\n", ""))
-        reader.skip(8)
-        chunk_type, chunk_data = reader.read_chunk()
-        while chunk_type != "IEND":
+        for chunk_type, chunk_data in reader:
+            if chunk_type == "IEND":
+                break
             if chunk_type == "tEXt":
                 content = "".join(chr(item) for item in chunk_data if item > 0)
-                if "#" not in content or "%%" not in content:  # pragma: no cover
+                if "#" not in content or "%%" not in content:
                     continue
                 alphabet, content = content.split("#", 1)
                 quality, content = content.split("%%", 1)
                 yield quality, cls._get_source(alphabet, content)
-            chunk_type, chunk_data = reader.read_chunk()
 
 
 @pluginmatcher(re.compile(
@@ -147,18 +154,19 @@
             return
 
         # check obfuscated stream URLs via self.URL_VIDEOS and ZTNR.translate() first
-        # self.URL_M3U8 appears to be valid for all streams, but doesn't provide any content in same cases
-        urls = self.session.http.get(
-            self.URL_VIDEOS.format(id=self.id),
-            schema=validate.Schema(
-                validate.transform(ZTNR.translate),
-                validate.transform(list),
-                [(str, validate.url())],
-            ),
-        )
-
-        # then fall back to self.URL_M3U8
-        if not urls:
+        # self.URL_M3U8 appears to be valid for all streams, but doesn't provide any content in some cases
+        try:
+            urls = self.session.http.get(
+                self.URL_VIDEOS.format(id=self.id),
+                schema=validate.Schema(
+                    validate.transform(ZTNR.translate),
+                    validate.transform(list),
+                    [(str, validate.url())],
+                    validate.length(1),
+                ),
+            )
+        except PluginError:
+            # catch HTTP errors and validation errors, and fall back to generic HLS URL template
             url = self.URL_M3U8.format(id=self.id)
         else:
             url = next((url for _, url in urls if urlparse(url).path.endswith(".m3u8")), None)

{"golden_diff": "diff --git a/src/streamlink/plugins/rtve.py b/src/streamlink/plugins/rtve.py\n--- a/src/streamlink/plugins/rtve.py\n+++ b/src/streamlink/plugins/rtve.py\n@@ -12,7 +12,7 @@\n from typing import Iterator, Sequence, Tuple\n from urllib.parse import urlparse\n \n-from streamlink.plugin import Plugin, pluginargument, pluginmatcher\n+from streamlink.plugin import Plugin, PluginError, pluginargument, pluginmatcher\n from streamlink.plugin.api import validate\n from streamlink.stream.ffmpegmux import MuxedStream\n from streamlink.stream.hls import HLSStream\n@@ -29,7 +29,7 @@\n         def _iterate():\n             while True:\n                 chunk = stream.read(1)\n-                if len(chunk) == 0:  # pragma: no cover\n+                if len(chunk) == 0:\n                     return\n                 yield ord(chunk)\n \n@@ -39,7 +39,7 @@\n         res = []\n         for _ in range(num):\n             item = next(self._iterator, None)\n-            if item is None:  # pragma: no cover\n+            if item is None:\n                 break\n             res.append(item)\n         return res\n@@ -63,6 +63,14 @@\n         self.skip(4)\n         return chunktype, chunkdata\n \n+    def __iter__(self):\n+        self.skip(8)\n+        while True:\n+            try:\n+                yield self.read_chunk()\n+            except ValueError:\n+                return\n+\n \n class ZTNR:\n     @staticmethod\n@@ -106,17 +114,16 @@\n     @classmethod\n     def translate(cls, data: str) -> Iterator[Tuple[str, str]]:\n         reader = Base64Reader(data.replace(\"\\n\", \"\"))\n-        reader.skip(8)\n-        chunk_type, chunk_data = reader.read_chunk()\n-        while chunk_type != \"IEND\":\n+        for chunk_type, chunk_data in reader:\n+            if chunk_type == \"IEND\":\n+                break\n             if chunk_type == \"tEXt\":\n                 content = \"\".join(chr(item) for item in chunk_data if item > 0)\n-                if \"#\" not in content or \"%%\" not in content:  # pragma: no cover\n+                if \"#\" not in content or \"%%\" not in content:\n                     continue\n                 alphabet, content = content.split(\"#\", 1)\n                 quality, content = content.split(\"%%\", 1)\n                 yield quality, cls._get_source(alphabet, content)\n-            chunk_type, chunk_data = reader.read_chunk()\n \n \n @pluginmatcher(re.compile(\n@@ -147,18 +154,19 @@\n             return\n \n         # check obfuscated stream URLs via self.URL_VIDEOS and ZTNR.translate() first\n-        # self.URL_M3U8 appears to be valid for all streams, but doesn't provide any content in same cases\n-        urls = self.session.http.get(\n-            self.URL_VIDEOS.format(id=self.id),\n-            schema=validate.Schema(\n-                validate.transform(ZTNR.translate),\n-                validate.transform(list),\n-                [(str, validate.url())],\n-            ),\n-        )\n-\n-        # then fall back to self.URL_M3U8\n-        if not urls:\n+        # self.URL_M3U8 appears to be valid for all streams, but doesn't provide any content in some cases\n+        try:\n+            urls = self.session.http.get(\n+                self.URL_VIDEOS.format(id=self.id),\n+                schema=validate.Schema(\n+                    validate.transform(ZTNR.translate),\n+                    validate.transform(list),\n+                    [(str, validate.url())],\n+                    validate.length(1),\n+                ),\n+            )\n+        except PluginError:\n+            # catch HTTP errors and validation errors, and fall back to generic HLS URL template\n             url = self.URL_M3U8.format(id=self.id)\n         else:\n             url = next((url for _, url in urls if urlparse(url).path.endswith(\".m3u8\")), None)\n", "issue": "plugins.rtve: ZTNR.translate() runs endlessly\n### Checklist\n\n- [X] This is a plugin issue and not a different kind of issue\n- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)\n- [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)\n- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)\n\n### Streamlink version\n\nLatest stable release\n\n### Description\n\nRTVE plugin is not resolving any streams and stuck after message \"Found matching plugin rtve for URL\". Upon debugging in Python the following while block seems to cause an endless loop: https://github.com/streamlink/streamlink/blob/master/src/streamlink/plugins/rtve.py#L111\r\n\r\nThanks for the good work!\r\n \r\nCheers.\n\n### Debug log\n\n```text\nbin\\streamlink.exe -l debug https://rtve.es/play/videos/directo/canales-lineales/24h\r\n[cli][debug] OS:         Windows 10\r\n[cli][debug] Python:     3.10.7\r\n[cli][debug] Streamlink: 5.0.0\r\n[cli][debug] Dependencies:\r\n[cli][debug]  isodate: 0.6.1\r\n[cli][debug]  lxml: 4.9.1\r\n[cli][debug]  pycountry: 22.3.5\r\n[cli][debug]  pycryptodome: 3.15.0\r\n[cli][debug]  PySocks: 1.7.1\r\n[cli][debug]  requests: 2.28.1\r\n[cli][debug]  websocket-client: 1.4.1\r\n[cli][debug] Arguments:\r\n[cli][debug]  url=https://rtve.es/play/videos/directo/canales-lineales/24h\r\n[cli][debug]  --loglevel=debug\r\n[cli][info] Found matching plugin rtve for URL https://rtve.es/play/videos/directo/canales-lineales/24h\n```\n\n", "before_files": [{"content": "\"\"\"\n$description Live TV channels and video on-demand service from RTVE, a Spanish public, state-owned broadcaster.\n$url rtve.es\n$type live, vod\n$region Spain\n\"\"\"\n\nimport logging\nimport re\nfrom base64 import b64decode\nfrom io import BytesIO\nfrom typing import Iterator, Sequence, Tuple\nfrom urllib.parse import urlparse\n\nfrom streamlink.plugin import Plugin, pluginargument, pluginmatcher\nfrom streamlink.plugin.api import validate\nfrom streamlink.stream.ffmpegmux import MuxedStream\nfrom streamlink.stream.hls import HLSStream\nfrom streamlink.stream.http import HTTPStream\nfrom streamlink.utils.url import update_scheme\n\nlog = logging.getLogger(__name__)\n\n\nclass Base64Reader:\n    def __init__(self, data: str):\n        stream = BytesIO(b64decode(data))\n\n        def _iterate():\n            while True:\n                chunk = stream.read(1)\n                if len(chunk) == 0:  # pragma: no cover\n                    return\n                yield ord(chunk)\n\n        self._iterator: Iterator[int] = _iterate()\n\n    def read(self, num: int) -> Sequence[int]:\n        res = []\n        for _ in range(num):\n            item = next(self._iterator, None)\n            if item is None:  # pragma: no cover\n                break\n            res.append(item)\n        return res\n\n    def skip(self, num: int) -> None:\n        self.read(num)\n\n    def read_chars(self, num: int) -> str:\n        return \"\".join(chr(item) for item in self.read(num))\n\n    def read_int(self) -> int:\n        a, b, c, d = self.read(4)\n        return a << 24 | b << 16 | c << 8 | d\n\n    def read_chunk(self) -> Tuple[str, Sequence[int]]:\n        size = self.read_int()\n        chunktype = self.read_chars(4)\n        chunkdata = self.read(size)\n        if len(chunkdata) != size:  # pragma: no cover\n            raise ValueError(\"Invalid chunk length\")\n        self.skip(4)\n        return chunktype, chunkdata\n\n\nclass ZTNR:\n    @staticmethod\n    def _get_alphabet(text: str) -> str:\n        res = []\n        j = 0\n        k = 0\n        for char in text:\n            if k > 0:\n                k -= 1\n            else:\n                res.append(char)\n                j = (j + 1) % 4\n                k = j\n        return \"\".join(res)\n\n    @staticmethod\n    def _get_url(text: str, alphabet: str) -> str:\n        res = []\n        j = 0\n        n = 0\n        k = 3\n        cont = 0\n        for char in text:\n            if j == 0:\n                n = int(char) * 10\n                j = 1\n            elif k > 0:\n                k -= 1\n            else:\n                res.append(alphabet[n + int(char)])\n                j = 0\n                k = cont % 4\n                cont += 1\n        return \"\".join(res)\n\n    @classmethod\n    def _get_source(cls, alphabet: str, data: str) -> str:\n        return cls._get_url(data, cls._get_alphabet(alphabet))\n\n    @classmethod\n    def translate(cls, data: str) -> Iterator[Tuple[str, str]]:\n        reader = Base64Reader(data.replace(\"\\n\", \"\"))\n        reader.skip(8)\n        chunk_type, chunk_data = reader.read_chunk()\n        while chunk_type != \"IEND\":\n            if chunk_type == \"tEXt\":\n                content = \"\".join(chr(item) for item in chunk_data if item > 0)\n                if \"#\" not in content or \"%%\" not in content:  # pragma: no cover\n                    continue\n                alphabet, content = content.split(\"#\", 1)\n                quality, content = content.split(\"%%\", 1)\n                yield quality, cls._get_source(alphabet, content)\n            chunk_type, chunk_data = reader.read_chunk()\n\n\n@pluginmatcher(re.compile(\n    r\"https?://(?:www\\.)?rtve\\.es/play/videos/.+\"\n))\n@pluginargument(\n    \"mux-subtitles\",\n    is_global=True,\n)\nclass Rtve(Plugin):\n    URL_M3U8 = \"https://ztnr.rtve.es/ztnr/{id}.m3u8\"\n    URL_VIDEOS = \"https://ztnr.rtve.es/ztnr/movil/thumbnail/rtveplayw/videos/{id}.png?q=v2\"\n    URL_SUBTITLES = \"https://www.rtve.es/api/videos/{id}/subtitulos.json\"\n\n    def _get_streams(self):\n        self.id = self.session.http.get(self.url, schema=validate.Schema(\n            re.compile(r\"\\bdata-setup='({.+?})'\", re.DOTALL),\n            validate.none_or_all(\n                validate.get(1),\n                validate.parse_json(),\n                {\n                    \"idAsset\": validate.any(int, validate.all(str, validate.transform(int))),\n                },\n                validate.get(\"idAsset\"),\n            ),\n        ))\n        if not self.id:\n            return\n\n        # check obfuscated stream URLs via self.URL_VIDEOS and ZTNR.translate() first\n        # self.URL_M3U8 appears to be valid for all streams, but doesn't provide any content in same cases\n        urls = self.session.http.get(\n            self.URL_VIDEOS.format(id=self.id),\n            schema=validate.Schema(\n                validate.transform(ZTNR.translate),\n                validate.transform(list),\n                [(str, validate.url())],\n            ),\n        )\n\n        # then fall back to self.URL_M3U8\n        if not urls:\n            url = self.URL_M3U8.format(id=self.id)\n        else:\n            url = next((url for _, url in urls if urlparse(url).path.endswith(\".m3u8\")), None)\n            if not url:\n                url = next((url for _, url in urls if urlparse(url).path.endswith(\".mp4\")), None)\n                if url:\n                    yield \"vod\", HTTPStream(self.session, url)\n                return\n\n        streams = HLSStream.parse_variant_playlist(self.session, url).items()\n\n        if self.options.get(\"mux-subtitles\"):\n            subs = self.session.http.get(\n                self.URL_SUBTITLES.format(id=self.id),\n                schema=validate.Schema(\n                    validate.parse_json(),\n                    {\n                        \"page\": {\n                            \"items\": [{\n                                \"lang\": str,\n                                \"src\": validate.url(),\n                            }],\n                        },\n                    },\n                    validate.get((\"page\", \"items\")),\n                ),\n            )\n            if subs:\n                subtitles = {\n                    s[\"lang\"]: HTTPStream(self.session, update_scheme(\"https://\", s[\"src\"], force=True))\n                    for s in subs\n                }\n                for quality, stream in streams:\n                    yield quality, MuxedStream(self.session, stream, subtitles=subtitles)\n                return\n\n        yield from streams\n\n\n__plugin__ = Rtve\n", "path": "src/streamlink/plugins/rtve.py"}], "after_files": [{"content": "\"\"\"\n$description Live TV channels and video on-demand service from RTVE, a Spanish public, state-owned broadcaster.\n$url rtve.es\n$type live, vod\n$region Spain\n\"\"\"\n\nimport logging\nimport re\nfrom base64 import b64decode\nfrom io import BytesIO\nfrom typing import Iterator, Sequence, Tuple\nfrom urllib.parse import urlparse\n\nfrom streamlink.plugin import Plugin, PluginError, pluginargument, pluginmatcher\nfrom streamlink.plugin.api import validate\nfrom streamlink.stream.ffmpegmux import MuxedStream\nfrom streamlink.stream.hls import HLSStream\nfrom streamlink.stream.http import HTTPStream\nfrom streamlink.utils.url import update_scheme\n\nlog = logging.getLogger(__name__)\n\n\nclass Base64Reader:\n    def __init__(self, data: str):\n        stream = BytesIO(b64decode(data))\n\n        def _iterate():\n            while True:\n                chunk = stream.read(1)\n                if len(chunk) == 0:\n                    return\n                yield ord(chunk)\n\n        self._iterator: Iterator[int] = _iterate()\n\n    def read(self, num: int) -> Sequence[int]:\n        res = []\n        for _ in range(num):\n            item = next(self._iterator, None)\n            if item is None:\n                break\n            res.append(item)\n        return res\n\n    def skip(self, num: int) -> None:\n        self.read(num)\n\n    def read_chars(self, num: int) -> str:\n        return \"\".join(chr(item) for item in self.read(num))\n\n    def read_int(self) -> int:\n        a, b, c, d = self.read(4)\n        return a << 24 | b << 16 | c << 8 | d\n\n    def read_chunk(self) -> Tuple[str, Sequence[int]]:\n        size = self.read_int()\n        chunktype = self.read_chars(4)\n        chunkdata = self.read(size)\n        if len(chunkdata) != size:  # pragma: no cover\n            raise ValueError(\"Invalid chunk length\")\n        self.skip(4)\n        return chunktype, chunkdata\n\n    def __iter__(self):\n        self.skip(8)\n        while True:\n            try:\n                yield self.read_chunk()\n            except ValueError:\n                return\n\n\nclass ZTNR:\n    @staticmethod\n    def _get_alphabet(text: str) -> str:\n        res = []\n        j = 0\n        k = 0\n        for char in text:\n            if k > 0:\n                k -= 1\n            else:\n                res.append(char)\n                j = (j + 1) % 4\n                k = j\n        return \"\".join(res)\n\n    @staticmethod\n    def _get_url(text: str, alphabet: str) -> str:\n        res = []\n        j = 0\n        n = 0\n        k = 3\n        cont = 0\n        for char in text:\n            if j == 0:\n                n = int(char) * 10\n                j = 1\n            elif k > 0:\n                k -= 1\n            else:\n                res.append(alphabet[n + int(char)])\n                j = 0\n                k = cont % 4\n                cont += 1\n        return \"\".join(res)\n\n    @classmethod\n    def _get_source(cls, alphabet: str, data: str) -> str:\n        return cls._get_url(data, cls._get_alphabet(alphabet))\n\n    @classmethod\n    def translate(cls, data: str) -> Iterator[Tuple[str, str]]:\n        reader = Base64Reader(data.replace(\"\\n\", \"\"))\n        for chunk_type, chunk_data in reader:\n            if chunk_type == \"IEND\":\n                break\n            if chunk_type == \"tEXt\":\n                content = \"\".join(chr(item) for item in chunk_data if item > 0)\n                if \"#\" not in content or \"%%\" not in content:\n                    continue\n                alphabet, content = content.split(\"#\", 1)\n                quality, content = content.split(\"%%\", 1)\n                yield quality, cls._get_source(alphabet, content)\n\n\n@pluginmatcher(re.compile(\n    r\"https?://(?:www\\.)?rtve\\.es/play/videos/.+\"\n))\n@pluginargument(\n    \"mux-subtitles\",\n    is_global=True,\n)\nclass Rtve(Plugin):\n    URL_M3U8 = \"https://ztnr.rtve.es/ztnr/{id}.m3u8\"\n    URL_VIDEOS = \"https://ztnr.rtve.es/ztnr/movil/thumbnail/rtveplayw/videos/{id}.png?q=v2\"\n    URL_SUBTITLES = \"https://www.rtve.es/api/videos/{id}/subtitulos.json\"\n\n    def _get_streams(self):\n        self.id = self.session.http.get(self.url, schema=validate.Schema(\n            re.compile(r\"\\bdata-setup='({.+?})'\", re.DOTALL),\n            validate.none_or_all(\n                validate.get(1),\n                validate.parse_json(),\n                {\n                    \"idAsset\": validate.any(int, validate.all(str, validate.transform(int))),\n                },\n                validate.get(\"idAsset\"),\n            ),\n        ))\n        if not self.id:\n            return\n\n        # check obfuscated stream URLs via self.URL_VIDEOS and ZTNR.translate() first\n        # self.URL_M3U8 appears to be valid for all streams, but doesn't provide any content in some cases\n        try:\n            urls = self.session.http.get(\n                self.URL_VIDEOS.format(id=self.id),\n                schema=validate.Schema(\n                    validate.transform(ZTNR.translate),\n                    validate.transform(list),\n                    [(str, validate.url())],\n                    validate.length(1),\n                ),\n            )\n        except PluginError:\n            # catch HTTP errors and validation errors, and fall back to generic HLS URL template\n            url = self.URL_M3U8.format(id=self.id)\n        else:\n            url = next((url for _, url in urls if urlparse(url).path.endswith(\".m3u8\")), None)\n            if not url:\n                url = next((url for _, url in urls if urlparse(url).path.endswith(\".mp4\")), None)\n                if url:\n                    yield \"vod\", HTTPStream(self.session, url)\n                return\n\n        streams = HLSStream.parse_variant_playlist(self.session, url).items()\n\n        if self.options.get(\"mux-subtitles\"):\n            subs = self.session.http.get(\n                self.URL_SUBTITLES.format(id=self.id),\n                schema=validate.Schema(\n                    validate.parse_json(),\n                    {\n                        \"page\": {\n                            \"items\": [{\n                                \"lang\": str,\n                                \"src\": validate.url(),\n                            }],\n                        },\n                    },\n                    validate.get((\"page\", \"items\")),\n                ),\n            )\n            if subs:\n                subtitles = {\n                    s[\"lang\"]: HTTPStream(self.session, update_scheme(\"https://\", s[\"src\"], force=True))\n                    for s in subs\n                }\n                for quality, stream in streams:\n                    yield quality, MuxedStream(self.session, stream, subtitles=subtitles)\n                return\n\n        yield from streams\n\n\n__plugin__ = Rtve\n", "path": "src/streamlink/plugins/rtve.py"}]}