Repository: streamlink/streamlink
Pull request #1416 (2018-01-06 19:42:50)

[mitele] Update for different api response
- fallback if not hls_url was found, just the suffix.
- added url tests
- added all channels

Fixed streamlink/streamlink#1337


Patch:
diff --git a/src/streamlink/plugins/mitele.py b/src/streamlink/plugins/mitele.py
index 67a8ae57..f2e7b7c9 100644
--- a/src/streamlink/plugins/mitele.py
+++ b/src/streamlink/plugins/mitele.py
@@ -1,141 +1,124 @@
 from __future__ import print_function
 
 import re
-from base64 import b64decode
-
-from datetime import datetime
-from uuid import uuid4
 
+from streamlink import NoStreamsError
+from streamlink.compat import urljoin
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import http
+from streamlink.plugin.api import useragents
 from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream
 from streamlink.utils import parse_json
 
 
 class Mitele(Plugin):
-    url_re = re.compile(r"https?://(?:www.)?mitele.es/directo/(\w+)")
-    supported_channels = ("telecinco", "bemad", "boing", "cuatro")
-    app_key = "56c3464fe4b0b8a18ac02511"
-    session_url = "https://appgrid-api.cloud.accedo.tv/session"
-    config_url = "https://appgrid-api.cloud.accedo.tv/metadata/general_configuration, web_configuration?" \
-                 "sessionKey={key}"
-    channel_id_url = "http://indalo.mediaset.es/mmc-player/api/mmc/v1/{channel}/live/flash.json"
-    stream_info_url = "http://player.ooyala.com/sas/player_api/v2/authorization/embed_code/{key}/{yoo}?" \
-                      "device=html5&domain=www.mitele.es"
-    session_schema = validate.Schema({
-        "sessionKey": validate.text,
-        "expiration": validate.transform(lambda d: datetime.strptime(d, "%Y%m%dT%H:%M:%S+0000"))
-    })
-    config_schema = validate.Schema({
-        "general_configuration": {
-            "api_configuration": {
-                "ooyala_discovery": {
-                    "api_key": validate.text
-                }
-            }
-        }
-    })
-    channel_id_schema = validate.Schema(
+    _url_re = re.compile(r"https?://(?:www\.)?mitele\.es/directo/(?P<channel>\w+)")
+    _suffix_re = re.compile(r"""acl=(?P<path>[^"'*]+)""")
+
+    # For more informations see:
+    # https://github.com/streamlink/streamlink/issues/1337
+    livehlsdai = "https://livehlsdai-i.akamaihd.net"
+    mdslivehls = "https://mdslivehls-i.akamaihd.net"
+
+    _channel_domains = {
+        "bemad": mdslivehls,
+        "boing": mdslivehls,
+        "cuatro": livehlsdai,
+        "divinity": mdslivehls,
+        "energy": mdslivehls,
+        "fdf": mdslivehls,
+        "telecinco": livehlsdai,
+    }
+
+    pdata_url = "https://indalo.mediaset.es/mmc-player/api/mmc/v1/{channel}/live/flash.json"
+    gate_url = "https://gatekeeper.mediaset.es"
+
+    pdata_schema = validate.Schema(
         validate.all(
-            {"locations": [{"yoo": validate.text}]},
+            {
+                "locations": [{
+                    "gcp": validate.text,
+                    "ogn": validate.text,
+                }],
+            },
             validate.get("locations"),
             validate.get(0),
-            validate.get("yoo")
         )
     )
-    stream_info_schema = validate.Schema({
-        "authorization_data": {
-            validate.text: {
-                "authorized": bool,
-                "message": validate.text,
-                validate.optional("streams"): [{
-                    "delivery_type": validate.text,
-                    "url": {
-                        "format": "encoded",
-                        "data": validate.all(validate.text,
-                                             validate.transform(b64decode),
-                                             validate.transform(lambda d: d.decode("utf8")),
-                                             validate.url())
-                    }
-                }]
-            }
+
+    def __init__(self, url):
+        super(Mitele, self).__init__(url)
+        self.headers = {
+            "User-Agent": useragents.FIREFOX,
+            "Referer": self.url
         }
-    })
 
     @classmethod
     def can_handle_url(cls, url):
-        m = cls.url_re.match(url)
-        return m and m.group(1) in cls.supported_channels
+        m = cls._url_re.match(url)
+        return m and m.group("channel") in cls._channel_domains.keys()
 
-    @property
-    def session_key(self):
-        """
-        Get a cached or new session key, uuid is a random uuid (type 4)
-        :return:
+    def get_pdata(self, channel):
         """
-        session_key = self.cache.get("sessionKey")
-        if session_key:
-            self.logger.debug("Using cached sessionKey")
-            return session_key
-        else:
-            self.logger.debug("Requesting new sessionKey")
-            uuid = uuid4()
-            res = http.get(self.session_url, params=dict(appKey=self.app_key, uuid=uuid))
-            data = parse_json(res.text, schema=self.session_schema)
-            # when to expire the sessionKey, -1 hour for good measure
-            expires_in = (data["expiration"] - datetime.now()).total_seconds() - 3600
-            self.cache.set("sessionKey", data["sessionKey"], expires=expires_in)
-            return data["sessionKey"]
-
-    @property
-    def config(self):
-        """
-        Get the API config data
-        """
-        config_res = http.get(self.config_url.format(key=self.session_key))
-        return parse_json(config_res.text, schema=self.config_schema)
-
-    def get_channel_id(self, channel):
-        """
-        Get the ID of the channel form the name
+        Get the params for the post request
         :param channel: channel name
-        :return: channel id
+        :return: "gcp" and "ogn"
         """
-        channel_id_res = http.get(self.channel_id_url.format(channel=channel))
-        return parse_json(channel_id_res.text, schema=self.channel_id_schema)
+        res = http.get(self.pdata_url.format(channel=channel))
+        return parse_json(res.text, schema=self.pdata_schema)
 
-    def get_stream_info(self, key, channel_id):
+    def create_hls_url(self, suffix):
         """
-        Get details about the streams
-        :param key: API key
-        :param channel_id: channel id
-        :return: stream info
+        creates a valid hls_url
+        :param suffix: url session params
+        :return: hls_url
         """
-        stream_info_res = http.get(self.stream_info_url.format(key=key, yoo=channel_id))
-        return parse_json(stream_info_res.text, schema=self.stream_info_schema)
+        m = self._suffix_re.search(suffix)
+        if not m:
+            return
+        self.logger.debug("create hls_url from suffix")
 
-    def _get_streams(self):
-        channel = self.url_re.match(self.url).group(1)
+        channel = self._url_re.match(self.url).group("channel")
+        domain = self._channel_domains[channel]
+        path = m.group("path")
+        path_suffix = "master.m3u8?{suffix}".format(suffix=suffix)
+
+        hls_url = urljoin(domain, path)
+        hls_url = urljoin(hls_url, path_suffix)
+        return hls_url
 
-        key, sig = self.config["general_configuration"]["api_configuration"]["ooyala_discovery"]["api_key"].split(".")
-        self.logger.debug("Got api key: {}.{}", key, sig)
+    def get_stream_url(self, data):
+        """
+        Get the hls_url from the post request
+        :param data: dict with "gcp" and "ogn"
+        :return: hls_url
+        """
+        try:
+            res = http.post(self.gate_url, headers=self.headers, data=data)
+        except Exception as e:
+            if "403" in str(e):
+                self.logger.error("This Video is Not Available in Your Country.")
+            raise NoStreamsError(self.url)
 
-        channel_id = self.get_channel_id(channel)
-        self.logger.debug("Got channel ID {} for channel {}", channel_id, channel)
+        r_data = parse_json(res.text)
+        hls_url = r_data.get("stream")
+        suffix = r_data.get("suffix")
 
-        data = self.get_stream_info(key, channel_id)
+        if hls_url is None and suffix:
+            hls_url = self.create_hls_url(suffix)
+        return hls_url
 
-        stream_info = data["authorization_data"][channel_id]
+    def _get_streams(self):
+        channel = self._url_re.match(self.url).group("channel")
 
-        if stream_info["authorized"]:
-            for stream in stream_info["streams"]:
-                if stream["delivery_type"] == "hls":
-                    for s in HLSStream.parse_variant_playlist(self.session, stream["url"]["data"]).items():
-                        yield s
+        pdata = self.get_pdata(channel)
+        hls_url = self.get_stream_url(pdata)
 
-        else:
-            self.logger.error("Cannot load streams: {}", stream_info["message"])
+        if hls_url:
+            self.logger.debug("HLS URL: {0}".format(hls_url))
+            for s in HLSStream.parse_variant_playlist(self.session, hls_url, headers=self.headers).items():
+                yield s
 
 
 __plugin__ = Mitele


Test patch:
diff --git a/tests/test_plugin_mitele.py b/tests/test_plugin_mitele.py
new file mode 100644
index 00000000..502c75a4
--- /dev/null
+++ b/tests/test_plugin_mitele.py
@@ -0,0 +1,25 @@
+import unittest
+
+from streamlink.plugins.mitele import Mitele
+
+
+class TestPluginMitele(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            "http://www.mitele.es/directo/bemad",
+            "http://www.mitele.es/directo/boing",
+            "http://www.mitele.es/directo/cuatro",
+            "http://www.mitele.es/directo/divinity",
+            "http://www.mitele.es/directo/energy",
+            "http://www.mitele.es/directo/fdf",
+            "http://www.mitele.es/directo/telecinco",
+        ]
+        for url in should_match:
+            self.assertTrue(Mitele.can_handle_url(url))
+
+        should_not_match = [
+            "http://www.mitele.es",
+            "http://www.mitele.es/directo/random_channel",
+        ]
+        for url in should_not_match:
+            self.assertFalse(Mitele.can_handle_url(url))