Repository: streamlink/streamlink

Issue:
CT Sport with ceskatelevize.py doesn't work
<!--
Thanks for reporting a bug!
USE THE TEMPLATE. Otherwise your bug report may be rejected.

First, see the contribution guidelines:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink

Also check the list of open and closed bug reports:
https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22

Please see the text preview to avoid unnecessary formatting errors.
-->


## Bug Report

<!-- Replace [ ] with [x] in order to check the box -->
- [x] This is a bug report and I have read the contribution guidelines.


CT Sport with ceskatelevize.py doesn't work.
CT1, CT2, CT24, CT Decko and CT Art are okay, but CT Sport see Description Logfiles

<!-- Explain the bug as thoroughly as you can. Don't leave out information which is necessary for us to reproduce and debug this issue. -->


### Expected / Actual behavior

<!-- What do you expect to happen, and what is actually happening? -->


### Reproduction steps / Explicit stream URLs to test
c:\Streamlink\1_3_1\bin\streamlink.exe https://www.ceskatelevize.cz/sport/zive-vysilani/ best -l trace
or
c:\Streamlink\1_3_1\bin\streamlink.exe https://sport.ceskatelevize.cz/#live best -l trace

<!-- How can we reproduce this? Please note the exact steps below using the list format supplied. If you need more steps please add them. -->

1. latest Streamlink Version
2. latest ceskatelevize.py but this Version is two years old
3. ...


### Log output

<!--
TEXT LOG OUTPUT IS REQUIRED for a bug report!
Use the `--loglevel debug` parameter and avoid using parameters which suppress log output.
https://streamlink.github.io/cli.html#cmdoption-l

Make sure to **remove usernames and passwords**
You can copy the output to https://gist.github.com/ or paste it below.
-->

```
c:\Streamlink\1_3_1\bin\streamlink.exe https://www.ceskatelevize.cz/sport/zive-vysilani/ best -l trace
[09:22:36,953][cli][debug] OS:         Windows 10
[09:22:36,954][cli][debug] Python:     3.6.6
[09:22:36,956][cli][debug] Streamlink: 1.3.1
[09:22:36,957][cli][debug] Requests(2.22.0), Socks(1.7.1), Websocket(0.57.0)
[09:22:36,958][cli][info] Found matching plugin ceskatelevize for URL https://www.ceskatelevize.cz/sport/zive-vysilani/
[09:22:36,958][plugin.ceskatelevize][warning] SSL certificate verification is disabled.
error: Cannot find playlist info in the player url!
```


### Additional comments, screenshots, etc.
But that is not correct.
At the CT Sport Homepage Chrome Network Tools finds a mpd Link.
The mpd Link is not a DRM Link
This Links are at CT1, CT2, CT24 ... too



