We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
dash plugin: MPDParsingError: root tag did not match the expected tag: MPD
### 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)
- [ ] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

Latest build from the master branch

### Description

The following steps worked until yesterday:

1. Connect to a channel livestream of Teleboy (teleboy.ch) or Zattoo (zattoo.com) with my web browser.
2. Get the .mpd URL link of the livestream.
3. Launch the command "streamlink "mpd-url-link best".
4. The livestream opens up in VLC.
 
Since today, the command fails.

Here is an example of the .mpd file content which streamlink fails to play: https://pastebin.com/jXD1NY1F


### Debug log

```text
streamlink "https://zh2-0-dash-live.zahs.tv/HD_orf1/m.mpd?z32=MF2WI2LPL5RW6ZDFMNZT2YLBMMTGG43JMQ6TCNSBII4DSMJSIZBTEMJVIQ2EKLJUGEZDORJSGVCDKOBTIE4ECOKEEZWWC6DSMF2GKPJVGAYDAJTNNFXHEYLUMU6TEMJQEZYHEZLGMVZHEZLEL5WGC3THOVQWOZJ5MZZCM43JM46TSX3FG4YDGMJXG5RGIMRVGNSTCMLBGI4GEMZRG5SDQN3DGRSGGYJXGETHK43FOJPWSZB5ORSWYZLCN54TUORYGM4DSMRXEZ3D2MA" best --loglevel debug
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.9.6
[cli][debug] Streamlink: 2.4.0
[cli][debug] Requests(2.26.0), Socks(1.7.1), Websocket(1.2.1)
[cli][debug] Arguments:
[cli][debug]  url=https://zh2-0-dash-live.zahs.tv/HD_orf1/m.mpd?z32=MF2WI2LPL5RW6ZDFMNZT2YLBMMTGG43JMQ6TCNSBII4DSMJSIZBTEMJVIQ2EKLJUGEZDORJSGVCDKOBTIE4ECOKEEZWWC6DSMF2GKPJVGAYDAJTNNFXHEYLUMU6TEMJQEZYHEZLGMVZHEZLEL5WGC3THOVQWOZJ5MZZCM43JM46TSX3FG4YDGMJXG5RGIMRVGNSTCMLBGI4GEMZRG5SDQN3DGRSGGYJXGETHK43FOJPWSZB5ORSWYZLCN54TUORYGM4DSMRXEZ3D2MA
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --rtmp-rtmpdump=rtmpdump.exe
[cli][debug]  --rtmpdump=C:\Program Files (x86)\Streamlink\rtmpdump\rtmpdump.exe
[cli][debug]  --ffmpeg-ffmpeg=C:\Program Files (x86)\Streamlink\ffmpeg\ffmpeg.exe
[cli][info] Found matching plugin dash for URL https://zh2-0-dash-live.zahs.tv/HD_orf1/m.mpd?z32=MF2WI2LPL5RW6ZDFMNZT2YLBMMTGG43JMQ6TCNSBII4DSMJSIZBTEMJVIQ2EKLJUGEZDORJSGVCDKOBTIE4ECOKEEZWWC6DSMF2GKPJVGAYDAJTNNFXHEYLUMU6TEMJQEZYHEZLGMVZHEZLEL5WGC3THOVQWOZJ5MZZCM43JM46TSX3FG4YDGMJXG5RGIMRVGNSTCMLBGI4GEMZRG5SDQN3DGRSGGYJXGETHK43FOJPWSZB5ORSWYZLCN54TUORYGM4DSMRXEZ3D2MA
[plugins.dash][debug] Parsing MPD URL: https://zh2-0-dash-live.zahs.tv/HD_orf1/m.mpd?z32=MF2WI2LPL5RW6ZDFMNZT2YLBMMTGG43JMQ6TCNSBII4DSMJSIZBTEMJVIQ2EKLJUGEZDORJSGVCDKOBTIE4ECOKEEZWWC6DSMF2GKPJVGAYDAJTNNFXHEYLUMU6TEMJQEZYHEZLGMVZHEZLEL5WGC3THOVQWOZJ5MZZCM43JM46TSX3FG4YDGMJXG5RGIMRVGNSTCMLBGI4GEMZRG5SDQN3DGRSGGYJXGETHK43FOJPWSZB5ORSWYZLCN54TUORYGM4DSMRXEZ3D2MA
Traceback (most recent call last):
  File "runpy.py", line 197, in _run_module_as_main
  File "runpy.py", line 87, in _run_code
  File "C:\Program Files (x86)\Streamlink\bin\streamlink.exe\__main__.py", line 18, in <module>
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink_cli\main.py", line 1082, in main
    handle_url()
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink_cli\main.py", line 576, in handle_url
    streams = fetch_streams(plugin)
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink_cli\main.py", line 470, in fetch_streams
    return plugin.streams(stream_types=args.stream_types,
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink\plugin\plugin.py", line 335, in streams
    ostreams = self._get_streams()
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink\plugins\dash.py", line 34, in _get_streams
    return DASHStream.parse_manifest(self.session, url)
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink\stream\dash.py", line 185, in parse_manifest
    mpd = MPD(session.http.xml(res, ignore_ns=True), base_url=urlunparse(urlp), url=url)
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink\stream\dash_manifest.py", line 224, in __init__
    super().__init__(node, root=self, *args, **kwargs)
  File "C:\Program Files (x86)\Streamlink\pkgs\streamlink\stream\dash_manifest.py", line 146, in __init__
    raise MPDParsingError("root tag did not match the expected tag: {}".format(self.__tag__))
streamlink.stream.dash_manifest.MPDParsingError: root tag did not match the expected tag: MPD
```


