Repository: streamlink/streamlink

Issue:
Kanal7 Defective again
Only 2 months later they have changed the design.

Not opening with latest 0.9.0 Release:

[cli][info] Found matching plugin kanal7 for URL http://www.kanal7.com/canli-izle
error: No playable streams found on this URL: http://www.kanal7.com/canli-izle

Comments:

Patch:
diff --git a/docs/issues.rst b/docs/issues.rst
index 5117b5c6..1a88df73 100644
--- a/docs/issues.rst
+++ b/docs/issues.rst
@@ -20,7 +20,6 @@ Player        Parameter                Note
 ============= ======================== ======================================
 MPC-HC        --                       Currently no way of configuring the cache
 MPlayer       ``-cache <kbytes>``      Between 1024 and 8192 is recommended
-MPlayer2      ``-cache <kbytes>``      Between 1024 and 8192 is recommended
 mpv           ``--cache <kbytes>``     Between 1024 and 8192 is recommended
 VLC           ``--file-caching <ms>    Between 1000 and 10000 is recommended
               --network-caching <ms>``
diff --git a/docs/players.rst b/docs/players.rst
index 00183d07..e8e94525 100644
--- a/docs/players.rst
+++ b/docs/players.rst
@@ -32,7 +32,6 @@ Name                                                  Stdin Pipe Named Pipe HTTP
 `Daum Pot Player <http://potplayer.daum.net>`_        No         No         Yes [1]_
 `MPC-HC <http://mpc-hc.org/>`_                        Yes [2]_   No         Yes [1]_
 `MPlayer <http://mplayerhq.hu>`_                      Yes        Yes        Yes
-`MPlayer2 <http://mplayer2.org>`_                     Yes        Yes        Yes
 `mpv <http://mpv.io>`_                                Yes        Yes        Yes
 `QuickTime <http://apple.com/quicktime>`_             No         No         No
 `VLC media player <http://videolan.org>`_             Yes [3]_   Yes        Yes
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index db98da77..8aab1490 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -30,7 +30,6 @@ artetv              arte.tv              Yes   Yes
 atresplayer         atresplayer.com      Yes   No    Streams are geo-restricted to Spain.
 bambuser            bambuser.com         Yes   Yes
 bbciplayer          bbc.co.uk/iplayer    Yes   Yes   Streams may be geo-restricted to the United Kingdom.
-beam                beam.pro             Yes   Yes
 beattv              be-at.tv             Yes   Yes   Playlist not implemented yet.
 bfmtv               bfmtv.com            Yes   Yes
                     01net.com
@@ -131,6 +130,7 @@ media_ccc_de        - media.ccc.de       Yes   Yes   Only mp4 and HLS are suppor
 mediaklikk          mediaklikk.hu        Yes   No    Streams may be geo-restricted to Hungary.
 mips                mips.tv              Yes   --    Requires rtmpdump with K-S-V patches.
 mitele              mitele.es            Yes   No    Streams may be geo-restricted to Spain.
+mixer               mixer.com            Yes   Yes
 mlgtv               mlg.tv               Yes   --
 nbc                 nbc.com              No    Yes   Streams are geo-restricted to USA. Authentication is not supported.
 nbcsports           nbcsports.com        No    Yes   Streams maybe be geo-restricted to USA. Authentication is not supported.
diff --git a/src/streamlink/plugins/beam.py b/src/streamlink/plugins/beam.py
deleted file mode 100644
index 4a71faba..00000000
--- a/src/streamlink/plugins/beam.py
+++ /dev/null
@@ -1,109 +0,0 @@
-import re
-
-from streamlink.compat import urlparse, parse_qsl, urljoin
-from streamlink.plugin import Plugin
-from streamlink.plugin.api import http, validate
-from streamlink.stream import HLSStream
-from streamlink.stream import HTTPStream
-from streamlink.stream import RTMPStream
-
-_url_re = re.compile(r"http(s)?://(\w+.)?beam.pro/(?P<channel>[^/?]+)")
-
-
-class Beam(Plugin):
-    api_url = "https://beam.pro/api/v1/{type}/{id}"
-    channel_manifest = "https://beam.pro/api/v1/channels/{id}/manifest.{type}"
-
-    _vod_schema = validate.Schema({
-        "state": "AVAILABLE",
-        "vods": [{
-            "baseUrl": validate.url(),
-            "data": validate.any(None, {
-                "Height": int
-            }),
-            "format": validate.text
-            }]
-        },
-        validate.get("vods"),
-        validate.filter(lambda x: x["format"] in ("raw", "hls")),
-        [validate.union({
-            "url": validate.get("baseUrl"),
-            "format": validate.get("format"),
-            "height": validate.all(validate.get("data"), validate.get("Height"))
-        })])
-    _assets_schema = validate.Schema(
-        validate.union({
-            "base": validate.all(
-                validate.xml_find("./head/meta"),
-                validate.get("base"),
-                validate.url(scheme="rtmp")
-            ),
-            "videos": validate.all(
-                validate.xml_findall(".//video"),
-                [
-                    validate.union({
-                        "src": validate.all(
-                            validate.get("src"),
-                            validate.text
-                        ),
-                        "height": validate.all(
-                            validate.get("height"),
-                            validate.text,
-                            validate.transform(int)
-                        )
-                    })
-                ]
-            )
-        })
-    )
-
-    @classmethod
-    def can_handle_url(cls, url):
-        return _url_re.match(url)
-
-    def _get_vod_stream(self, vod_id):
-        res = http.get(self.api_url.format(type="recordings", id=vod_id))
-        for sdata in http.json(res, schema=self._vod_schema):
-            if sdata["format"] == "hls":
-                hls_url = urljoin(sdata["url"], "manifest.m3u8")
-                yield "{0}p".format(sdata["height"]), HLSStream(self.session, hls_url)
-            elif sdata["format"] == "raw":
-                raw_url = urljoin(sdata["url"], "source.mp4")
-                yield "{0}p".format(sdata["height"]), HTTPStream(self.session, raw_url)
-
-    def _get_live_stream(self, channel):
-        res = http.get(self.api_url.format(type="channels", id=channel))
-        channel_info = http.json(res)
-
-        if not channel_info["online"]:
-            return
-
-        res = http.get(self.channel_manifest.format(id=channel_info["id"], type="smil"))
-        assets = http.xml(res, schema=self._assets_schema)
-
-        for video in assets["videos"]:
-            name = "{0}p".format(video["height"])
-            stream = RTMPStream(self.session, {
-                "rtmp": "{0}/{1}".format(assets["base"], video["src"])
-            })
-            yield name, stream
-
-        for s in HLSStream.parse_variant_playlist(self.session,
-                                                  self.channel_manifest.format(id=channel_info["id"], type="m3u8")).items():
-            yield s
-
-    def _get_streams(self):
-        params = dict(parse_qsl(urlparse(self.url).query))
-        vod_id = params.get("vod")
-        match = _url_re.match(self.url)
-        channel = match.group("channel")
-
-        if vod_id:
-            self.logger.debug("Looking for VOD {0} from channel: {1}", vod_id, channel)
-            return self._get_vod_stream(vod_id)
-        else:
-            self.logger.debug("Looking for channel: {0}", channel)
-            return self._get_live_stream(channel)
-
-
-__plugin__ = Beam
diff --git a/src/streamlink/plugins/kanal7.py b/src/streamlink/plugins/kanal7.py
index d6677a74..2f9cc2d9 100644
--- a/src/streamlink/plugins/kanal7.py
+++ b/src/streamlink/plugins/kanal7.py
@@ -11,7 +11,7 @@ from streamlink.stream import HLSStream
 class Kanal7(Plugin):
     url_re = re.compile(r"https?://(?:www.)?kanal7.com/canli-izle")
     iframe_re = re.compile(r'iframe .*?src="(http://[^"]*?)"')
-    stream_re = re.compile(r'src="(http[^"]*?)"')
+    stream_re = re.compile(r'''tp_file\s+=\s+['"](http[^"]*?)['"]''')
 
     @classmethod
     def can_handle_url(cls, url):
