We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
plugins.ard_mediathek: rewrite plugin
Resolves #4191 

One issue I couldn't fix is the text encoding of the metadata which gets messed up by `validate.parse_html()`. See the VOD title down below...
https://github.com/streamlink/streamlink/blob/175d4748561c7154bb80c5a47dae22039e45d4ce/src/streamlink/utils/parse.py#L54-L55

Some VODs also have a second title, eg. if it's a TV show, but I couldn't be bothered to implement this. Not important.

----

Das Erste - Live:
```
$ streamlink -l debug --title '{author} - {title}' 'https://www.ardmediathek.de/daserste/live/Y3JpZDovL2Rhc2Vyc3RlLmRlL0xpdmVzdHJlYW0tRGFzRXJzdGU/' best
[cli.output][debug] Opening subprocess: mpv "--force-media-title=Das Erste - Das Erste" -
```

WDR - Live:
```
$ streamlink -l debug --title '{author} - {title}' 'https://www.ardmediathek.de/live/Y3JpZDovL3dkci5kZS9CZWl0cmFnLTNkYTY2NGRlLTE4YzItNDY1MC1hNGZmLTRmNjQxNDcyMDcyYg/' best
[cli.output][debug] Opening subprocess: mpv "--force-media-title=WDR - WDR Fernsehen im Livestream" -
```

VOD
```
$ streamlink -l debug --title '{author} - {title}' 'https://www.ardmediathek.de/video/dokus-im-ersten/wirecard-die-milliarden-luege/das-erste/Y3JpZDovL2Rhc2Vyc3RlLmRlL3JlcG9ydGFnZSBfIGRva3VtZW50YXRpb24gaW0gZXJzdGVuL2NlMjQ0OWM4LTQ4YTUtNGIyNC1iMTdlLWNhOTNjMDQ5OTc4Zg/' best
[cli.output][debug] Opening subprocess: mpv "--force-media-title=Das Erste - Wirecard - Die Milliarden-LÃ¼ge" -
```

--- 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/ard_mediathek.py`
Content:
```
1 import logging
2 import re
3 
4 from streamlink.plugin import Plugin, pluginmatcher
5 from streamlink.plugin.api import validate
6 from streamlink.stream.hls import HLSStream
7 
8 
9 log = logging.getLogger(__name__)
10 
11 
12 @pluginmatcher(re.compile(
13     r"https?://(?:(\w+\.)?ardmediathek\.de/|mediathek\.daserste\.de/)"
14 ))
15 class ARDMediathek(Plugin):
16     def _get_streams(self):
17         data_json = self.session.http.get(self.url, schema=validate.Schema(
18             validate.parse_html(),
19             validate.xml_findtext(".//script[@id='fetchedContextValue'][@type='application/json']"),
20             validate.any(None, validate.all(
21                 validate.parse_json(),
22                 {str: dict},
23                 validate.transform(lambda obj: list(obj.items())),
24                 validate.filter(lambda item: item[0].startswith("https://api.ardmediathek.de/page-gateway/pages/")),
25                 validate.any(validate.get((0, 1)), [])
26             ))
27         ))
28         if not data_json:
29             return
30 
31         schema_data = validate.Schema({
32             "id": str,
33             "widgets": validate.all(
34                 [dict],
35                 validate.filter(lambda item: item.get("mediaCollection")),
36                 validate.get(0),
37                 {
38                     "geoblocked": bool,
39                     "publicationService": {
40                         "name": str,
41                     },
42                     "title": str,
43                     "mediaCollection": {
44                         "embedded": {
45                             "_mediaArray": [{
46                                 "_mediaStreamArray": [{
47                                     "_quality": validate.any(str, int),
48                                     "_stream": validate.url()
49                                 }]
50                             }]
51                         }
52                     }
53                 }
54             )
55         })
56         data = schema_data.validate(data_json)
57 
58         log.debug(f"Found media id: {data['id']}")
59         data_media = data["widgets"]
60 
61         if data_media["geoblocked"]:
62             log.info("The content is not available in your region")
63             return
64 
65         self.author = data_media["publicationService"]["name"]
66         self.title = data_media["title"]
67 
68         for media in data_media["mediaCollection"]["embedded"]["_mediaArray"]:
69             for stream in media["_mediaStreamArray"]:
70                 if stream["_quality"] != "auto" or ".m3u8" not in stream["_stream"]:
71                     continue
72                 return HLSStream.parse_variant_playlist(self.session, stream["_stream"])
73 
74 
75 __plugin__ = ARDMediathek
76
```
--- 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/ard_mediathek.py b/src/streamlink/plugins/ard_mediathek.py
--- a/src/streamlink/plugins/ard_mediathek.py
+++ b/src/streamlink/plugins/ard_mediathek.py
@@ -4,6 +4,7 @@
 from streamlink.plugin import Plugin, pluginmatcher
 from streamlink.plugin.api import validate
 from streamlink.stream.hls import HLSStream
+from streamlink.stream.http import HTTPStream
 
 
 log = logging.getLogger(__name__)
@@ -13,6 +14,14 @@
     r"https?://(?:(\w+\.)?ardmediathek\.de/|mediathek\.daserste\.de/)"
 ))
 class ARDMediathek(Plugin):
+    _QUALITY_MAP = {
+        4: "1080p",
+        3: "720p",
+        2: "540p",
+        1: "360p",
+        0: "270p"
+    }
+
     def _get_streams(self):
         data_json = self.session.http.get(self.url, schema=validate.Schema(
             validate.parse_html(),
@@ -34,42 +43,64 @@
                 [dict],
                 validate.filter(lambda item: item.get("mediaCollection")),
                 validate.get(0),
-                {
-                    "geoblocked": bool,
-                    "publicationService": {
-                        "name": str,
+                validate.any(None, validate.all(
+                    {
+                        "geoblocked": bool,
+                        "publicationService": {
+                            "name": str,
+                        },
+                        "show": validate.any(None, validate.all(
+                            {"title": str},
+                            validate.get("title")
+                        )),
+                        "title": str,
+                        "mediaCollection": {
+                            "embedded": {
+                                "_mediaArray": [validate.all(
+                                    {
+                                        "_mediaStreamArray": [validate.all(
+                                            {
+                                                "_quality": validate.any(str, int),
+                                                "_stream": validate.url(),
+                                            },
+                                            validate.union_get("_quality", "_stream")
+                                        )]
+                                    },
+                                    validate.get("_mediaStreamArray"),
+                                    validate.transform(dict)
+                                )]
+                            }
+                        },
                     },
-                    "title": str,
-                    "mediaCollection": {
-                        "embedded": {
-                            "_mediaArray": [{
-                                "_mediaStreamArray": [{
-                                    "_quality": validate.any(str, int),
-                                    "_stream": validate.url()
-                                }]
-                            }]
-                        }
-                    }
-                }
+                    validate.union_get(
+                        "geoblocked",
+                        ("mediaCollection", "embedded", "_mediaArray", 0),
+                        ("publicationService", "name"),
+                        "title",
+                        "show",
+                    )
+                ))
             )
         })
         data = schema_data.validate(data_json)
 
         log.debug(f"Found media id: {data['id']}")
-        data_media = data["widgets"]
+        if not data["widgets"]:
+            log.info("The content is unavailable")
+            return
 
-        if data_media["geoblocked"]:
+        geoblocked, media, self.author, self.title, show = data["widgets"]
+        if geoblocked:
             log.info("The content is not available in your region")
             return
+        if show:
+            self.title = f"{show}: {self.title}"
 
-        self.author = data_media["publicationService"]["name"]
-        self.title = data_media["title"]
-
-        for media in data_media["mediaCollection"]["embedded"]["_mediaArray"]:
-            for stream in media["_mediaStreamArray"]:
-                if stream["_quality"] != "auto" or ".m3u8" not in stream["_stream"]:
-                    continue
-                return HLSStream.parse_variant_playlist(self.session, stream["_stream"])
+        if media.get("auto"):
+            yield from HLSStream.parse_variant_playlist(self.session, media.get("auto")).items()
+        else:
+            for quality, stream in media.items():
+                yield self._QUALITY_MAP.get(quality, quality), HTTPStream(self.session, stream)
 
 
 __plugin__ = ARDMediathek

{"golden_diff": "diff --git a/src/streamlink/plugins/ard_mediathek.py b/src/streamlink/plugins/ard_mediathek.py\n--- a/src/streamlink/plugins/ard_mediathek.py\n+++ b/src/streamlink/plugins/ard_mediathek.py\n@@ -4,6 +4,7 @@\n from streamlink.plugin import Plugin, pluginmatcher\n from streamlink.plugin.api import validate\n from streamlink.stream.hls import HLSStream\n+from streamlink.stream.http import HTTPStream\n \n \n log = logging.getLogger(__name__)\n@@ -13,6 +14,14 @@\n     r\"https?://(?:(\\w+\\.)?ardmediathek\\.de/|mediathek\\.daserste\\.de/)\"\n ))\n class ARDMediathek(Plugin):\n+    _QUALITY_MAP = {\n+        4: \"1080p\",\n+        3: \"720p\",\n+        2: \"540p\",\n+        1: \"360p\",\n+        0: \"270p\"\n+    }\n+\n     def _get_streams(self):\n         data_json = self.session.http.get(self.url, schema=validate.Schema(\n             validate.parse_html(),\n@@ -34,42 +43,64 @@\n                 [dict],\n                 validate.filter(lambda item: item.get(\"mediaCollection\")),\n                 validate.get(0),\n-                {\n-                    \"geoblocked\": bool,\n-                    \"publicationService\": {\n-                        \"name\": str,\n+                validate.any(None, validate.all(\n+                    {\n+                        \"geoblocked\": bool,\n+                        \"publicationService\": {\n+                            \"name\": str,\n+                        },\n+                        \"show\": validate.any(None, validate.all(\n+                            {\"title\": str},\n+                            validate.get(\"title\")\n+                        )),\n+                        \"title\": str,\n+                        \"mediaCollection\": {\n+                            \"embedded\": {\n+                                \"_mediaArray\": [validate.all(\n+                                    {\n+                                        \"_mediaStreamArray\": [validate.all(\n+                                            {\n+                                                \"_quality\": validate.any(str, int),\n+                                                \"_stream\": validate.url(),\n+                                            },\n+                                            validate.union_get(\"_quality\", \"_stream\")\n+                                        )]\n+                                    },\n+                                    validate.get(\"_mediaStreamArray\"),\n+                                    validate.transform(dict)\n+                                )]\n+                            }\n+                        },\n                     },\n-                    \"title\": str,\n-                    \"mediaCollection\": {\n-                        \"embedded\": {\n-                            \"_mediaArray\": [{\n-                                \"_mediaStreamArray\": [{\n-                                    \"_quality\": validate.any(str, int),\n-                                    \"_stream\": validate.url()\n-                                }]\n-                            }]\n-                        }\n-                    }\n-                }\n+                    validate.union_get(\n+                        \"geoblocked\",\n+                        (\"mediaCollection\", \"embedded\", \"_mediaArray\", 0),\n+                        (\"publicationService\", \"name\"),\n+                        \"title\",\n+                        \"show\",\n+                    )\n+                ))\n             )\n         })\n         data = schema_data.validate(data_json)\n \n         log.debug(f\"Found media id: {data['id']}\")\n-        data_media = data[\"widgets\"]\n+        if not data[\"widgets\"]:\n+            log.info(\"The content is unavailable\")\n+            return\n \n-        if data_media[\"geoblocked\"]:\n+        geoblocked, media, self.author, self.title, show = data[\"widgets\"]\n+        if geoblocked:\n             log.info(\"The content is not available in your region\")\n             return\n+        if show:\n+            self.title = f\"{show}: {self.title}\"\n \n-        self.author = data_media[\"publicationService\"][\"name\"]\n-        self.title = data_media[\"title\"]\n-\n-        for media in data_media[\"mediaCollection\"][\"embedded\"][\"_mediaArray\"]:\n-            for stream in media[\"_mediaStreamArray\"]:\n-                if stream[\"_quality\"] != \"auto\" or \".m3u8\" not in stream[\"_stream\"]:\n-                    continue\n-                return HLSStream.parse_variant_playlist(self.session, stream[\"_stream\"])\n+        if media.get(\"auto\"):\n+            yield from HLSStream.parse_variant_playlist(self.session, media.get(\"auto\")).items()\n+        else:\n+            for quality, stream in media.items():\n+                yield self._QUALITY_MAP.get(quality, quality), HTTPStream(self.session, stream)\n \n \n __plugin__ = ARDMediathek\n", "issue": "plugins.ard_mediathek: rewrite plugin\nResolves #4191 \r\n\r\nOne issue I couldn't fix is the text encoding of the metadata which gets messed up by `validate.parse_html()`. See the VOD title down below...\r\nhttps://github.com/streamlink/streamlink/blob/175d4748561c7154bb80c5a47dae22039e45d4ce/src/streamlink/utils/parse.py#L54-L55\r\n\r\nSome VODs also have a second title, eg. if it's a TV show, but I couldn't be bothered to implement this. Not important.\r\n\r\n----\r\n\r\nDas Erste - Live:\r\n```\r\n$ streamlink -l debug --title '{author} - {title}' 'https://www.ardmediathek.de/daserste/live/Y3JpZDovL2Rhc2Vyc3RlLmRlL0xpdmVzdHJlYW0tRGFzRXJzdGU/' best\r\n[cli.output][debug] Opening subprocess: mpv \"--force-media-title=Das Erste - Das Erste\" -\r\n```\r\n\r\nWDR - Live:\r\n```\r\n$ streamlink -l debug --title '{author} - {title}' 'https://www.ardmediathek.de/live/Y3JpZDovL3dkci5kZS9CZWl0cmFnLTNkYTY2NGRlLTE4YzItNDY1MC1hNGZmLTRmNjQxNDcyMDcyYg/' best\r\n[cli.output][debug] Opening subprocess: mpv \"--force-media-title=WDR - WDR Fernsehen im Livestream\" -\r\n```\r\n\r\nVOD\r\n```\r\n$ streamlink -l debug --title '{author} - {title}' 'https://www.ardmediathek.de/video/dokus-im-ersten/wirecard-die-milliarden-luege/das-erste/Y3JpZDovL2Rhc2Vyc3RlLmRlL3JlcG9ydGFnZSBfIGRva3VtZW50YXRpb24gaW0gZXJzdGVuL2NlMjQ0OWM4LTQ4YTUtNGIyNC1iMTdlLWNhOTNjMDQ5OTc4Zg/' best\r\n[cli.output][debug] Opening subprocess: mpv \"--force-media-title=Das Erste - Wirecard - Die Milliarden-L\u00c3\u00bcge\" -\r\n```\n", "before_files": [{"content": "import logging\nimport re\n\nfrom streamlink.plugin import Plugin, pluginmatcher\nfrom streamlink.plugin.api import validate\nfrom streamlink.stream.hls import HLSStream\n\n\nlog = logging.getLogger(__name__)\n\n\n@pluginmatcher(re.compile(\n    r\"https?://(?:(\\w+\\.)?ardmediathek\\.de/|mediathek\\.daserste\\.de/)\"\n))\nclass ARDMediathek(Plugin):\n    def _get_streams(self):\n        data_json = self.session.http.get(self.url, schema=validate.Schema(\n            validate.parse_html(),\n            validate.xml_findtext(\".//script[@id='fetchedContextValue'][@type='application/json']\"),\n            validate.any(None, validate.all(\n                validate.parse_json(),\n                {str: dict},\n                validate.transform(lambda obj: list(obj.items())),\n                validate.filter(lambda item: item[0].startswith(\"https://api.ardmediathek.de/page-gateway/pages/\")),\n                validate.any(validate.get((0, 1)), [])\n            ))\n        ))\n        if not data_json:\n            return\n\n        schema_data = validate.Schema({\n            \"id\": str,\n            \"widgets\": validate.all(\n                [dict],\n                validate.filter(lambda item: item.get(\"mediaCollection\")),\n                validate.get(0),\n                {\n                    \"geoblocked\": bool,\n                    \"publicationService\": {\n                        \"name\": str,\n                    },\n                    \"title\": str,\n                    \"mediaCollection\": {\n                        \"embedded\": {\n                            \"_mediaArray\": [{\n                                \"_mediaStreamArray\": [{\n                                    \"_quality\": validate.any(str, int),\n                                    \"_stream\": validate.url()\n                                }]\n                            }]\n                        }\n                    }\n                }\n            )\n        })\n        data = schema_data.validate(data_json)\n\n        log.debug(f\"Found media id: {data['id']}\")\n        data_media = data[\"widgets\"]\n\n        if data_media[\"geoblocked\"]:\n            log.info(\"The content is not available in your region\")\n            return\n\n        self.author = data_media[\"publicationService\"][\"name\"]\n        self.title = data_media[\"title\"]\n\n        for media in data_media[\"mediaCollection\"][\"embedded\"][\"_mediaArray\"]:\n            for stream in media[\"_mediaStreamArray\"]:\n                if stream[\"_quality\"] != \"auto\" or \".m3u8\" not in stream[\"_stream\"]:\n                    continue\n                return HLSStream.parse_variant_playlist(self.session, stream[\"_stream\"])\n\n\n__plugin__ = ARDMediathek\n", "path": "src/streamlink/plugins/ard_mediathek.py"}], "after_files": [{"content": "import logging\nimport re\n\nfrom streamlink.plugin import Plugin, pluginmatcher\nfrom streamlink.plugin.api import validate\nfrom streamlink.stream.hls import HLSStream\nfrom streamlink.stream.http import HTTPStream\n\n\nlog = logging.getLogger(__name__)\n\n\n@pluginmatcher(re.compile(\n    r\"https?://(?:(\\w+\\.)?ardmediathek\\.de/|mediathek\\.daserste\\.de/)\"\n))\nclass ARDMediathek(Plugin):\n    _QUALITY_MAP = {\n        4: \"1080p\",\n        3: \"720p\",\n        2: \"540p\",\n        1: \"360p\",\n        0: \"270p\"\n    }\n\n    def _get_streams(self):\n        data_json = self.session.http.get(self.url, schema=validate.Schema(\n            validate.parse_html(),\n            validate.xml_findtext(\".//script[@id='fetchedContextValue'][@type='application/json']\"),\n            validate.any(None, validate.all(\n                validate.parse_json(),\n                {str: dict},\n                validate.transform(lambda obj: list(obj.items())),\n                validate.filter(lambda item: item[0].startswith(\"https://api.ardmediathek.de/page-gateway/pages/\")),\n                validate.any(validate.get((0, 1)), [])\n            ))\n        ))\n        if not data_json:\n            return\n\n        schema_data = validate.Schema({\n            \"id\": str,\n            \"widgets\": validate.all(\n                [dict],\n                validate.filter(lambda item: item.get(\"mediaCollection\")),\n                validate.get(0),\n                validate.any(None, validate.all(\n                    {\n                        \"geoblocked\": bool,\n                        \"publicationService\": {\n                            \"name\": str,\n                        },\n                        \"show\": validate.any(None, validate.all(\n                            {\"title\": str},\n                            validate.get(\"title\")\n                        )),\n                        \"title\": str,\n                        \"mediaCollection\": {\n                            \"embedded\": {\n                                \"_mediaArray\": [validate.all(\n                                    {\n                                        \"_mediaStreamArray\": [validate.all(\n                                            {\n                                                \"_quality\": validate.any(str, int),\n                                                \"_stream\": validate.url(),\n                                            },\n                                            validate.union_get(\"_quality\", \"_stream\")\n                                        )]\n                                    },\n                                    validate.get(\"_mediaStreamArray\"),\n                                    validate.transform(dict)\n                                )]\n                            }\n                        },\n                    },\n                    validate.union_get(\n                        \"geoblocked\",\n                        (\"mediaCollection\", \"embedded\", \"_mediaArray\", 0),\n                        (\"publicationService\", \"name\"),\n                        \"title\",\n                        \"show\",\n                    )\n                ))\n            )\n        })\n        data = schema_data.validate(data_json)\n\n        log.debug(f\"Found media id: {data['id']}\")\n        if not data[\"widgets\"]:\n            log.info(\"The content is unavailable\")\n            return\n\n        geoblocked, media, self.author, self.title, show = data[\"widgets\"]\n        if geoblocked:\n            log.info(\"The content is not available in your region\")\n            return\n        if show:\n            self.title = f\"{show}: {self.title}\"\n\n        if media.get(\"auto\"):\n            yield from HLSStream.parse_variant_playlist(self.session, media.get(\"auto\")).items()\n        else:\n            for quality, stream in media.items():\n                yield self._QUALITY_MAP.get(quality, quality), HTTPStream(self.session, stream)\n\n\n__plugin__ = ARDMediathek\n", "path": "src/streamlink/plugins/ard_mediathek.py"}]}