--- 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/utils/parse.py`
Content:
```
1 import json
2 import re
3 from urllib.parse import parse_qsl
4 
5 from lxml.etree import HTML, XML
6 
7 from streamlink.plugin import PluginError
8 
9 
10 def _parse(parser, data, name, exception, schema, *args, **kwargs):
11     try:
12         parsed = parser(data, *args, **kwargs)
13     except Exception as err:
14         snippet = repr(data)
15         if len(snippet) > 35:
16             snippet = f"{snippet[:35]} ..."
17 
18         raise exception(f"Unable to parse {name}: {err} ({snippet})")
19 
20     if schema:
21         parsed = schema.validate(parsed, name=name, exception=exception)
22 
23     return parsed
24 
25 
26 def parse_json(
27     data,
28     name="JSON",
29     exception=PluginError,
30     schema=None,
31     *args, **kwargs
32 ):
33     """Wrapper around json.loads.
34 
35     Provides these extra features:
36      - Wraps errors in custom exception with a snippet of the data in the message
37     """
38     return _parse(json.loads, data, name, exception, schema, *args, **kwargs)
39 
40 
41 def parse_html(
42     data,
43     name="HTML",
44     exception=PluginError,
45     schema=None,
46     *args, **kwargs
47 ):
48     """Wrapper around lxml.etree.HTML with some extras.
49 
50     Provides these extra features:
51      - Handles incorrectly encoded HTML
52      - Wraps errors in custom exception with a snippet of the data in the message
53     """
54     if isinstance(data, str):
55         data = bytes(data, "utf8")
56 
57     return _parse(HTML, data, name, exception, schema, *args, **kwargs)
58 
59 
60 def parse_xml(
61     data,
62     ignore_ns=False,
63     invalid_char_entities=False,
64     name="XML",
65     exception=PluginError,
66     schema=None,
67     *args, **kwargs
68 ):
69     """Wrapper around lxml.etree.XML with some extras.
70 
71     Provides these extra features:
72      - Handles incorrectly encoded XML
73      - Allows stripping namespace information
74      - Wraps errors in custom exception with a snippet of the data in the message
75     """
76     if isinstance(data, str):
77         data = bytes(data, "utf8")
78     if ignore_ns:
79         data = re.sub(br"[\t ]xmlns=\"(.+?)\"", b"", data)
80     if invalid_char_entities:
81         data = re.sub(br"&(?!(?:#(?:[0-9]+|[Xx][0-9A-Fa-f]+)|[A-Za-z0-9]+);)", b"&amp;", data)
82 
83     return _parse(XML, data, name, exception, schema, *args, **kwargs)
84 
85 
86 def parse_qsd(
87     data,
88     name="query string",
89     exception=PluginError,
90     schema=None,
91     *args, **kwargs
92 ):
93     """Parses a query string into a dict.
94 
95     Provides these extra features:
96      - Unlike parse_qs and parse_qsl, duplicate keys are not preserved in favor of a simpler return value
97      - Wraps errors in custom exception with a snippet of the data in the message
98     """
99     return _parse(lambda d: dict(parse_qsl(d, *args, **kwargs)), data, name, exception, schema)
100
```
--- 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/utils/parse.py b/src/streamlink/utils/parse.py
--- a/src/streamlink/utils/parse.py
+++ b/src/streamlink/utils/parse.py
@@ -76,7 +76,7 @@
     if isinstance(data, str):
         data = bytes(data, "utf8")
     if ignore_ns:
-        data = re.sub(br"[\t ]xmlns=\"(.+?)\"", b"", data)
+        data = re.sub(br"\s+xmlns=\"(.+?)\"", b"", data)
     if invalid_char_entities:
         data = re.sub(br"&(?!(?:#(?:[0-9]+|[Xx][0-9A-Fa-f]+)|[A-Za-z0-9]+);)", b"&amp;", data)

{"golden_diff": "diff --git a/src/streamlink/utils/parse.py b/src/streamlink/utils/parse.py\n--- a/src/streamlink/utils/parse.py\n+++ b/src/streamlink/utils/parse.py\n@@ -76,7 +76,7 @@\n     if isinstance(data, str):\n         data = bytes(data, \"utf8\")\n     if ignore_ns:\n-        data = re.sub(br\"[\\t ]xmlns=\\\"(.+?)\\\"\", b\"\", data)\n+        data = re.sub(br\"\\s+xmlns=\\\"(.+?)\\\"\", b\"\", data)\n     if invalid_char_entities:\n         data = re.sub(br\"&(?!(?:#(?:[0-9]+|[Xx][0-9A-Fa-f]+)|[A-Za-z0-9]+);)\", b\"&amp;\", data)\n", "issue": "dash plugin: MPDParsingError: root tag did not match the expected tag: MPD\n### Checklist\r\n\r\n- [X] This is a plugin issue and not a different kind of issue\r\n- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)\r\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)\r\n- [ ] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)\r\n\r\n### Streamlink version\r\n\r\nLatest build from the master branch\r\n\r\n### Description\r\n\r\nThe following steps worked until yesterday:\r\n\r\n1. Connect to a channel livestream of Teleboy (teleboy.ch) or Zattoo (zattoo.com) with my web browser.\r\n2. Get the .mpd URL link of the livestream.\r\n3. Launch the command \"streamlink \"mpd-url-link best\".\r\n4. The livestream opens up in VLC.\r\n \r\nSince today, the command fails.\r\n\r\nHere is an example of the .mpd file content which streamlink fails to play: https://pastebin.com/jXD1NY1F\r\n\r\n\r\n### Debug log\r\n\r\n```text\r\nstreamlink \"https://zh2-0-dash-live.zahs.tv/HD_orf1/m.mpd?z32=MF2WI2LPL5RW6ZDFMNZT2YLBMMTGG43JMQ6TCNSBII4DSMJSIZBTEMJVIQ2EKLJUGEZDORJSGVCDKOBTIE4ECOKEEZWWC6DSMF2GKPJVGAYDAJTNNFXHEYLUMU6TEMJQEZYHEZLGMVZHEZLEL5WGC3THOVQWOZJ5MZZCM43JM46TSX3FG4YDGMJXG5RGIMRVGNSTCMLBGI4GEMZRG5SDQN3DGRSGGYJXGETHK43FOJPWSZB5ORSWYZLCN54TUORYGM4DSMRXEZ3D2MA\" best --loglevel debug\r\n[cli][debug] OS:         Windows 10\r\n[cli][debug] Python:     3.9.6\r\n[cli][debug] Streamlink: 2.4.0\r\n[cli][debug] Requests(2.26.0), Socks(1.7.1), Websocket(1.2.1)\r\n[cli][debug] Arguments:\r\n[cli][debug]  url=https://zh2-0-dash-live.zahs.tv/HD_orf1/m.mpd?z32=MF2WI2LPL5RW6ZDFMNZT2YLBMMTGG43JMQ6TCNSBII4DSMJSIZBTEMJVIQ2EKLJUGEZDORJSGVCDKOBTIE4ECOKEEZWWC6DSMF2GKPJVGAYDAJTNNFXHEYLUMU6TEMJQEZYHEZLGMVZHEZLEL5WGC3THOVQWOZJ5MZZCM43JM46TSX3FG4YDGMJXG5RGIMRVGNSTCMLBGI4GEMZRG5SDQN3DGRSGGYJXGETHK43FOJPWSZB5ORSWYZLCN54TUORYGM4DSMRXEZ3D2MA\r\n[cli][debug]  stream=['best']\r\n[cli][debug]  --loglevel=debug\r\n[cli][debug]  --rtmp-rtmpdump=rtmpdump.exe\r\n[cli][debug]  --rtmpdump=C:\\Program Files (x86)\\Streamlink\\rtmpdump\\rtmpdump.exe\r\n[cli][debug]  --ffmpeg-ffmpeg=C:\\Program Files (x86)\\Streamlink\\ffmpeg\\ffmpeg.exe\r\n[cli][info] Found matching plugin dash for URL https://zh2-0-dash-live.zahs.tv/HD_orf1/m.mpd?z32=MF2WI2LPL5RW6ZDFMNZT2YLBMMTGG43JMQ6TCNSBII4DSMJSIZBTEMJVIQ2EKLJUGEZDORJSGVCDKOBTIE4ECOKEEZWWC6DSMF2GKPJVGAYDAJTNNFXHEYLUMU6TEMJQEZYHEZLGMVZHEZLEL5WGC3THOVQWOZJ5MZZCM43JM46TSX3FG4YDGMJXG5RGIMRVGNSTCMLBGI4GEMZRG5SDQN3DGRSGGYJXGETHK43FOJPWSZB5ORSWYZLCN54TUORYGM4DSMRXEZ3D2MA\r\n[plugins.dash][debug] Parsing MPD URL: https://zh2-0-dash-live.zahs.tv/HD_orf1/m.mpd?z32=MF2WI2LPL5RW6ZDFMNZT2YLBMMTGG43JMQ6TCNSBII4DSMJSIZBTEMJVIQ2EKLJUGEZDORJSGVCDKOBTIE4ECOKEEZWWC6DSMF2GKPJVGAYDAJTNNFXHEYLUMU6TEMJQEZYHEZLGMVZHEZLEL5WGC3THOVQWOZJ5MZZCM43JM46TSX3FG4YDGMJXG5RGIMRVGNSTCMLBGI4GEMZRG5SDQN3DGRSGGYJXGETHK43FOJPWSZB5ORSWYZLCN54TUORYGM4DSMRXEZ3D2MA\r\nTraceback (most recent call last):\r\n  File \"runpy.py\", line 197, in _run_module_as_main\r\n  File \"runpy.py\", line 87, in _run_code\r\n  File \"C:\\Program Files (x86)\\Streamlink\\bin\\streamlink.exe\\__main__.py\", line 18, in <module>\r\n  File \"C:\\Program Files (x86)\\Streamlink\\pkgs\\streamlink_cli\\main.py\", line 1082, in main\r\n    handle_url()\r\n  File \"C:\\Program Files (x86)\\Streamlink\\pkgs\\streamlink_cli\\main.py\", line 576, in handle_url\r\n    streams = fetch_streams(plugin)\r\n  File \"C:\\Program Files (x86)\\Streamlink\\pkgs\\streamlink_cli\\main.py\", line 470, in fetch_streams\r\n    return plugin.streams(stream_types=args.stream_types,\r\n  File \"C:\\Program Files (x86)\\Streamlink\\pkgs\\streamlink\\plugin\\plugin.py\", line 335, in streams\r\n    ostreams = self._get_streams()\r\n  File \"C:\\Program Files (x86)\\Streamlink\\pkgs\\streamlink\\plugins\\dash.py\", line 34, in _get_streams\r\n    return DASHStream.parse_manifest(self.session, url)\r\n  File \"C:\\Program Files (x86)\\Streamlink\\pkgs\\streamlink\\stream\\dash.py\", line 185, in parse_manifest\r\n    mpd = MPD(session.http.xml(res, ignore_ns=True), base_url=urlunparse(urlp), url=url)\r\n  File \"C:\\Program Files (x86)\\Streamlink\\pkgs\\streamlink\\stream\\dash_manifest.py\", line 224, in __init__\r\n    super().__init__(node, root=self, *args, **kwargs)\r\n  File \"C:\\Program Files (x86)\\Streamlink\\pkgs\\streamlink\\stream\\dash_manifest.py\", line 146, in __init__\r\n    raise MPDParsingError(\"root tag did not match the expected tag: {}\".format(self.__tag__))\r\nstreamlink.stream.dash_manifest.MPDParsingError: root tag did not match the expected tag: MPD\r\n```\r\n\n", "before_files": [{"content": "import json\nimport re\nfrom urllib.parse import parse_qsl\n\nfrom lxml.etree import HTML, XML\n\nfrom streamlink.plugin import PluginError\n\n\ndef _parse(parser, data, name, exception, schema, *args, **kwargs):\n    try:\n        parsed = parser(data, *args, **kwargs)\n    except Exception as err:\n        snippet = repr(data)\n        if len(snippet) > 35:\n            snippet = f\"{snippet[:35]} ...\"\n\n        raise exception(f\"Unable to parse {name}: {err} ({snippet})\")\n\n    if schema:\n        parsed = schema.validate(parsed, name=name, exception=exception)\n\n    return parsed\n\n\ndef parse_json(\n    data,\n    name=\"JSON\",\n    exception=PluginError,\n    schema=None,\n    *args, **kwargs\n):\n    \"\"\"Wrapper around json.loads.\n\n    Provides these extra features:\n     - Wraps errors in custom exception with a snippet of the data in the message\n    \"\"\"\n    return _parse(json.loads, data, name, exception, schema, *args, **kwargs)\n\n\ndef parse_html(\n    data,\n    name=\"HTML\",\n    exception=PluginError,\n    schema=None,\n    *args, **kwargs\n):\n    \"\"\"Wrapper around lxml.etree.HTML with some extras.\n\n    Provides these extra features:\n     - Handles incorrectly encoded HTML\n     - Wraps errors in custom exception with a snippet of the data in the message\n    \"\"\"\n    if isinstance(data, str):\n        data = bytes(data, \"utf8\")\n\n    return _parse(HTML, data, name, exception, schema, *args, **kwargs)\n\n\ndef parse_xml(\n    data,\n    ignore_ns=False,\n    invalid_char_entities=False,\n    name=\"XML\",\n    exception=PluginError,\n    schema=None,\n    *args, **kwargs\n):\n    \"\"\"Wrapper around lxml.etree.XML with some extras.\n\n    Provides these extra features:\n     - Handles incorrectly encoded XML\n     - Allows stripping namespace information\n     - Wraps errors in custom exception with a snippet of the data in the message\n    \"\"\"\n    if isinstance(data, str):\n        data = bytes(data, \"utf8\")\n    if ignore_ns:\n        data = re.sub(br\"[\\t ]xmlns=\\\"(.+?)\\\"\", b\"\", data)\n    if invalid_char_entities:\n        data = re.sub(br\"&(?!(?:#(?:[0-9]+|[Xx][0-9A-Fa-f]+)|[A-Za-z0-9]+);)\", b\"&amp;\", data)\n\n    return _parse(XML, data, name, exception, schema, *args, **kwargs)\n\n\ndef parse_qsd(\n    data,\n    name=\"query string\",\n    exception=PluginError,\n    schema=None,\n    *args, **kwargs\n):\n    \"\"\"Parses a query string into a dict.\n\n    Provides these extra features:\n     - Unlike parse_qs and parse_qsl, duplicate keys are not preserved in favor of a simpler return value\n     - Wraps errors in custom exception with a snippet of the data in the message\n    \"\"\"\n    return _parse(lambda d: dict(parse_qsl(d, *args, **kwargs)), data, name, exception, schema)\n", "path": "src/streamlink/utils/parse.py"}], "after_files": [{"content": "import json\nimport re\nfrom urllib.parse import parse_qsl\n\nfrom lxml.etree import HTML, XML\n\nfrom streamlink.plugin import PluginError\n\n\ndef _parse(parser, data, name, exception, schema, *args, **kwargs):\n    try:\n        parsed = parser(data, *args, **kwargs)\n    except Exception as err:\n        snippet = repr(data)\n        if len(snippet) > 35:\n            snippet = f\"{snippet[:35]} ...\"\n\n        raise exception(f\"Unable to parse {name}: {err} ({snippet})\")\n\n    if schema:\n        parsed = schema.validate(parsed, name=name, exception=exception)\n\n    return parsed\n\n\ndef parse_json(\n    data,\n    name=\"JSON\",\n    exception=PluginError,\n    schema=None,\n    *args, **kwargs\n):\n    \"\"\"Wrapper around json.loads.\n\n    Provides these extra features:\n     - Wraps errors in custom exception with a snippet of the data in the message\n    \"\"\"\n    return _parse(json.loads, data, name, exception, schema, *args, **kwargs)\n\n\ndef parse_html(\n    data,\n    name=\"HTML\",\n    exception=PluginError,\n    schema=None,\n    *args, **kwargs\n):\n    \"\"\"Wrapper around lxml.etree.HTML with some extras.\n\n    Provides these extra features:\n     - Handles incorrectly encoded HTML\n     - Wraps errors in custom exception with a snippet of the data in the message\n    \"\"\"\n    if isinstance(data, str):\n        data = bytes(data, \"utf8\")\n\n    return _parse(HTML, data, name, exception, schema, *args, **kwargs)\n\n\ndef parse_xml(\n    data,\n    ignore_ns=False,\n    invalid_char_entities=False,\n    name=\"XML\",\n    exception=PluginError,\n    schema=None,\n    *args, **kwargs\n):\n    \"\"\"Wrapper around lxml.etree.XML with some extras.\n\n    Provides these extra features:\n     - Handles incorrectly encoded XML\n     - Allows stripping namespace information\n     - Wraps errors in custom exception with a snippet of the data in the message\n    \"\"\"\n    if isinstance(data, str):\n        data = bytes(data, \"utf8\")\n    if ignore_ns:\n        data = re.sub(br\"\\s+xmlns=\\\"(.+?)\\\"\", b\"\", data)\n    if invalid_char_entities:\n        data = re.sub(br\"&(?!(?:#(?:[0-9]+|[Xx][0-9A-Fa-f]+)|[A-Za-z0-9]+);)\", b\"&amp;\", data)\n\n    return _parse(XML, data, name, exception, schema, *args, **kwargs)\n\n\ndef parse_qsd(\n    data,\n    name=\"query string\",\n    exception=PluginError,\n    schema=None,\n    *args, **kwargs\n):\n    \"\"\"Parses a query string into a dict.\n\n    Provides these extra features:\n     - Unlike parse_qs and parse_qsl, duplicate keys are not preserved in favor of a simpler return value\n     - Wraps errors in custom exception with a snippet of the data in the message\n    \"\"\"\n    return _parse(lambda d: dict(parse_qsl(d, *args, **kwargs)), data, name, exception, schema)\n", "path": "src/streamlink/utils/parse.py"}]}