diff --git a/src/streamlink/plugins/mixer.py b/src/streamlink/plugins/mixer.py
new file mode 100644
index 00000000..34b0ead8
--- /dev/null
+++ b/src/streamlink/plugins/mixer.py
@@ -0,0 +1,87 @@
+"""
+    Mixer API 1.0 documentation
+    https://dev.mixer.com/rest.html
+"""
+import re
+
+from streamlink import NoStreamsError
+from streamlink.compat import urlparse, parse_qsl, urljoin
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http, validate
+from streamlink.stream import HLSStream
+
+_url_re = re.compile(r"http(s)?://(\w+.)?mixer\.com/(?P<channel>[^/?]+)")
+
+
+class Mixer(Plugin):
+    api_url = "https://mixer.com/api/v1/{type}/{id}"
+
+    _vod_schema = validate.Schema(
+        {
+            "state": "AVAILABLE",
+            "vods": [{
+                "baseUrl": validate.url(),
+                "data": validate.any(None, {
+                    "Height": int
+                }),
+                "format": validate.text
+            }]
+        },
+        validate.get("vods"),
+        validate.filter(lambda x: x["format"] in ("raw", "hls")),
+        [validate.union({
+            "url": validate.get("baseUrl"),
+            "format": validate.get("format"),
+            "height": validate.all(validate.get("data"), validate.get("Height"))
+        })])
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return _url_re.match(url)
+
+    def _get_api_res(self, api_type, api_id):
+        try:
+            res = http.get(self.api_url.format(type=api_type, id=api_id))
+            return res
+        except Exception as e:
+            if "404" in str(e):
+                self.logger.error("invalid {0} - {1}".format(api_type, api_id))
+            elif "429" in str(e):
+                self.logger.error("Too Many Requests, API rate limit exceeded.")
+            raise NoStreamsError(self.url)
+
+    def _get_vod_stream(self, vod_id):
+        res = self._get_api_res("recordings", vod_id)
+
+        for sdata in http.json(res, schema=self._vod_schema):
+            if sdata["format"] == "hls":
+                hls_url = urljoin(sdata["url"], "manifest.m3u8")
+                yield "{0}p".format(sdata["height"]), HLSStream(self.session, hls_url)
+
+    def _get_live_stream(self, channel):
+        res = self._get_api_res("channels", channel)
+
+        channel_info = http.json(res)
+        if not channel_info["online"]:
+            return
+
+        user_id = channel_info["id"]
+        hls_url = self.api_url.format(type="channels", id="{0}/manifest.m3u8".format(user_id))
+        for s in HLSStream.parse_variant_playlist(self.session, hls_url).items():
+            yield s
+
+    def _get_streams(self):
+        params = dict(parse_qsl(urlparse(self.url).query))
+        vod_id = params.get("vod")
+        match = _url_re.match(self.url)
+        channel = match.group("channel")
+
+        if vod_id:
+            self.logger.debug("Looking for VOD {0} from channel: {1}", vod_id, channel)
+            return self._get_vod_stream(vod_id)
+        else:
+            self.logger.debug("Looking for channel: {0}", channel)
+            return self._get_live_stream(channel)
+
+
+__plugin__ = Mixer
diff --git a/win32/streamlinkrc b/win32/streamlinkrc
index 39016668..a6d4fd96 100644
--- a/win32/streamlinkrc
+++ b/win32/streamlinkrc
@@ -22,9 +22,6 @@
 #player="C:\Program Files (x86)\MPC-HC\mpc-hc.exe"
 #player="C:\Program Files\MPC-HC\mpc-hc64.exe"
 
-# MPlayer2
-#player=C:\mplayer2\mplayer2.exe -cache 4096
-
 
 # Use this if you want to transport the stream to the player via a named pipe.
 #player-fifo


Test patch:
diff --git a/tests/test_plugin_mixer.py b/tests/test_plugin_mixer.py
new file mode 100644
index 00000000..9c437dda
--- /dev/null
+++ b/tests/test_plugin_mixer.py
@@ -0,0 +1,15 @@
+import unittest
+
+from streamlink.plugins.mixer import Mixer
+
+
+class TestPluginMixer(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Mixer.can_handle_url('https://mixer.com/Minecraft?vod=11250002'))
+        self.assertTrue(Mixer.can_handle_url('https://mixer.com/Minecraft'))
+        self.assertTrue(Mixer.can_handle_url('https://mixer.com/Monstercat?vod=11030270'))
+        self.assertTrue(Mixer.can_handle_url('https://mixer.com/Monstercat'))
+
+        # shouldn't match
+        self.assertFalse(Mixer.can_handle_url('https://mixer.com'))