Repository: streamlink/streamlink

Issue:
plugins.tvp: Unable to validate value of key 'result'
### 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 build from the master branch

### Description

When trying TVP plugin an error occurs
"error: Unable to validate response text: ValidationError(dict):"

### Debug log

```text
[cli][info] streamlink is running as root! Be careful!
[cli][debug] OS:         Linux-5.4.0-132-generic-x86_64-with-glibc2.29
[cli][debug] Python:     3.8.10
[cli][debug] Streamlink: 5.1.1+2.g29d1f392
[cli][debug] Dependencies:
[cli][debug]  certifi: 2022.6.15
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.6.5
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.9.9
[cli][debug]  PySocks: 1.6.8
[cli][debug]  requests: 2.27.1
[cli][debug]  urllib3: 1.26.12
[cli][debug]  websocket-client: 1.3.2
[cli][debug]  importlib-metadata: 1.5.0
[cli][debug] Arguments:
[cli][debug]  url=https://stream.tvp.pl/?channel_id=63759349
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][info] Found matching plugin tvp for URL https://stream.tvp.pl/?channel_id=63759349
[plugins.tvp][debug] video ID: 63759349
error: Unable to validate response text: ValidationError(dict):
  Unable to validate value of key 'result'
  Context(type):
    Type of None should be dict, but is NoneType
```



Discussion:
> https://stream.tvp.pl/?channel_id=63759349

The player JSON data includes `null` for the requested ID. The ID that's being used by the plugin is the one from the URL. Their website however is not using this ID, it's using the first item of the channels JSON data that's embedded in the site of the input URL and this does only get read when no channel ID was provided by the user. When the plugin was rewritten by me last month in #4905, this issue did not come up, so I'm a bit surprised here. Let me have a look...
Indeed I took the id from inside the html page (14812849) and it works, but from the url the 1455 does not work
I changed the order of ```video_id = video_id or self._get_video_id()``` to ```video_id = self._get_video_id() or video_id``` to force it to retrieve the video id from the json and it works.

EDIT : It gets always the first one so in reality it doesnt work
The fix is to look up the ID in the channels JSON and return the first one if no ID could be found. I'll open a PR with a fix in a bit.

Resolving patch:
diff --git a/src/streamlink/plugins/tvp.py b/src/streamlink/plugins/tvp.py
--- a/src/streamlink/plugins/tvp.py
+++ b/src/streamlink/plugins/tvp.py
@@ -7,10 +7,14 @@
 
 import logging
 import re
+from typing import List, Optional, Tuple
+from urllib.parse import urlparse
 
 from streamlink.plugin import Plugin, pluginmatcher
 from streamlink.plugin.api import validate
+from streamlink.stream.dash import DASHStream
 from streamlink.stream.hls import HLSStream
+from streamlink.stream.http import HTTPStream
 
 log = logging.getLogger(__name__)
 
@@ -19,7 +23,7 @@
     r"""
         https?://
         (?:
-            (?:tvpstream\.vod|stream)\.tvp\.pl(?:/(?:\?channel_id=(?P<video_id>\d+))?)?$
+            (?:tvpstream\.vod|stream)\.tvp\.pl(?:/(?:\?channel_id=(?P<channel_id>\d+))?)?$
             |
             vod\.tvp\.pl/[^/]+/.+,(?P<vod_id>\d+)$
         )
@@ -30,8 +34,8 @@ class TVP(Plugin):
     _URL_PLAYER = "https://stream.tvp.pl/sess/TVPlayer2/embed.php"
     _URL_VOD = "https://vod.tvp.pl/api/products/{vod_id}/videos/playlist"
 
-    def _get_video_id(self):
-        return self.session.http.get(
+    def _get_video_id(self, channel_id: Optional[str]):
+        items: List[Tuple[int, int]] = self.session.http.get(
             self.url,
             headers={
                 # required, otherwise the next request for retrieving the HLS URL will be aborted by the server
@@ -42,27 +46,42 @@ def _get_video_id(self):
                 validate.none_or_all(
                     validate.get("json"),
                     validate.parse_json(),
-                    [{
-                        "items": validate.none_or_all(
-                            [{
-                                "video_id": int,
-                            }],
+                    [
+                        validate.all(
+                            {
+                                "id": int,
+                                "items": validate.none_or_all(
+                                    [{
+                                        "video_id": int,
+                                    }],
+                                    validate.get((0, "video_id")),
+                                ),
+                            },
+                            validate.union_get("id", "items"),
                         ),
-                    }],
-                    validate.get((0, "items", 0, "video_id")),
+                    ],
                 ),
             ),
         )
 
-    def _get_live(self, video_id):
-        video_id = video_id or self._get_video_id()
+        if channel_id is not None:
+            _channel_id = int(channel_id)
+            try:
+                return next(item[1] for item in items if item[0] == _channel_id)
+            except StopIteration:
+                pass
+
+        return items[0][1] if items else None
+
+    def _get_live(self, channel_id: Optional[str]):
+        video_id = self._get_video_id(channel_id)
         if not video_id:
             log.error("Could not find video ID")
             return
 
         log.debug(f"video ID: {video_id}")
 
-        return self.session.http.get(
+        streams: Optional[List[Tuple[str, str]]] = self.session.http.get(
             self._URL_PLAYER,
             params={
                 "ID": video_id,
@@ -76,21 +95,46 @@ def _get_live(self, video_id):
                 validate.get("json"),
                 validate.parse_json(),
                 {
-                    "result": {
-                        "content": {
-                            "files": validate.all(
-                                [{
-                                    "type": str,
-                                    "url": validate.url(),
-                                }],
-                                validate.filter(lambda item: item["type"] == "hls"),
-                            ),
+                    "result": validate.none_or_all(
+                        {
+                            "content": {
+                                "files": [
+                                    validate.all(
+                                        {
+                                            "type": str,
+                                            "url": validate.url(),
+                                        },
+                                        validate.union_get("type", "url"),
+                                    ),
+                                ],
+                            },
                         },
-                    },
+                        validate.get(("content", "files")),
+                    ),
                 },
-                validate.get(("result", "content", "files", 0, "url")),
+                validate.get("result"),
             ),
         )
+        if not streams:
+            return
+
+        def get(items, condition):
+            return next((_url for _stype, _url in items if condition(_stype, urlparse(_url).path)), None)
+
+        # prioritize HLSStream and get the first available stream
+        url = get(streams, lambda t, _: t == "hls")
+        if url:
+            return HLSStream.parse_variant_playlist(self.session, url)
+
+        # fall back to DASHStream
+        url = get(streams, lambda t, p: t == "any_native" and p.endswith(".mpd"))
+        if url:
+            return DASHStream.parse_manifest(self.session, url)
+
+        # fall back to HTTPStream
+        url = get(streams, lambda t, p: t == "any_native" and p.endswith(".mp4"))
+        if url:
+            return {"vod": HTTPStream(self.session, url)}
 
     def _get_vod(self, vod_id):
         data = self.session.http.get(
@@ -123,16 +167,13 @@ def _get_vod(self, vod_id):
             return
 
         if data.get("HLS"):
-            return data["HLS"][0]["src"]
+            return HLSStream.parse_variant_playlist(self.session, data["HLS"][0]["src"])
 
     def _get_streams(self):
         if self.match["vod_id"]:
-            hls_url = self._get_vod(self.match["vod_id"])
+            return self._get_vod(self.match["vod_id"])
         else:
-            hls_url = self._get_live(self.match["video_id"])
-
-        if hls_url:
-            return HLSStream.parse_variant_playlist(self.session, hls_url)
+            return self._get_live(self.match["channel_id"])
 
 
 __plugin__ = TVP


Test patch:
diff --git a/tests/plugins/test_tvp.py b/tests/plugins/test_tvp.py
--- a/tests/plugins/test_tvp.py
+++ b/tests/plugins/test_tvp.py
@@ -9,13 +9,13 @@ class TestPluginCanHandleUrlTVP(PluginCanHandleUrl):
         # live
         ("https://stream.tvp.pl", {}),
         ("https://stream.tvp.pl/", {}),
-        ("https://stream.tvp.pl/?channel_id=63759349", {"video_id": "63759349"}),
-        ("https://stream.tvp.pl/?channel_id=14812849", {"video_id": "14812849"}),
+        ("https://stream.tvp.pl/?channel_id=63759349", {"channel_id": "63759349"}),
+        ("https://stream.tvp.pl/?channel_id=14812849", {"channel_id": "14812849"}),
         # old live URLs
         ("https://tvpstream.vod.tvp.pl", {}),
         ("https://tvpstream.vod.tvp.pl/", {}),
-        ("https://tvpstream.vod.tvp.pl/?channel_id=63759349", {"video_id": "63759349"}),
-        ("https://tvpstream.vod.tvp.pl/?channel_id=14812849", {"video_id": "14812849"}),
+        ("https://tvpstream.vod.tvp.pl/?channel_id=63759349", {"channel_id": "63759349"}),
+        ("https://tvpstream.vod.tvp.pl/?channel_id=14812849", {"channel_id": "14812849"}),
 
         # VOD
         (