prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  plugins.tviplayer: unable to handle CNN Portugal
  ### 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
  
  - issue:
    - the new `tviplayer` plugin is unable to handle https://tviplayer.iol.pt/direto/CNN 
    - of note, the previous TVI 24 became CNN Portugal after #4199.
  
  - to reproduce:
    ```sh
    streamlink https://tviplayer.iol.pt/direto/CNN
    ```
    ```sh
    [cli][info] Found matching plugin tviplayer for URL https://tviplayer.iol.pt/direto/CNN
    error: Unable to validate response text: Unable to parse HTML: Unicode strings with encoding declaration are not supported. Please use bytes input or XML fragments without declaration. ('<?xml version=\'1.0\' encoding=\'U ...)
    ```
  
  
  
  
  ### Debug log
  
  ```text
  streamlink --loglevel debug https://tviplayer.iol.pt/direto/CNN
  [cli][debug] OS:         Linux-5.10.0-9-amd64-x86_64-with-glibc2.31
  [cli][debug] Python:     3.9.2
  [cli][debug] Streamlink: 3.0.2
  [cli][debug] Requests(2.26.0), Socks(1.7.1), Websocket(1.2.1)
  [cli][debug] Arguments:
  [cli][debug]  url=https://tviplayer.iol.pt/direto/CNN
  [cli][debug]  --loglevel=debug
  [cli][info] Found matching plugin tviplayer for URL https://tviplayer.iol.pt/direto/CNN
  error: Unable to validate response text: Unable to parse HTML: Unicode strings with encoding declaration are not supported. Please use bytes input or XML fragments without declaration. ('<?xml version=\'1.0\' encoding=\'U ...)
  ```
  
  
  --- 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      - Wraps errors in custom exception with a snippet of the data in the message
  52     """
  53     return _parse(HTML, data, name, exception, schema, *args, **kwargs)
  54 
  55 
  56 def parse_xml(
  57     data,
  58     ignore_ns=False,
  59     invalid_char_entities=False,
  60     name="XML",
  61     exception=PluginError,
  62     schema=None,
  63     *args, **kwargs
  64 ):
  65     """Wrapper around lxml.etree.XML with some extras.
  66 
  67     Provides these extra features:
  68      - Handles incorrectly encoded XML
  69      - Allows stripping namespace information
  70      - Wraps errors in custom exception with a snippet of the data in the message
  71     """
  72     if isinstance(data, str):
  73         data = bytes(data, "utf8")
  74     if ignore_ns:
  75         data = re.sub(br"\s+xmlns=\"(.+?)\"", b"", data)
  76     if invalid_char_entities:
  77         data = re.sub(br"&(?!(?:#(?:[0-9]+|[Xx][0-9A-Fa-f]+)|[A-Za-z0-9]+);)", b"&amp;", data)
  78 
  79     return _parse(XML, data, name, exception, schema, *args, **kwargs)
  80 
  81 
  82 def parse_qsd(
  83     data,
  84     name="query string",
  85     exception=PluginError,
  86     schema=None,
  87     *args, **kwargs
  88 ):
  89     """Parses a query string into a dict.
  90 
  91     Provides these extra features:
  92      - Unlike parse_qs and parse_qsl, duplicate keys are not preserved in favor of a simpler return value
  93      - Wraps errors in custom exception with a snippet of the data in the message
  94     """
  95     return _parse(lambda d: dict(parse_qsl(d, *args, **kwargs)), data, name, exception, schema)
  96
  ```
  --- 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()
  
  ```
  
golden_diff: 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
  @@ -48,8 +48,12 @@
       """Wrapper around lxml.etree.HTML with some extras.
   
       Provides these extra features:
  +     - Removes XML declarations of invalid XHTML5 documents
        - Wraps errors in custom exception with a snippet of the data in the message
       """
  +    if isinstance(data, str) and data.lstrip().startswith("<?xml"):
  +        data = re.sub(r"^\s*<\?xml.+?\?>", "", data)
  +
       return _parse(HTML, data, name, exception, schema, *args, **kwargs)
  
verification_info: {"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@@ -48,8 +48,12 @@\n     \"\"\"Wrapper around lxml.etree.HTML with some extras.\n \n     Provides these extra features:\n+     - Removes XML declarations of invalid XHTML5 documents\n      - Wraps errors in custom exception with a snippet of the data in the message\n     \"\"\"\n+    if isinstance(data, str) and data.lstrip().startswith(\"<?xml\"):\n+        data = re.sub(r\"^\\s*<\\?xml.+?\\?>\", \"\", data)\n+\n     return _parse(HTML, data, name, exception, schema, *args, **kwargs)\n", "issue": "plugins.tviplayer: unable to handle CNN Portugal\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\n- issue:\r\n  - the new `tviplayer` plugin is unable to handle https://tviplayer.iol.pt/direto/CNN \r\n  - of note, the previous TVI 24 became CNN Portugal after #4199.\r\n\r\n- to reproduce:\r\n  ```sh\r\n  streamlink https://tviplayer.iol.pt/direto/CNN\r\n  ```\r\n  ```sh\r\n  [cli][info] Found matching plugin tviplayer for URL https://tviplayer.iol.pt/direto/CNN\r\n  error: Unable to validate response text: Unable to parse HTML: Unicode strings with encoding declaration are not supported. Please use bytes input or XML fragments without declaration. ('<?xml version=\\'1.0\\' encoding=\\'U ...)\r\n  ```\r\n\r\n\r\n\n\n### Debug log\n\n```text\nstreamlink --loglevel debug https://tviplayer.iol.pt/direto/CNN\r\n[cli][debug] OS:         Linux-5.10.0-9-amd64-x86_64-with-glibc2.31\r\n[cli][debug] Python:     3.9.2\r\n[cli][debug] Streamlink: 3.0.2\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://tviplayer.iol.pt/direto/CNN\r\n[cli][debug]  --loglevel=debug\r\n[cli][info] Found matching plugin tviplayer for URL https://tviplayer.iol.pt/direto/CNN\r\nerror: Unable to validate response text: Unable to parse HTML: Unicode strings with encoding declaration are not supported. Please use bytes input or XML fragments without declaration. ('<?xml version=\\'1.0\\' encoding=\\'U ...)\n```\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     - Wraps errors in custom exception with a snippet of the data in the message\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"}], "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     - Removes XML declarations of invalid XHTML5 documents\n     - Wraps errors in custom exception with a snippet of the data in the message\n    \"\"\"\n    if isinstance(data, str) and data.lstrip().startswith(\"<?xml\"):\n        data = re.sub(r\"^\\s*<\\?xml.+?\\?>\", \"\", data)\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"}]}