[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)



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
@@ -13,89 +13,52 @@
 """
 import logging
 import re
+import json
 
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import useragents, validate
-from streamlink.stream import HLSStream
+from streamlink.stream import HLSStream, DASHStream
 from streamlink.exceptions import PluginError
-
-_url_re = re.compile(
-    r'http(s)?://([^.]*.)?ceskatelevize.cz'
-)
-_player_re = re.compile(
-    r'ivysilani/embed/iFramePlayer[^"]+'
-)
-_hash_re = re.compile(
-    r'hash:"([0-9a-z]+)"'
-)
-_playlist_info_re = re.compile(
-    r'{"type":"([a-z]+)","id":"([0-9]+)"'
-)
-_playlist_url_schema = validate.Schema({
-    validate.optional("streamingProtocol"): validate.text,
-    "url": validate.any(
-        validate.url(),
-        "Error",
-        "error_region"
-    )
-})
-_playlist_schema = validate.Schema({
-    "playlist": [{
-        validate.optional("type"): validate.text,
-        "streamUrls": {
-            "main": validate.url(),
-        }
-    }]
-})
+from streamlink.compat import html_unescape, quote
 
 log = logging.getLogger(__name__)
 
 
-def _find_playlist_info(response):
-    """
-    Finds playlist info (type, id) in HTTP response.
-
-    :param response: Response object.
-    :returns: Dictionary with type and id.
-    """
-    values = {}
-    matches = _playlist_info_re.search(response.text)
-    if matches:
-        values['type'] = matches.group(1)
-        values['id'] = matches.group(2)
-
-    return values
-
-
-def _find_player_url(response):
-    """
-    Finds embedded player url in HTTP response.
-
-    :param response: Response object.
-    :returns: Player url (str).
-    """
-    url = ''
-    matches = _player_re.search(response.text)
-    if matches:
-        tmp_url = matches.group(0).replace('&amp;', '&')
-        if 'hash' not in tmp_url:
-            # there's no hash in the URL, try to find it
-            matches = _hash_re.search(response.text)
-            if matches:
-                url = tmp_url + '&hash=' + matches.group(1)
-        else:
-            url = tmp_url
-
-    return 'http://ceskatelevize.cz/' + url
-
-
 class Ceskatelevize(Plugin):
 
     ajax_url = 'https://www.ceskatelevize.cz/ivysilani/ajax/get-client-playlist'
+    _url_re = re.compile(
+        r'http(s)?://([^.]*.)?ceskatelevize.cz'
+    )
+    _player_re = re.compile(
+        r'ivysilani/embed/iFramePlayer[^"]+'
+    )
+    _hash_re = re.compile(
+        r'hash:"([0-9a-z]+)"'
+    )
+    _playlist_info_re = re.compile(
+        r'{"type":"([a-z]+)","id":"([0-9]+)"'
+    )
+    _playlist_url_schema = validate.Schema({
+        validate.optional("streamingProtocol"): validate.text,
+        "url": validate.any(
+            validate.url(),
+            "Error",
+            "error_region"
+        )
+    })
+    _playlist_schema = validate.Schema({
+        "playlist": [{
+            validate.optional("type"): validate.text,
+            "streamUrls": {
+                "main": validate.url(),
+            }
+        }]
+    })
 
     @classmethod
     def can_handle_url(cls, url):
-        return _url_re.match(url)
+        return cls._url_re.match(url)
 
     def _get_streams(self):
         self.session.http.headers.update({'User-Agent': useragents.IPAD})
@@ -103,19 +66,26 @@ def _get_streams(self):
         log.warning('SSL certificate verification is disabled.')
         # fetch requested url and find playlist info
         response = self.session.http.get(self.url)
-        info = _find_playlist_info(response)
+        info = self._find_playlist_info(response)
 
         if not info:
+            # do next try with new API
+            def _fallback_api(*args, **kwargs):
+                self.api2 = CeskatelevizeAPI2(self.session, self.url, *args, **kwargs)
+                return self.api2._get_streams()
+
             # playlist info not found, let's try to find player url
-            player_url = _find_player_url(response)
+            player_url = self._find_player_url(response)
             if not player_url:
-                raise PluginError('Cannot find playlist info or player url!')
+                log.debug('Cannot find playlist info or player url, do next try with new API')
+                return _fallback_api(res=response)
 
             # get player url and try to find playlist info in it
             response = self.session.http.get(player_url)
-            info = _find_playlist_info(response)
+            info = self._find_playlist_info(response)
             if not info:
-                raise PluginError('Cannot find playlist info in the player url!')
+                log.debug('Cannot find playlist info in the player url, do next try with new API')
+                return _fallback_api()
 
         log.trace('{0!r}'.format(info))
 
@@ -136,7 +106,7 @@ def _get_streams(self):
             data=data,
             headers=headers
         )
-        json_data = self.session.http.json(response, schema=_playlist_url_schema)
+        json_data = self.session.http.json(response, schema=self._playlist_url_schema)
         log.trace('{0!r}'.format(json_data))
 
         if json_data['url'] in ['Error', 'error_region']:
@@ -145,10 +115,154 @@ def _get_streams(self):
 
         # fetch playlist
         response = self.session.http.post(json_data['url'])
-        json_data = self.session.http.json(response, schema=_playlist_schema)
+        json_data = self.session.http.json(response, schema=self._playlist_schema)
         log.trace('{0!r}'.format(json_data))
         playlist = json_data['playlist'][0]['streamUrls']['main']
         return HLSStream.parse_variant_playlist(self.session, playlist)
 
+    @classmethod
+    def _find_playlist_info(cls, response):
+        """
+        Finds playlist info (type, id) in HTTP response.
+
+        :param response: Response object.
+        :returns: Dictionary with type and id.
+        """
+        values = {}
+        matches = cls._playlist_info_re.search(response.text)
+        if matches:
+            values['type'] = matches.group(1)
+            values['id'] = matches.group(2)
+
+        return values
+
+    @classmethod
+    def _find_player_url(cls, response):
+        """
+        Finds embedded player url in HTTP response.
+
+        :param response: Response object.
+        :returns: Player url (str).
+        """
+        url = ''
+        matches = cls._player_re.search(response.text)
+        if matches:
+            tmp_url = matches.group(0).replace('&amp;', '&')
+            if 'hash' not in tmp_url:
+                # there's no hash in the URL, try to find it
+                matches = cls._hash_re.search(response.text)
+                if matches:
+                    url = tmp_url + '&hash=' + matches.group(1)
+            else:
+                url = tmp_url
+
+        return 'http://ceskatelevize.cz/' + url
+
+
+class CeskatelevizeAPI2(object):
+    _player_api = 'https://playlist.ceskatelevize.cz/'
+    _url_re = re.compile(r'http(s)?://([^.]*.)?ceskatelevize.cz')
+    _playlist_info_re = re.compile(r'{\s*"type":\s*"([a-z]+)",\s*"id":\s*"(\w+)"')
+    _playlist_schema = validate.Schema({
+        "CODE": validate.contains("OK"),
+        "RESULT": {
+            "playlist": [{
+                "streamUrls": {
+                    "main": validate.url(),
+                }
+            }]
+        }
+    })
+    _ctcomp_re = re.compile(r'data-ctcomp="Video"\sdata-video-id="(?P<val1>[^"]*)"\sdata-ctcomp-data="(?P<val2>[^"]+)">')
+    _ctcomp_schema = validate.Schema(
+        validate.text,
+        validate.transform(_ctcomp_re.findall),
+        validate.transform(lambda vl: [{"video-id": v[0], "ctcomp-data": json.loads(html_unescape(v[1]))} for v in vl])
+    )
+    _playlist_info_schema = validate.Schema({
+        "type": validate.text,
+        "id": validate.any(validate.text, int),
+        "key": validate.text,
+        "date": validate.text,
+        "requestSource": validate.text,
+        "drm": int,
+        validate.optional("canBePlay"): int,
+        validate.optional("assetId"): validate.text,
+        "quality": validate.text,
+        validate.optional("region"): int
+    })
+
+    def __init__(self, session, url, res=None):
+        self.session = session
+        self.url = url
+        self.response = res
+
+    def _get_streams(self):
+        if self.response is None:
+            infos = self.session.http.get(self.url, schema=self._ctcomp_schema)
+        else:
+            infos = self.session.http.json(self.response, schema=self._ctcomp_schema)
+        if not infos:
+            # playlist infos not found
+            raise PluginError('Cannot find playlist infos!')
+
+        vod_prio = len(infos) == 2
+        for info in infos:
+            try:
+                pl = info['ctcomp-data']['source']['playlist'][0]
+            except KeyError:
+                raise PluginError('Cannot find playlist info!')
+
+            pl = self._playlist_info_schema.validate(pl)
+            if vod_prio and pl['type'] != 'VOD':
+                continue
+
+            log.trace('{0!r}'.format(info))
+            if pl['type'] == 'LIVE':
+                data = {
+                    "contentType": "live",
+                    "items": [{
+                        "id": pl["id"],
+                        "assetId": pl["assetId"],
+                        "key": pl["key"],
+                        "playerType": "dash",
+                        "date": pl["date"],
+                        "requestSource": pl["requestSource"],
+                        "drm": pl["drm"],
+                        "quality": pl["quality"],
+                    }]
+                }
+            elif pl['type'] == 'VOD':
+                data = {
+                    "contentType": "vod",
+                    "items": [{
+                        "id": pl["id"],
+                        "key": pl["key"],
+                        "playerType": "dash",
+                        "date": pl["date"],
+                        "requestSource": pl["requestSource"],
+                        "drm": pl["drm"],
+                        "canBePlay": pl["canBePlay"],
+                        "quality": pl["quality"],
+                        "region": pl["region"]
+                    }]
+                }
+
+        headers = {
+            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+        }
+
+        data = json.dumps(data)
+        response = self.session.http.post(
+            self._player_api,
+            data="data={}".format(quote(data)),
+            headers=headers
+        )
+        json_data = self.session.http.json(response, schema=self._playlist_schema)
+        log.trace('{0!r}'.format(json_data))
+        playlist = json_data['RESULT']['playlist'][0]['streamUrls']['main']
+        for s in DASHStream.parse_manifest(self.session, playlist).items():
+            yield s
+
 
 __plugin__ = Ceskatelevize


Test patch: