Repository: streamlink/streamlink

Issue:
plugins.ceskatelevize: URL updates needed for CT24 and CT Sport
### 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

Ceska Televize offers five channels. For three of them, ceskatelevize.py still works fine (CT1, CT 2, CT :D/Art).
However, the remaining two channels have recently changed their URLs., i.e.

CT 24: formerly  https://www.ceskatelevize.cz/zive/ct24/ , now redirects to
https://ct24.ceskatelevize.cz/#live

CT Sport: formerly  https://www.ceskatelevize.cz/zive/sport/ , now redirects to
https://sport.ceskatelevize.cz/#live

These two new URLs will not be recognised by ceskatelevize.py anymore as it basically looks for www.ceskatelevize.cz/zive/...
It yould be great if you could update ceskatelevize.py to enable it again for CT 24 and CT Sport.
Please note that CT Sport is geoblocked for the Czech Republic. This is not the case for CT 24.


### Debug log

```text
Debug für CT24 with its now outdated URL:
[streamlinksrv][debug] Arguments:
[streamlinksrv][debug]  url=https://www.ceskatelevize.cz/zive/ct24/
[streamlinksrv][debug]  stream=['best']
[streamlinksrv][info] Found matching plugin ceskatelevize for URL https://www.ceskatelevize.cz/zive/ct24/
[streamlinksrv][error] No playable streams found on this URL: https://www.ceskatelevize.cz/zive/ct24/
[streamlinksrv][debug] Send Offline clip
```



Discussion:

Resolving patch:
diff --git a/src/streamlink/plugins/ceskatelevize.py b/src/streamlink/plugins/ceskatelevize.py
--- a/src/streamlink/plugins/ceskatelevize.py
+++ b/src/streamlink/plugins/ceskatelevize.py
@@ -5,131 +5,206 @@
 $region Czechia
 """
 
+import json
 import logging
 import re
+from urllib.parse import urlparse
 
-from streamlink.plugin import Plugin, PluginError, pluginmatcher
+from streamlink.plugin import Plugin, pluginmatcher
 from streamlink.plugin.api import validate
 from streamlink.stream.dash import DASHStream
 
 log = logging.getLogger(__name__)
 
 
-@pluginmatcher(re.compile(
-    r"https://(?:www\.)?ceskatelevize\.cz/zive/\w+"
-))
+@pluginmatcher(re.compile(r"https?://ct24\.ceskatelevize\.cz/"))
+@pluginmatcher(re.compile(r"https?://decko\.ceskatelevize\.cz/"))
+@pluginmatcher(re.compile(r"https?://sport\.ceskatelevize\.cz/"))
+@pluginmatcher(re.compile(r"https?://(?:www\.)?ceskatelevize\.cz/zive/\w+"))
 class Ceskatelevize(Plugin):
-    _re_playlist_info = re.compile(r"{\"type\":\"([a-z]+)\",\"id\":\"([0-9]+)\"")
-
-    def _get_streams(self):
-        self.session.http.headers.update({"Referer": self.url})
-        schema_data = validate.Schema(
-            validate.parse_html(),
-            validate.xml_xpath_string(".//script[@id='__NEXT_DATA__'][text()]/text()"),
-            str,
-            validate.parse_json(),
-            {
-                "props": {
-                    "pageProps": {
-                        "data": {
-                            "liveBroadcast": {
-                                # "id": str,
-                                "current": validate.any(None, {
-                                    "channel": str,
-                                    "channelName": str,
-                                    "legacyEncoder": str,
-                                }),
-                                "next": validate.any(None, {
-                                    "channel": str,
-                                    "channelName": str,
-                                    "legacyEncoder": str,
-                                })
-                            }
-                        }
-                    }
-                }
+    schema_playlist = {
+        "playlist": [{
+            "streamUrls": {
+                "main": validate.url(),
             },
-            validate.get(("props", "pageProps", "data", "liveBroadcast")),
-            validate.union_get("current", "next"),
-        )
+        }],
+    }
 
-        try:
-            data_current, data_next = self.session.http.get(
-                self.url, schema=schema_data)
-        except PluginError:
-            return
-
-        log.debug(f"current={data_current!r}")
-        log.debug(f"next={data_next!r}")
-
-        data = data_current or data_next
-        video_id = data["legacyEncoder"]
-        self.title = data["channelName"]
-
-        _hash = self.session.http.get(
-            "https://www.ceskatelevize.cz/v-api/iframe-hash/",
-            schema=validate.Schema(str))
-        res = self.session.http.get(
-            "https://www.ceskatelevize.cz/ivysilani/embed/iFramePlayer.php",
-            params={
-                "hash": _hash,
-                "origin": "iVysilani",
-                "autoStart": "true",
-                "videoID": video_id,
-            },
-        )
-
-        m = self._re_playlist_info.search(res.text)
-        if not m:
-            return
-        _type, _id = m.groups()
-
-        data = self.session.http.post(
+    def get_stream_url(self, video_id):
+        url = self.session.http.post(
             "https://www.ceskatelevize.cz/ivysilani/ajax/get-client-playlist/",
             data={
-                "playlist[0][type]": _type,
-                "playlist[0][id]": _id,
+                "playlist[0][type]": "channel",
+                "playlist[0][id]": video_id,
                 "requestUrl": "/ivysilani/embed/iFramePlayer.php",
                 "requestSource": "iVysilani",
                 "type": "html",
                 "canPlayDRM": "false",
             },
-            headers={
-                "x-addr": "127.0.0.1",
-            },
             schema=validate.Schema(
                 validate.parse_json(),
                 {
-                    validate.optional("streamingProtocol"): str,
-                    "url": validate.any(
-                        validate.url(),
-                        "Error",
-                        "error_region"
-                    )
-                }
+                    "url": validate.url(),
+                },
+                validate.get("url"),
             ),
         )
 
-        if data["url"] in ["Error", "error_region"]:
-            log.error("This stream is not available")
-            return
+        return self.session.http.get(
+            url,
+            schema=validate.Schema(
+                validate.parse_json(),
+                self.schema_playlist,
+                validate.get(("playlist", 0, "streamUrls", "main")),
+            ),
+        )
+
+    def get_ct24(self):
+        self.id = 24
+        self.title = "ČT24"
+        return self.get_stream_url(self.id)
 
-        url = self.session.http.get(
-            data["url"],
+    def get_decko(self):
+        self.id = 5
+        self.title = "Déčko"
+        return self.get_stream_url(self.id)
+
+    def get_sport(self, content):
+        video_id, key, date = validate.Schema(
+            validate.parse_html(),
+            validate.xml_xpath_string(".//section[@id='live']/@data-ctcomp-data"),
+            str,
+            validate.parse_json(),
+            {
+                "items": [{
+                    "items": [{
+                        validate.optional("video"): {
+                            "data": {
+                                "source": {
+                                    "playlist": [{
+                                        "id": int,
+                                        "key": str,
+                                        "date": str,
+                                        "noDrmData": {
+                                            "id": int,
+                                            "key": str,
+                                            "drm": int,
+                                            "quality": str,
+                                            "assetId": str,
+                                        },
+                                    }],
+                                },
+                            },
+                        },
+                    }],
+                }],
+            },
+            validate.get(("items", 0, "items", 0, "video", "data", "source", "playlist", 0)),
+            validate.union_get(("noDrmData", "id"), ("noDrmData", "key"), "date"),
+        ).validate(content)
+
+        self.id = video_id
+        self.title = "ČT sport"
+
+        return self.session.http.post(
+            "https://playlist.ceskatelevize.cz/",
+            data={
+                "data": json.dumps(
+                    {
+                        "contentType": "live",
+                        "items": [{
+                            "id": video_id,
+                            "key": f"{key}",
+                            "assetId": "CT4DRM",
+                            "playerType": "dash",
+                            "date": f"{date}",
+                            "requestSource": "front-sport",
+                            "drm": 0,
+                            "quality": "web",
+                        }],
+                    },
+                    separators=(',', ':'),
+                ),
+            },
             schema=validate.Schema(
                 validate.parse_json(),
                 {
-                    "playlist": [{
-                        validate.optional("type"): str,
-                        "streamUrls": {
-                            "main": validate.url(),
-                        }
-                    }]
+                    "RESULT": self.schema_playlist,
                 },
-                validate.get(("playlist", 0, "streamUrls", "main"))
-            )
+                validate.get(("RESULT", "playlist", 0, "streamUrls", "main")),
+            ),
         )
-        return DASHStream.parse_manifest(self.session, url)
+
+    def get_channel(self, content):
+        data = validate.Schema(
+            validate.parse_html(),
+            validate.xml_xpath_string(".//script[@id='__NEXT_DATA__'][text()]/text()"),
+            str,
+            validate.parse_json(),
+            {
+                "props": {
+                    "pageProps": {
+                        validate.optional("data"): {
+                            "liveBroadcast": {
+                                "id": str,
+                                "current": validate.none_or_all(
+                                    {
+                                        "id": str,
+                                        "channelName": str,
+                                        "isPlayable": bool,
+                                    },
+                                ),
+                                "next": validate.none_or_all(
+                                    {
+                                        "id": str,
+                                        "channelName": str,
+                                        "isPlayable": bool,
+                                    },
+                                ),
+                            },
+                        },
+                    },
+                },
+            },
+            validate.get(("props", "pageProps")),
+        ).validate(content)
+
+        if not data:
+            return
+
+        log.debug(f"data={data}")
+        data = data.get("data").get("liveBroadcast")
+        self.id = data.get("id")
+
+        data = data.get("current") or data.get("next")
+        if not data:
+            return
+
+        self.title = data.get("channelName")
+
+        return self.get_stream_url(self.id)
+
+    def _get_streams(self):
+        res = self.session.http.get(self.url)
+
+        if "://ct24" in res.url:
+            url = self.get_ct24()
+        elif "://decko" in res.url:
+            url = self.get_decko()
+        elif "://sport" in res.url:
+            url = self.get_sport(res.content)
+        else:
+            url = self.get_channel(res.content)
+
+        if url:
+            res = self.session.http.head(url, allow_redirects=True)
+            log.debug(f"res.url={res.url}")
+            p = urlparse(res.url).path
+            if not p.split("/")[-1].startswith(str(self.id)):
+                log.error("This stream is not available")
+                return
+            else:
+                return DASHStream.parse_manifest(self.session, res.url)
 
 
 __plugin__ = Ceskatelevize


Test patch:
diff --git a/tests/plugins/test_ceskatelevize.py b/tests/plugins/test_ceskatelevize.py
--- a/tests/plugins/test_ceskatelevize.py
+++ b/tests/plugins/test_ceskatelevize.py
@@ -6,23 +6,17 @@ class TestPluginCanHandleUrlCeskatelevize(PluginCanHandleUrl):
     __plugin__ = Ceskatelevize
 
     should_match = [
-        "https://www.ceskatelevize.cz/zive/ct1/",
-        "https://www.ceskatelevize.cz/zive/ct2/",
-        "https://www.ceskatelevize.cz/zive/ct24/",
-        "https://www.ceskatelevize.cz/zive/ct26/",
-        "https://www.ceskatelevize.cz/zive/ct27/",
-        "https://www.ceskatelevize.cz/zive/ct28/",
-        "https://www.ceskatelevize.cz/zive/ct31/",
-        "https://www.ceskatelevize.cz/zive/ct32/",
-        "https://www.ceskatelevize.cz/zive/decko/",
-        "https://www.ceskatelevize.cz/zive/sport/",
+        "https://ceskatelevize.cz/zive/any",
+        "https://www.ceskatelevize.cz/zive/any",
+        "https://ct24.ceskatelevize.cz/",
+        "https://ct24.ceskatelevize.cz/any",
+        "https://decko.ceskatelevize.cz/",
+        "https://decko.ceskatelevize.cz/any",
+        "https://sport.ceskatelevize.cz/",
+        "https://sport.ceskatelevize.cz/any",
     ]
 
     should_not_match = [
-        "http://decko.ceskatelevize.cz/zive/",
-        "http://www.ceskatelevize.cz/art/zive/",
-        "http://www.ceskatelevize.cz/ct1/zive/",
-        "http://www.ceskatelevize.cz/ct2/zive/",
-        "http://www.ceskatelevize.cz/ct24/",
-        "http://www.ceskatelevize.cz/sport/zive-vysilani/",
+        "https://ceskatelevize.cz/",
+        "https://www.ceskatelevize.cz/zive/",
     ]