Repository: streamlink/streamlink
Pull request #706 (2017-03-15 01:48:37)

plugins.douyutv: support vod


Patch:
diff --git a/docs/install.rst b/docs/install.rst
index 3a3362a0..fc30d34a 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -9,9 +9,9 @@ Linux and BSD packages
 ==================================== ===========================================
 Distribution                         Installing
 ==================================== ===========================================
-`Arch Linux (aur)`_                  .. code-block:: console
+`Arch Linux`_                        .. code-block:: console
 
-                                        # pacaur -S streamlink
+                                        # pacman -S streamlink
 
 `Arch Linux (aur, git)`_             .. code-block:: console
 
@@ -42,7 +42,7 @@ Distribution                         Installing
                                         # xbps-install streamlink
 ==================================== ===========================================
 
-.. _Arch Linux (aur): https://aur.archlinux.org/packages/streamlink/
+.. _Arch Linux: https://www.archlinux.org/packages/community/any/streamlink/
 .. _Arch Linux (aur, git): https://aur.archlinux.org/packages/streamlink-git/
 .. _Fedora: https://apps.fedoraproject.org/packages/python-streamlink
 .. _Gentoo Linux: https://packages.gentoo.org/package/net-misc/streamlink
@@ -78,7 +78,8 @@ Package maintainers
 ==================================== ===========================================
 Distribution/Platform                Maintainer
 ==================================== ===========================================
-Arch                                 Josip Ponjavic <josipponjavic at gmail.com>
+Arch                                 Giancarlo Razzolini <grazzolini at archlinux.org>
+Arch (aur, git)                      Josip Ponjavic <josipponjavic at gmail.com>
 Chocolatey                           Scott Walters <me at scowalt.com>
 Fedora                               Mohamed El Morabity <melmorabity at fedoraproject.org>
 Gentoo                               soredake <fdsfgs at krutt.org>
@@ -168,6 +169,17 @@ Name                                 Notes
                                      audio and video streams, eg. YouTube 1080p+
 ==================================== ===========================================
 
+Using pycrypto and pycountry
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+With these two environment variables it is possible to use `pycrypto`_ instead of
+`pycryptodome`_ and `pycountry`_ instead of `iso-639`_ and `iso3166`_.
+
+.. code-block:: console
+
+    $ export STREAMLINK_USE_PYCRYPTO="true"
+    $ export STREAMLINK_USE_PYCOUNTRY="true"
+
 .. _Python: http://python.org/
 .. _python-setuptools: http://pypi.python.org/pypi/setuptools
 .. _python-argparse: http://pypi.python.org/pypi/argparse
@@ -175,6 +187,8 @@ Name                                 Notes
 .. _python-requests: http://python-requests.org/
 .. _python-singledispatch: http://pypi.python.org/pypi/singledispatch
 .. _RTMPDump: http://rtmpdump.mplayerhq.hu/
+.. _pycountry: https://pypi.python.org/pypi/pycountry
+.. _pycrypto: https://www.dlitz.net/software/pycrypto/
 .. _pycryptodome: https://pycryptodome.readthedocs.io/en/latest/
 .. _python-librtmp: https://github.com/chrippa/python-librtmp
 .. _ffmpeg: https://www.ffmpeg.org/
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index e3e4dfe2..bce36dbc 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -33,12 +33,17 @@ beattv              be-at.tv             Yes   Yes   Playlist not implemented ye
 bigo                - live.bigo.tv       Yes   --
                     - bigoweb.co
 bilibili            live.bilibili.com    Yes   ?
-bliptv              blip.tv              --    Yes
 bongacams           bongacams.com        Yes   No    Only RTMP streams are available.
 btv                 btv.bg               Yes   No    Requires login, and geo-restricted to Bulgaria.
+camsoda             camsoda.com          Yes   No
 canalplus           - canalplus.fr       Yes   Yes   Streams may be geo-restricted to France.
                     - c8.fr
                     - cstar.fr
+canlitv             - canlitv.com        Yes   --
+                    - canlitv.life
+                    - canlitvlive.co
+                    - canlitvlive.live
+                    - ecanlitvizle.net
 cdnbg               - tv.bnt.bg          Yes   No    Streams may be geo-restricted to Bulgaria.
                     - bgonair.bg
                     - kanal3.bg
@@ -71,7 +76,8 @@ dogus               - startv.com.tr      Yes   No
                     - ntv.com.tr
                     - eurostartv.com.tr
 dommune             dommune.com          Yes   --
-douyutv             douyutv.com          Yes   --
+douyutv             - douyu.com          Yes   Yes
+                    - v.douyu.com
 dplay               - dplay.se           --    Yes   Streams may be geo-restricted.
                                                      Only non-premium streams currently supported.
                     - dplay.no
@@ -87,7 +93,7 @@ foxtr               fox.com.tr           Yes   No
 funimationnow       - funimation.com     --    Yes
                     - funimationnow.uk
 furstream           furstre.am           Yes   No
-gaminglive          gaminglive.tv        Yes   Yes
+garena              garena.live          Yes   --
 gomexp              gomexp.com           Yes   No
 goodgame            goodgame.ru          Yes   No    Only HLS streams are available.
 gulli               replay.gulli.fr      Yes   Yes   Streams may be geo-restricted to France.
@@ -98,15 +104,12 @@ huya                huya.com             Yes   No    Temporarily only HLS stream
 ine                 ine.com              ---   Yes
 itvplayer           itv.com/itvplayer    Yes   Yes   Streams may be geo-restricted to Great Britain.
 kanal7              kanal7.com           Yes   No
-letontv             leton.tv             Yes   --
 livecoding          livecoding.tv        Yes   --
 liveme              liveme.com           Yes   --
-livestation         livestation.com      Yes   --
 livestream          new.livestream.com   Yes   --
 media_ccc_de        - media.ccc.de       Yes   Yes   Only mp4 and HLS are supported.
                     - streaming... [4]_
 mediaklikk          mediaklikk.hu        Yes   No    Streams may be geo-restricted to Hungary.
-meerkat             meerkatapp.co        Yes   --
 mips                mips.tv              Yes   --    Requires rtmpdump with K-S-V patches.
 mitele              mitele.es            Yes   No    Streams may be geo-restricted to Spain.
 mlgtv               mlg.tv               Yes   --
@@ -126,9 +129,9 @@ pandatv             panda.tv             Yes   ?
 periscope           periscope.tv         Yes   Yes   Replay/VOD is supported.
 picarto             picarto.tv           Yes   --
 playtv              playtv.fr            Yes   --    Streams may be geo-restricted to France.
-pluzz               pluzz.francetv.fr    Yes   Yes   Streams may be geo-restricted to France, Andorra and Monaco.
-                    ludo.fr
-                    zouzous.fr
+pluzz               - pluzz.francetv.fr  Yes   Yes   Streams may be geo-restricted to France, Andorra and Monaco.
+                    - ludo.fr
+                    - zouzous.fr
 powerapp            powerapp.com.tr      Yes   No
 raiplay             raiplay.it           Yes   No    Most streams are geo-restricted to Italy.
 rtlxl               rtlxl.nl             No    Yes   Streams may be geo-restricted to The Netherlands. Livestreams not supported.
@@ -180,7 +183,7 @@ tv4play             - tv4play.se         Yes   Yes   Streams may be geo-restrict
 tv8                 tv8.com.tr           Yes   No
 tv8cat              tv8.cat              Yes   No    Streams may be geo-restricted to Spain/Catalunya.
 tv360               tv360.com.tr         Yes   No
-tvcatchup           - tvcatchup.com      Yes   No    Streams may be geo-restricted to Great Britain.
+tvcatchup           tvcatchup.com        Yes   No    Streams may be geo-restricted to Great Britain.
 tvplayer            tvplayer.com         Yes   No    Streams may be geo-restricted to Great Britain. Premium streams are not supported.
 tvrby               tvr.by               Yes   No    Streams may be geo-restricted to Belarus.
 tvrplus             tvrplus.ro           Yes   No    Streams may be geo-restricted to Romania.
@@ -191,9 +194,7 @@ vaughnlive          - vaughnlive.tv      Yes   --
                     - breakers.tv
                     - instagib.tv
                     - vapers.tv
-veetle              veetle.com           Yes   Yes
 vgtv                vgtv.no              Yes   Yes
-viagame             viagame.com
 viasat              - tv3play.se         Yes   Yes   Streams may be geo-restricted.
                     - tv3play.no
                     - tv3play.dk
@@ -224,4 +225,4 @@ zhanqitv            zhanqi.tv            Yes   No
 .. [2] streamingvideoprovider.co.uk
 .. [3] original.livestream.com
 .. [4] streaming.media.ccc.de
-.. [5] mediathek.daserste.de
\ No newline at end of file
+.. [5] mediathek.daserste.de
diff --git a/setup.py b/setup.py
index 1b85eae7..5768a0d3 100644
--- a/setup.py
+++ b/setup.py
@@ -25,8 +25,12 @@ if version_info[0] == 2 or (version_info[0] == 3 and version_info[1] < 4):
 
 deps.append("requests>=2.2,!=2.12.0,!=2.12.1,<3.0")
 
-# this version of pycryptodome is known to work and has a Windows wheel for py2.7, py3.3-3.6
-deps.append("pycryptodome>=3.4.3,<4")
+# for encrypted streams
+if environ.get("STREAMLINK_USE_PYCRYPTO"):
+    deps.append("pycrypto")
+else:
+    # this version of pycryptodome is known to work and has a Windows wheel for py2.7, py3.3-3.6
+    deps.append("pycryptodome>=3.4.3,<4")
 
 # shutil.get_terminal_size and which were added in Python 3.3
 if version_info[0] == 2:
diff --git a/src/streamlink/plugins/app17.py b/src/streamlink/plugins/app17.py
index b1e996ec..d0a17598 100644
--- a/src/streamlink/plugins/app17.py
+++ b/src/streamlink/plugins/app17.py
@@ -3,7 +3,7 @@ import re
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import http, validate, useragents
 from streamlink.plugin.api.utils import parse_json
-from streamlink.stream import HLSStream, RTMPStream
+from streamlink.stream import HLSStream, RTMPStream, HTTPStream
 
 API_URL = "https://api-dsa.17app.co/api/v1/liveStreams/isUserOnLiveStream"
 ROOM_URL = "http://17app.co/share/live/{0}"
@@ -58,11 +58,14 @@ class App17(Plugin):
             return
 
         url = _rtmp_re.search(res.text).group(1)
-        stream = RTMPStream(self.session, {
-                "rtmp": url,
-                "live": True
-                })
-        yield "live", stream
+        if 'rtmp:' in url:
+            stream = RTMPStream(self.session, {
+                    "rtmp": url,
+                    "live": True
+                    })
+            yield "live", stream
+        else:
+            yield "live", HTTPStream(self.session, url)
 
         prefix = url.replace("rtmp:", "http:").replace(".flv", ".m3u8")
         if '.m3u8' not in prefix:
@@ -71,8 +74,7 @@ class App17(Plugin):
                 yield stream
         else:
             url = prefix
-            stream = HLSStream(self.session, url)
-            yield "live", stream
+            yield "live", HLSStream(self.session, url)
 
 
 __plugin__ = App17
diff --git a/src/streamlink/plugins/bbciplayer.py b/src/streamlink/plugins/bbciplayer.py
index 37c0213b..d4c37ad9 100644
--- a/src/streamlink/plugins/bbciplayer.py
+++ b/src/streamlink/plugins/bbciplayer.py
@@ -9,6 +9,7 @@ from streamlink.plugin import Plugin
 from streamlink.plugin.api import http
 from streamlink.plugin.api import validate
 from streamlink.stream import HDSStream
+from streamlink.stream import HLSStream
 from streamlink.utils import parse_xml, parse_json
 
 
@@ -24,12 +25,19 @@ class BBCiPlayer(Plugin):
     swf_url = "http://emp.bbci.co.uk/emp/SMPf/1.18.3/StandardMediaPlayerChromelessFlash.swf"
     hash = base64.b64decode(b"N2RmZjc2NzFkMGM2OTdmZWRiMWQ5MDVkOWExMjE3MTk5MzhiOTJiZg==")
     api_url = ("http://open.live.bbc.co.uk/mediaselector/5/select/"
-               "version/2.0/mediaset/pc/vpid/{vpid}/atk/{vpid_hash}/asn/1/")
+               "version/2.0/mediaset/{platform}/vpid/{vpid}/atk/{vpid_hash}/asn/1/")
+    platforms = ("pc", "iptv-all")
+
     mediaselector_schema = validate.Schema(
         validate.transform(partial(parse_xml, ignore_ns=True)),
-        validate.xml_findall(".//media[@kind='video']//connection[@transferFormat='hds']"),
-        [validate.all(validate.getattr("attrib"), validate.get("href"))],
-        validate.transform(lambda x: list(set(x)))  # unique
+        validate.union({
+            "hds": validate.xml_findall(".//media[@kind='video']//connection[@transferFormat='hds']"),
+            "hls": validate.xml_findall(".//media[@kind='video']//connection[@transferFormat='hls']")
+        }),
+        {validate.text: validate.all(
+            [validate.all(validate.getattr("attrib"), validate.get("href"))],
+            validate.transform(lambda x: list(set(x)))  # unique
+        )}
     )
 
     @classmethod
@@ -53,9 +61,15 @@ class BBCiPlayer(Plugin):
         return m and m.group(1)
 
     def mediaselector(self, vpid):
-        url = self.api_url.format(vpid=vpid, vpid_hash=self._hash_vpid(vpid))
-        stream_urls = http.get(url, schema=self.mediaselector_schema)
-        return stream_urls
+        for platform in self.platforms:
+            url = self.api_url.format(vpid=vpid, vpid_hash=self._hash_vpid(vpid), platform=platform)
+            stream_urls = http.get(url, schema=self.mediaselector_schema)
+            for surl in stream_urls.get("hls"):
+                for s in HLSStream.parse_variant_playlist(self.session, surl).items():
+                    yield s
+            for surl in stream_urls.get("hds"):
+                for s in HDSStream.parse_manifest(self.session, surl).items():
+                    yield s
 
     def _get_streams(self):
         m = self.url_re.match(self.url)
@@ -67,10 +81,8 @@ class BBCiPlayer(Plugin):
             vpid = self.find_vpid(self.url)
             if vpid:
                 self.logger.debug("Found VPID: {0}", vpid)
-                s = self.mediaselector(vpid)
-                for url in s:
-                    for s in HDSStream.parse_manifest(self.session, url).items():
-                        yield s
+                for s in self.mediaselector(vpid):
+                    yield s
             else:
                 self.logger.error("Could not find VPID for episode {0}", episode_id)
         elif channel_name:
@@ -78,9 +90,8 @@ class BBCiPlayer(Plugin):
             tvip = self.find_tvip(self.url)
             if tvip:
                 self.logger.debug("Found TVIP: {0}", tvip)
-                s = self.mediaselector(tvip)
-                for url in s:
-                    for s in HDSStream.parse_manifest(self.session, url).items():
-                        yield s
+                for s in self.mediaselector(tvip):
+                    yield s
+
 
 __plugin__ = BBCiPlayer
diff --git a/src/streamlink/plugins/bigo.py b/src/streamlink/plugins/bigo.py
index 98101c78..365d38cd 100644
--- a/src/streamlink/plugins/bigo.py
+++ b/src/streamlink/plugins/bigo.py
@@ -47,7 +47,7 @@ class BigoStream(Stream):
 
 
 class Bigo(Plugin):
-    _url_re = re.compile(r"https?://(live.bigo.tv/\d+|bigoweb.co/show/\d+)")
+    _url_re = re.compile(r"https?://(?:www\.)?(bigo\.tv/\d+|bigoweb\.co/show/\d+)")
     _flashvars_re = flashvars = re.compile(
         r'''^\s*(?<!<!--)<param.*value="tmp=(\d+)&channel=(\d+)&srv=(\d+\.\d+\.\d+\.\d+)&port=(\d+)"''',
         re.M)
diff --git a/src/streamlink/plugins/bliptv.py b/src/streamlink/plugins/bliptv.py
deleted file mode 100644
index 6b252398..00000000
--- a/src/streamlink/plugins/bliptv.py
+++ /dev/null
@@ -1,74 +0,0 @@
-import re
-
-from streamlink.plugin import Plugin, PluginError
-from streamlink.plugin.api import http
-from streamlink.stream import HTTPStream
-
-_url_re = re.compile(r"(http(s)?://)?blip.tv/.*-(?P<videoid>\d+)")
-VIDEO_GET_URL = 'http://player.blip.tv/file/get/{0}'
-SINGLE_VIDEO_URL = re.compile(r'.*\.((mp4)|(mov)|(m4v)|(flv))')
-
-QUALITY_WEIGHTS = {
-    "ultra": 1080,
-    "high": 720,
-    "medium": 480,
-    "low": 240,
-}
-
-QUALITY_WEIGHTS_ULTRA = re.compile(r'ultra+_(?P<level>\d+)')
-
-
-def get_quality_dict(quality_list):
-    quality_list.sort()
-    quality_dict = {}
-    i = 0
-    for i, bitrate in enumerate(quality_list):
-        if i == 0:
-            quality_dict['%i' % bitrate] = 'low'
-        elif i == 1:
-            quality_dict['%i' % bitrate] = 'medium'
-        elif i == 2:
-            quality_dict['%i' % bitrate] = 'high'
-        elif i == 3:
-            quality_dict['%i' % bitrate] = 'ultra'
-        else:
-            quality_dict['%i' % bitrate] = 'ultra+_%i' % (i - 3)
-    return quality_dict
-
-
-class bliptv(Plugin):
-    @classmethod
-    def can_handle_url(cls, url):
-        return _url_re.match(url)
-
-    @classmethod
-    def stream_weight(cls, key):
-        match_ultra = QUALITY_WEIGHTS_ULTRA.match(key)
-        if match_ultra:
-            ultra_level = int(match_ultra.group('level'))
-            return 1080 * (ultra_level + 1), "bliptv"
-        weight = QUALITY_WEIGHTS.get(key)
-        if weight:
-            return weight, "bliptv"
-        return Plugin.stream_weight(key)
-
-    def _get_streams(self):
-        match = _url_re.match(self.url)
-        videoid = match.group('videoid')
-        get_return = http.get(VIDEO_GET_URL.format(videoid))
-        json_decode = http.json(get_return)
-        streams = {}
-        quality_list = []
-        for stream in json_decode:
-            if SINGLE_VIDEO_URL.match(stream['direct_url']):
-                quality_list.append(int(stream['video_bitrate']))
-        if len(quality_list) == 0:
-            return
-        quality_dict = get_quality_dict(quality_list)
-        for stream in json_decode:
-            if SINGLE_VIDEO_URL.match(stream['direct_url']):
-                streams[quality_dict[stream['video_bitrate']]] = HTTPStream(self.session, stream['direct_url'])
-        return streams
-
-
-__plugin__ = bliptv
diff --git a/src/streamlink/plugins/camsoda.py b/src/streamlink/plugins/camsoda.py
new file mode 100644
index 00000000..b91f3841
--- /dev/null
+++ b/src/streamlink/plugins/camsoda.py
@@ -0,0 +1,96 @@
+import random
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.plugin.api import validate
+from streamlink.stream import HLSStream
+
+_url_re = re.compile(r"http(s)?://(www\.)?camsoda\.com/(?P<username>[^\"\']+)")
+
+_api_user_schema = validate.Schema(
+    {
+        "status": validate.any(int, validate.text),
+        validate.optional("user"): {
+            "online": validate.any(int, validate.text),
+            "chatstatus": validate.text,
+        }
+    }
+)
+
+_api_video_schema = validate.Schema(
+    {
+        "token": validate.text,
+        "app": validate.text,
+        "edge_servers": [validate.text],
+        "private_servers": [validate.text],
+        "mjpeg_server": validate.text,
+        "stream_name": validate.text
+    }
+)
+
+
+class Camsoda(Plugin):
+    API_URL_USER = "https://www.camsoda.com/api/v1/user/{0}"
+    API_URL_VIDEO = "https://www.camsoda.com/api/v1/video/vtoken/{0}?username=guest_{1}"
+    HLS_URL_VIDEO = "https://{server}/{app}/mp4:{stream_name}_mjpeg/playlist.m3u8?token={token}"
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return _url_re.match(url)
+
+    def _stream_status(self, data_user):
+        invalid_username = data_user["status"] is False
+        if invalid_username:
+            self.logger.info("No validate username found for {0}".format(self.url))
+            return
+
+        is_offline = data_user["user"]["online"] is False
+        if is_offline:
+            self.logger.info("This stream is currently offline")
+            return
+
+        return True
+
+    def _get_api_user(self, username):
+        res = http.get(self.API_URL_USER.format(username))
+        data_user = http.json(res, schema=_api_user_schema)
+        return data_user
+
+    def _get_api_video(self, username):
+        res = http.get(self.API_URL_VIDEO.format(username, str(random.randint(1000, 99999))))
+        data_video = http.json(res, schema=_api_video_schema)
+        return data_video
+
+    def _get_hls_url(self, data_user, data_video):
+        is_edge = data_user["user"]["chatstatus"] == "online"
+        is_priv = data_user["user"]["chatstatus"] == "private"
+        if is_edge:
+            server = data_video["edge_servers"][0]
+        elif is_priv:
+            server = data_video["private_servers"][0]
+        else:
+            server = data_video["mjpeg_server"]
+
+        hls_url = self.HLS_URL_VIDEO.format(server=server, app=data_video["app"], stream_name=data_video["stream_name"], token=data_video["token"])
+        return hls_url
+
+    def _get_streams(self):
+        match = _url_re.match(self.url)
+        username = match.group("username")
+        username = username.replace("/", "")
+
+        data_user = self._get_api_user(username)
+        stream_status = self._stream_status(data_user)
+
+        if stream_status:
+            data_video = self._get_api_video(username)
+            hls_url = self._get_hls_url(data_user, data_video)
+
+            try:
+                for s in HLSStream.parse_variant_playlist(self.session, hls_url).items():
+                    yield s
+            except IOError as err:
+                self.logger.error("Error parsing stream: {0}", err)
+
+__plugin__ = Camsoda
diff --git a/src/streamlink/plugins/canlitv.py b/src/streamlink/plugins/canlitv.py
new file mode 100644
index 00000000..4ff9877b
--- /dev/null
+++ b/src/streamlink/plugins/canlitv.py
@@ -0,0 +1,61 @@
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.plugin.api import useragents
+from streamlink.stream import HLSStream
+
+EMBED_URL_1 = "http://www.canlitv.life/kanallar.php?kanal={0}"
+EMBED_URL_2 = "http://www.ecanlitvizle.net/embed.php?kanal={0}"
+
+_m3u8_re = re.compile(r"file:(?:\s+)?(?:\'|\")(?P<url>[^\"']+)(?:\'|\")")
+_url_re = re.compile(r"""http(s)?://(?:www\.)?(?P<domain>
+    canlitv\.(com|life)
+    |
+    canlitvlive\.(co|live)
+    |
+    ecanlitvizle\.net
+    )
+    /(izle/|(?:onizleme|tv)\.php\?kanal=)?
+    (?P<channel>[\w\-\=]+)""", re.VERBOSE)
+
+
+class Canlitv(Plugin):
+    @classmethod
+    def can_handle_url(cls, url):
+        return _url_re.match(url)
+
+    def _get_streams(self):
+        match = _url_re.match(self.url)
+        channel = match.group("channel")
+        domain = match.group("domain")
+
+        headers = {
+            "Referer": self.url,
+            "User-Agent": useragents.FIREFOX
+        }
+
+        if domain == "canlitv.life":
+            res = http.get(EMBED_URL_1.format(channel), headers=headers)
+        elif domain == "ecanlitvizle.net":
+            res = http.get(EMBED_URL_2.format(channel), headers=headers)
+        else:
+            res = http.get(self.url, headers=headers)
+
+        url_match = _m3u8_re.search(res.text)
+
+        if url_match:
+            hls_url = url_match.group("url")
+
+            self.logger.debug("Found URL: {0}".format(hls_url))
+
+            try:
+                s = []
+                for s in HLSStream.parse_variant_playlist(self.session, hls_url).items():
+                    yield s
+                if not s:
+                    yield "live", HLSStream(self.session, hls_url)
+            except IOError as err:
+                self.logger.error("Failed to extract streams: {0}", err)
+
+__plugin__ = Canlitv
diff --git a/src/streamlink/plugins/douyutv.py b/src/streamlink/plugins/douyutv.py
index b085089f..fdb413d4 100644
--- a/src/streamlink/plugins/douyutv.py
+++ b/src/streamlink/plugins/douyutv.py
@@ -6,25 +6,32 @@ from requests.adapters import HTTPAdapter
 
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import http, validate, useragents
-from streamlink.stream import HTTPStream, HLSStream
+from streamlink.stream import HTTPStream, HLSStream, RTMPStream
 
 #new API and key from https://gist.github.com/spacemeowx2/629b1d131bd7e240a7d28742048e80fc by spacemeowx2
 MAPI_URL = "https://m.douyu.com/html5/live?roomId={0}"
-LAPI_URL = "http://coapi.douyucdn.cn/lapi/live/thirdPart/getPlay/{0}?rate={1}"
-
+LAPI_URL = "https://coapi.douyucdn.cn/lapi/live/thirdPart/getPlay/{0}?cdn={1}&rate={2}"
 LAPI_SECRET = "9TUk5fjjUjg9qIMH3sdnh"
-
+VAPI_URL = "https://vmobile.douyu.com/video/getInfo?vid={0}"
 SHOW_STATUS_ONLINE = 1
 SHOW_STATUS_OFFLINE = 2
 STREAM_WEIGHTS = {
     "low": 540,
     "medium": 720,
     "source": 1080
-    }
+}
 
 _url_re = re.compile(r"""
-    http(s)?://(www\.)?douyu.com
-    /(?P<channel>[^/]+)
+    http(s)?://
+    (?:
+        (?P<subdomain>.+)
+        \.
+    )?
+    douyu.com/
+    (?:
+        show/(?P<vid>[^/&?]+)|
+        (?P<channel>[^/&?]+)
+    )
 """, re.VERBOSE)
 
 _room_id_re = re.compile(r'"room_id\\*"\s*:\s*(\d+),')
@@ -78,6 +85,15 @@ _lapi_schema = validate.Schema(
     validate.get("data")
 )
 
+_vapi_schema = validate.Schema(
+    {
+        "data": validate.any(None, {
+            "video_url": validate.text
+        })
+    },
+    validate.get("data")
+)
+
 
 class Douyutv(Plugin):
     @classmethod
@@ -90,30 +106,40 @@ class Douyutv(Plugin):
             return STREAM_WEIGHTS[stream], "douyutv"
         return Plugin.stream_weight(stream)
 
-    #quality:
-    # 0:source 2:medium 1:low
-    def _get_room_json(self, channel, quality):
+    def _get_room_json(self, channel, rate):
+        cdn = "ws" #cdns: ["ws", "tct", "ws2", "dl"]
         ts = int(time.time())
-        sign = hashlib.md5("lapi/live/thirdPart/getPlay/{0}?aid=pcclient&rate={1}&time={2}{3}".format(channel, quality, ts, LAPI_SECRET).encode('ascii')).hexdigest()
-        data = {
+        sign = hashlib.md5("lapi/live/thirdPart/getPlay/{0}?aid=pcclient&cdn={1}&rate={2}&time={3}{4}".format(channel, cdn, rate, ts, LAPI_SECRET).encode("ascii")).hexdigest()
+        headers = {
             "auth": sign,
             "time": str(ts),
-            "aid": 'pcclient'
+            "aid": "pcclient"
         }
-
-        res = http.get(LAPI_URL.format(channel, quality), headers=data)
+        res = http.get(LAPI_URL.format(channel, cdn, rate), headers=headers)
         room = http.json(res, schema=_lapi_schema)
         return room
 
     def _get_streams(self):
         match = _url_re.match(self.url)
-        channel = match.group("channel")
+        subdomain = match.group("subdomain")
 
-        http.headers.update({'User-Agent': useragents.CHROME})
         http.verify = False
         http.mount('https://', HTTPAdapter(max_retries=99))
 
+        if subdomain == 'v':
+            vid = match.group("vid")
+            headers = {
+                "User-Agent": useragents.ANDROID,
+                "X-Requested-With": "XMLHttpRequest"
+            }
+            res = http.get(VAPI_URL.format(vid), headers=headers)
+            room = http.json(res, schema=_vapi_schema)
+            yield "source", HLSStream(self.session, room["video_url"])
+            return
+
         #Thanks to @ximellon for providing method.
+        channel = match.group("channel")
+        http.headers.update({'User-Agent': useragents.CHROME})
         try:
             channel = int(channel)
         except ValueError:
@@ -131,15 +157,20 @@ class Douyutv(Plugin):
             self.logger.info("Stream currently unavailable.")
             return
 
-        room_source = self._get_room_json(channel, 0)
-        yield "source", HTTPStream(self.session, room_source['live_url'])
-        yield "source", HLSStream(self.session, room_source['hls_url'])
+        rate = [0, 2, 1]
+        quality = ['source', 'medium', 'low']
+        for i in range(0, 3, 1):
+            room = self._get_room_json(channel, rate[i])
+            url = room["live_url"]
+            if 'rtmp:' in url:
+                stream = RTMPStream(self.session, {
+                        "rtmp": url,
+                        "live": True
+                        })
+                yield quality[i], stream
+            else:
+                yield quality[i], HTTPStream(self.session, url)
+            yield quality[i], HLSStream(self.session, room["hls_url"])
 
-        room_medium = self._get_room_json(channel, 2)
-        yield "medium", HTTPStream(self.session, room_medium['live_url'])
-        yield "medium", HLSStream(self.session, room_medium['hls_url'])
 
-        room_low = self._get_room_json(channel, 1)
-        yield "low", HTTPStream(self.session, room_low['live_url'])
-        yield "low", HLSStream(self.session, room_low['hls_url'])
 __plugin__ = Douyutv
diff --git a/src/streamlink/plugins/gaminglive.py b/src/streamlink/plugins/gaminglive.py
deleted file mode 100644
index 59fbdf9e..00000000
--- a/src/streamlink/plugins/gaminglive.py
+++ /dev/null
@@ -1,111 +0,0 @@
-import re
-
-from streamlink.plugin import Plugin
-from streamlink.plugin.api import http, validate
-from streamlink.stream import RTMPStream
-
-SWF_URL = "http://www.gaminglive.tv/lib/flowplayer/flash/flowplayer.commercial-3.2.18.swf"
-API_URL = "http://api.gaminglive.tv/{0}/{1}"
-VOD_RTMP_URL = "rtmp://gamingfs.fplive.net/gaming/{0}/"
-QUALITY_WEIGHTS = {
-    "source": 5,
-    "live": 5,
-    "1080": 4,
-    "720": 3,
-    "480": 2,
-    "medium": 2,
-    "360": 1,
-    "low": 1
-}
-
-_url_re = re.compile(r"""
-    http(s)?://(\w+\.)?gaminglive\.tv
-    /(?P<type>channels|videos)/(?P<name>[^/]+)
-""", re.VERBOSE)
-_quality_re = re.compile(r"[^/]+-(?P<quality>[^/]+)")
-
-_channel_schema = validate.Schema(
-    {
-        validate.optional("state"): {
-            "stream": {
-                "qualities": [validate.text],
-                "rootUrl": validate.url(scheme="rtmp")
-            }
-        }
-    },
-    validate.get("state")
-)
-
-_vod_schema = validate.Schema(
-    {
-        "name": validate.text,
-        "channel_slug": validate.text,
-        "title": validate.text,
-        "created_at": validate.transform(int)
-    },
-)
-
-
-class GamingLive(Plugin):
-    @classmethod
-    def can_handle_url(self, url):
-        return _url_re.match(url)
-
-    @classmethod
-    def stream_weight(cls, key):
-        weight = QUALITY_WEIGHTS.get(key)
-        if weight:
-            return weight, "gaminglive"
-
-        return Plugin.stream_weight(key)
-
-    def _get_quality(self, label):
-        match = _quality_re.match(label)
-        if match:
-            return match.group("quality")
-
-        return "live"
-
-    def _create_rtmp_stream(self, rtmp, playpath, live):
-        return RTMPStream(self.session, {
-            "rtmp": rtmp,
-            "playpath": playpath,
-            "pageUrl": self.url,
-            "swfVfy": SWF_URL,
-            "live": live
-        })
-
-    def _get_live_streams(self, name):
-        res = http.get(API_URL.format("channels", name))
-        json = http.json(res, schema=_channel_schema)
-        if not json:
-            return
-
-        streams = {}
-        for quality in json["stream"]["qualities"]:
-            streams[self._get_quality(quality)] = self._create_rtmp_stream(json["stream"]["rootUrl"], quality, True)
-
-        return streams
-
-    def _get_vod_streams(self, name):
-        res = http.get(API_URL.format("videos", name))
-        json = http.json(res, schema=_vod_schema)
-        if not json:
-            return
-
-        streams = {}
-        streams["source"] = self._create_rtmp_stream(VOD_RTMP_URL.format(json["channel_slug"]), json["name"], True)
-
-        return streams
-
-    def _get_streams(self):
-        match = _url_re.match(self.url)
-        type = match.group("type")
-
-        if type == "channels":
-            return self._get_live_streams(match.group("name"))
-        elif type == "videos":
-            return self._get_vod_streams(match.group("name"))
-
-
-__plugin__ = GamingLive
diff --git a/src/streamlink/plugins/garena.py b/src/streamlink/plugins/garena.py
new file mode 100644
index 00000000..8349ebc7
--- /dev/null
+++ b/src/streamlink/plugins/garena.py
@@ -0,0 +1,69 @@
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.plugin.api import validate
+from streamlink.stream import HLSStream
+
+_url_re = re.compile(r"https?\:\/\/garena\.live\/(?:(?P<channel_id>\d+)|(?P<alias>\w+))")
+
+
+class Garena(Plugin):
+    API_INFO = "https://garena.live/api/channel_info_get"
+    API_STREAM = "https://garena.live/api/channel_stream_get"
+
+    _info_schema = validate.Schema(
+        {
+            "reply": validate.any({
+                "channel_id": int,
+            }, None),
+            "result": validate.text
+        }
+    )
+    _stream_schema = validate.Schema(
+        {
+            "reply": validate.any({
+                "streams": [
+                    {
+                        "url": validate.text,
+                        "resolution": int,
+                        "bitrate": int,
+                        "format": int
+                    }
+                ]
+            }, None),
+            "result": validate.text
+        }
+    )
+
+    @classmethod
+    def can_handle_url(self, url):
+        return _url_re.match(url)
+
+    def _post_api(self, api, payload, schema):
+        res = http.post(api, json=payload)
+        data = http.json(res, schema=schema)
+
+        if data["result"] == "success":
+            post_data = data["reply"]
+            return post_data
+
+    def _get_streams(self):
+        match = _url_re.match(self.url)
+        if match.group("alias"):
+            payload = {"alias": match.group("alias")}
+            info_data = self._post_api(self.API_INFO, payload, self._info_schema)
+            channel_id = info_data["channel_id"]
+        elif match.group("channel_id"):
+            channel_id = int(match.group("channel_id"))
+
+        if channel_id:
+            payload = {"channel_id": channel_id}
+            stream_data = self._post_api(self.API_STREAM, payload, self._stream_schema)
+            for stream in stream_data["streams"]:
+                n = "{0}p".format(stream["resolution"])
+                if stream["format"] == 3:
+                    s = HLSStream(self.session, stream["url"])
+                    yield n, s
+
+__plugin__ = Garena
diff --git a/src/streamlink/plugins/letontv.py b/src/streamlink/plugins/letontv.py
deleted file mode 100644
index a2c15803..00000000
--- a/src/streamlink/plugins/letontv.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import re
-
-from streamlink.plugin import Plugin
-from streamlink.plugin.api import http, validate
-from streamlink.stream import RTMPStream
-
-PLAYER_URL = "http://leton.tv/player.php"
-SWF_URL = "http://files.leton.tv/jwplayer.flash.swf"
-
-_url_re = re.compile(r"""
-    http?://(\w+.)?leton.tv
-    (?:
-        /player\.php\?.*streampage=
-    )?
-    (?:
-        /broadcast/
-    )?
-    (?P<streampage>[^/?&]+)
-""", re.VERBOSE)
-_js_var_re = re.compile(r"var (?P<var>\w+)\s?=\s?'?(?P<value>[^;']+)'?;")
-_rtmp_re = re.compile(r"/(?P<app>[^/]+)/(?P<playpath>.+)")
-
-
-def _parse_server_ip(values):
-    octets = [
-        values["a"] / values["f"],
-        values["b"] / values["f"],
-        values["c"] / values["f"],
-        values["d"] / values["f"],
-    ]
-
-    return ".".join(str(int(octet)) for octet in octets)
-
-
-_schema = validate.Schema(
-    validate.transform(_js_var_re.findall),
-    validate.transform(dict),
-    {
-        "a": validate.transform(int),
-        "b": validate.transform(int),
-        "c": validate.transform(int),
-        "d": validate.transform(int),
-        "f": validate.transform(int),
-        "v_part": validate.text,
-    },
-    validate.union({
-        "server_ip": validate.transform(_parse_server_ip),
-        "path": validate.all(
-            validate.get("v_part"),
-            validate.transform(_rtmp_re.findall),
-            validate.get(0)
-        )
-    })
-)
-
-
-class LetOnTV(Plugin):
-    @classmethod
-    def can_handle_url(self, url):
-        return _url_re.match(url)
-
-    def _get_streams(self):
-        match = _url_re.match(self.url)
-        info = http.get(PLAYER_URL, params=match.groupdict(), schema=_schema)
-        if not info["path"]:
-            return
-
-        app, playpath = info["path"]
-        stream = RTMPStream(self.session, {
-            "rtmp": "rtmp://{0}/{1}".format(info["server_ip"], app),
-            "playpath": playpath,
-            "pageUrl": self.url,
-            "swfUrl": SWF_URL,
-            "live": True
-        })
-
-        return dict(live=stream)
-
-
-__plugin__ = LetOnTV
diff --git a/src/streamlink/plugins/livestation.py b/src/streamlink/plugins/livestation.py
deleted file mode 100644
index dfa7155f..00000000
--- a/src/streamlink/plugins/livestation.py
+++ /dev/null
@@ -1,87 +0,0 @@
-import re
-
-from streamlink.plugin import Plugin, PluginError, PluginOptions
-from streamlink.plugin.api import http, validate
-from streamlink.stream import HLSStream
-
-LOGIN_PAGE_URL = "http://www.livestation.com/en/users/new"
-LOGIN_POST_URL = "http://www.livestation.com/en/sessions.json"
-
-_csrf_token_re = re.compile(r"<meta content=\"([^\"]+)\" name=\"csrf-token\"")
-_hls_playlist_re = re.compile(r"<meta content=\"([^\"]+.m3u8)\" property=\"og:video\" />")
-_url_re = re.compile(r"http(s)?://(\w+\.)?livestation.com")
-
-_csrf_token_schema = validate.Schema(
-    validate.transform(_csrf_token_re.search),
-    validate.any(None, validate.get(1))
-)
-_hls_playlist_schema = validate.Schema(
-    validate.transform(_hls_playlist_re.search),
-    validate.any(
-        None,
-        validate.all(
-            validate.get(1),
-            validate.url(scheme="http", path=validate.endswith(".m3u8"))
-        )
-    )
-)
-_login_schema = validate.Schema({
-    "email": validate.text,
-    validate.optional("errors"): validate.all(
-        {
-            "base": [validate.text]
-        },
-        validate.get("base"),
-    )
-})
-
-
-class Livestation(Plugin):
-    options = PluginOptions({
-        "email": "",
-        "password": ""
-    })
-
-    @classmethod
-    def can_handle_url(self, url):
-        return _url_re.match(url)
-
-    def _authenticate(self, email, password):
-        csrf_token = http.get(LOGIN_PAGE_URL, schema=_csrf_token_schema)
-        if not csrf_token:
-            raise PluginError("Unable to find CSRF token")
-
-        data = {
-            "authenticity_token": csrf_token,
-            "channel_id": "",
-            "commit": "Login",
-            "plan_id": "",
-            "session[email]": email,
-            "session[password]": password,
-            "utf8": "\xE2\x9C\x93",  # Check Mark Character
-        }
-
-        res = http.post(LOGIN_POST_URL, data=data, acceptable_status=(200, 422))
-        result = http.json(res, schema=_login_schema)
-
-        errors = result.get("errors")
-        if errors:
-            errors = ", ".join(errors)
-            raise PluginError("Unable to authenticate: {0}".format(errors))
-
-        self.logger.info("Successfully logged in as {0}", result["email"])
-
-    def _get_streams(self):
-        login_email = self.options.get("email")
-        login_password = self.options.get("password")
-        if login_email and login_password:
-            self._authenticate(login_email, login_password)
-
-        hls_playlist = http.get(self.url, schema=_hls_playlist_schema)
-        if not hls_playlist:
-            return
-
-        return HLSStream.parse_variant_playlist(self.session, hls_playlist)
-
-
-__plugin__ = Livestation
diff --git a/src/streamlink/plugins/meerkat.py b/src/streamlink/plugins/meerkat.py
deleted file mode 100644
index 8e48fe19..00000000
--- a/src/streamlink/plugins/meerkat.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import re
-
-from streamlink.plugin import Plugin
-from streamlink.stream import HLSStream
-
-
-_url_re = re.compile(r"http(s)?://meerkatapp.co/(?P<user>[\w\-\=]+)/(?P<token>[\w\-]+)")
-
-
-class Meerkat(Plugin):
-    @classmethod
-    def can_handle_url(cls, url):
-        return _url_re.match(url)
-
-    def _get_streams(self):
-        match = _url_re.match(self.url)
-        if not match:
-            return
-
-        streams = {}
-        streams["live"] = HLSStream(self.session, "http://cdn.meerkatapp.co/broadcast/{0}/live.m3u8".format(match.group("token")))
-
-        return streams
-
-
-__plugin__ = Meerkat
diff --git a/src/streamlink/plugins/pluzz.py b/src/streamlink/plugins/pluzz.py
index 63f74160..db49cdc1 100644
--- a/src/streamlink/plugins/pluzz.py
+++ b/src/streamlink/plugins/pluzz.py
@@ -7,6 +7,7 @@ from streamlink.plugin import Plugin, PluginOptions
 from streamlink.plugin.api import http, validate
 from streamlink.stream import HDSStream, HLSStream, HTTPStream
 from streamlink.stream.ffmpegmux import MuxedStream
+from streamlink.utils import update_scheme
 
 
 class Pluzz(Plugin):
@@ -18,7 +19,7 @@ class Pluzz(Plugin):
     _pluzz_video_id_re = re.compile(r'id="current_video" href="http://.+?\.(?:francetv|francetelevisions)\.fr/(?:video/|\?id-video=)(?P<video_id>.+?)"')
     _other_video_id_re = re.compile(r'playlist: \[{.*?,"identity":"(?P<video_id>.+?)@(?P<catalogue>Ludo|Zouzous)"')
     _player_re = re.compile(r'src="(?P<player>//staticftv-a\.akamaihd\.net/player/jquery\.player.+?-[0-9a-f]+?\.js)"></script>')
-    _swf_re = re.compile(r'getUrl\("(?P<swf>/bower_components/player_flash/dist/FranceTVNVPVFlashPlayer\.akamai.+?\.swf)"\)')
+    _swf_re = re.compile(r'//staticftv-a\.akamaihd\.net/player/bower_components/player_flash/dist/FranceTVNVPVFlashPlayer\.akamai-[0-9a-f]+\.swf')
     _hds_pv_data_re = re.compile(r"~data=.+?!")
     _mp4_bitrate_re = re.compile(r'.*-(?P<bitrate>[0-9]+k)\.mp4')
 
@@ -102,11 +103,11 @@ class Pluzz(Plugin):
         match = self._player_re.search(res.text)
         swf_url = None
         if match is not None:
-            player_url = 'http:' + match.group('player')
+            player_url = update_scheme(self.url, match.group('player'))
             res = http.get(player_url)
             match = self._swf_re.search(res.text)
             if match is not None:
-                swf_url = os.path.dirname(player_url) + match.group('swf')
+                swf_url = update_scheme(self.url, match.group(0))
 
         res = http.get(self.API_URL.format(video_id, catalogue))
         videos = http.json(res, schema=self._api_schema)
diff --git a/src/streamlink/plugins/veetle.py b/src/streamlink/plugins/veetle.py
deleted file mode 100644
index 3a1c19b5..00000000
--- a/src/streamlink/plugins/veetle.py
+++ /dev/null
@@ -1,63 +0,0 @@
-import re
-
-from streamlink.compat import urlparse
-from streamlink.plugin import Plugin
-from streamlink.plugin.api import http, validate
-from streamlink.stream import FLVPlaylist, HTTPStream
-
-API_URL = "http://veetle.com/index.php/stream/ajaxStreamLocation/{0}/flash"
-
-_url_re = re.compile(r"""
-    http(s)?://(\w+\.)?veetle.com
-    (:?
-        /.*(v|view)/
-        (?P<channel>[^/]+/[^/&?]+)
-    )?
-""", re.VERBOSE)
-
-_schema = validate.Schema({
-    validate.optional("isLive"): bool,
-    "payload": validate.any(int, validate.url(scheme="http")),
-    "success": bool
-})
-
-
-class Veetle(Plugin):
-    @classmethod
-    def can_handle_url(self, url):
-        return _url_re.match(url)
-
-    def _get_streams(self):
-        self.url = http.resolve_url(self.url)
-        match = _url_re.match(self.url)
-        parsed = urlparse(self.url)
-        if parsed.fragment:
-            channel_id = parsed.fragment
-        elif parsed.path[:3] == '/v/':
-            channel_id = parsed.path.split('/')[-1]
-        else:
-            channel_id = match.group("channel")
-
-        if not channel_id:
-            return
-
-        channel_id = channel_id.lower().replace("/", "_")
-        res = http.get(API_URL.format(channel_id))
-        info = http.json(res, schema=_schema)
-
-        if not info["success"]:
-            return
-
-        if info.get("isLive"):
-            name = "live"
-        else:
-            name = "vod"
-
-        stream = HTTPStream(self.session, info["payload"])
-        # Wrap the stream in a FLVPlaylist to verify the FLV tags
-        stream = FLVPlaylist(self.session, [stream])
-
-        return {name: stream}
-
-
-__plugin__ = Veetle
diff --git a/src/streamlink/plugins/viagame.py b/src/streamlink/plugins/viagame.py
deleted file mode 100644
index 29bab733..00000000
--- a/src/streamlink/plugins/viagame.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""Plugin for Viasat's gaming site Viagame."""
-
-import re
-
-from streamlink.plugin.api import http, validate
-from streamlink.plugin.api.utils import parse_json
-from streamlink.plugin.api.support_plugin import viasat
-
-STREAM_API_URL = "http://playapi.mtgx.tv/v3/videos/stream/{0}"
-
-_embed_url_re = re.compile(
-    '<meta itemprop="embedURL" content="http://www.viagame.com/embed/video/([^"]+)"'
-)
-_store_data_re = re.compile(r"window.fluxData\s*=\s*JSON.parse\(\"(.+)\"\);")
-_url_re = re.compile(r"http(s)?://(www\.)?viagame.com/channels/.+")
-
-_store_schema = validate.Schema(
-    {
-        "initialStoresData": [{
-            "instanceName": validate.text,
-            "storeName": validate.text,
-            "initialData": validate.any(dict, list)
-        }]
-    },
-    validate.get("initialStoresData")
-)
-_match_store_schema = validate.Schema(
-    {
-        "match": {
-            "id": validate.text,
-            "type": validate.text,
-            "videos": [{
-                "id": validate.text,
-                "play_id": validate.text,
-            }]
-        }
-    },
-    validate.get("match")
-)
-
-
-class Viagame(viasat.Viasat):
-    @classmethod
-    def can_handle_url(cls, url):
-        return _url_re.match(url)
-
-    def _find_store(self, res, name):
-        match = _store_data_re.search(res.text)
-        if not match:
-            return
-
-        stores_data = parse_json(match.group(1).replace('\\"', '"'),
-                                 schema=_store_schema)
-        if not stores_data:
-            return
-
-        for store in filter(lambda s: s["instanceName"] == name, stores_data):
-            return store
-
-    def _find_video_id(self, res):
-        match = _embed_url_re.search(res.text)
-        if match:
-            return match.group(1)
-
-    def _find_stream_id(self):
-        res = http.get(self.url)
-        video_id = self._find_video_id(res)
-        match_store = self._find_store(res, "matchStore")
-        if not (video_id and match_store):
-            return
-
-        match_store = _match_store_schema.validate(match_store["initialData"])
-        for video in filter(lambda v: v["id"] == video_id, match_store["videos"]):
-            return video["play_id"]
-
-    def _get_streams(self):
-        stream_id = self._find_stream_id()
-        if not stream_id:
-            return
-
-        return self._extract_streams(stream_id)
-
-
-__plugin__ = Viagame
diff --git a/src/streamlink/utils/l10n.py b/src/streamlink/utils/l10n.py
index 1791446b..461fbecc 100644
--- a/src/streamlink/utils/l10n.py
+++ b/src/streamlink/utils/l10n.py
@@ -12,7 +12,9 @@ except ImportError:  # pragma: no cover
 
     PYCOUNTRY = True
 
-DEFAULT_LANGUAGE_CODE = "en_US"
+DEFAULT_LANGUAGE = "en"
+DEFAULT_COUNTRY = "US"
+DEFAULT_LANGUAGE_CODE = "{0}_{1}".format(DEFAULT_LANGUAGE, DEFAULT_COUNTRY)
 
 
 class Country(object):
@@ -116,8 +118,15 @@ class Localization(object):
     def language_code(self):
         return self._language_code
 
+    def _parse_locale_code(self, language_code):
+        parts = language_code.split("_", 1)
+        if len(parts) != 2 or len(parts[0]) != 2 or len(parts[1]) != 2:
+                raise LookupError("Invalid language code: {0}".format(language_code))
+        return self.get_language(parts[0]), self.get_country(parts[1])
+
     @language_code.setter
     def language_code(self, language_code):
+        is_system_locale = language_code is None
         if language_code is None:
             try:
                 language_code, _ = locale.getdefaultlocale()
@@ -125,16 +134,19 @@ class Localization(object):
                 language_code = None
             if language_code is None or language_code == "C":
                 # cannot be determined
-                language_code = DEFAULT_LANGUAGE_CODE
-
-        parts = language_code.split("_", 1)
+                language_code = DEFAULT_LANGUAGE
 
-        if len(parts) != 2 or len(parts[0]) != 2 or len(parts[1]) != 2:
-            raise LookupError("Invalid language code: {0}".format(language_code))
-
-        self._language_code = language_code
-        self.language = self.get_language(parts[0])
-        self.country = self.get_country(parts[1])
+        try:
+            self.language, self.country = self._parse_locale_code(language_code)
+            self._language_code = language_code
+        except LookupError:
+            if is_system_locale:
+                # If the system locale returns an invalid code, use the default
+                self.language = self.get_language(DEFAULT_LANGUAGE)
+                self.country = self.get_country(DEFAULT_COUNTRY)
+                self._language_code = DEFAULT_LANGUAGE_CODE
+            else:
+                raise
 
     def equivalent(self, language=None, country=None):
         equivalent = True
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index de4127a6..94018533 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -1063,21 +1063,6 @@ plugin.add_argument(
     region restrictions
     """
 )
-plugin.add_argument(
-    "--livestation-email",
-    metavar="EMAIL",
-    help="""
-    A Livestation account email to access restricted or premium
-    quality streams.
-    """
-)
-plugin.add_argument(
-    "--livestation-password",
-    metavar="PASSWORD",
-    help="""
-    A Livestation account password to use with --livestation-email.
-    """
-)
 plugin.add_argument(
     "--btv-username",
     metavar="USERNAME",
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 73d24932..db743137 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -827,14 +827,6 @@ def setup_plugin_options():
         streamlink.set_plugin_option("crunchyroll", "locale",
                                      args.crunchyroll_locale)
 
-    if args.livestation_email:
-        streamlink.set_plugin_option("livestation", "email",
-                                     args.livestation_email)
-
-    if args.livestation_password:
-        streamlink.set_plugin_option("livestation", "password",
-                                     args.livestation_password)
-
     if args.btv_username:
         streamlink.set_plugin_option("btv", "username", args.btv_username)
 


Test patch:
diff --git a/tests/test_localization.py b/tests/test_localization.py
index 102a6472..4a17da9c 100644
--- a/tests/test_localization.py
+++ b/tests/test_localization.py
@@ -63,6 +63,13 @@ class LocalizationTestsMixin(object):
         self.assertEqual("en_US", l.language_code)
         self.assertTrue(l.equivalent(language="en", country="US"))
 
+    @patch("locale.getdefaultlocale")
+    def test_default_invalid(self, getdefaultlocale):
+        getdefaultlocale.return_value = ("en_150", None)
+        l = l10n.Localization()
+        self.assertEqual("en_US", l.language_code)
+        self.assertTrue(l.equivalent(language="en", country="US"))
+
     def test_get_country(self):
         self.assertEqual("US",
                          l10n.Localization.get_country("USA").alpha2)
diff --git a/tests/test_plugin_bigo.py b/tests/test_plugin_bigo.py
new file mode 100644
index 00000000..99bd4e17
--- /dev/null
+++ b/tests/test_plugin_bigo.py
@@ -0,0 +1,31 @@
+import unittest
+
+from streamlink.plugins.bigo import Bigo
+
+
+class TestPluginBongacams(unittest.TestCase):
+    def test_can_handle_url(self):
+        # Correct urls
+        self.assertTrue(Bigo.can_handle_url("http://www.bigoweb.co/show/00000000"))
+        self.assertTrue(Bigo.can_handle_url("https://www.bigoweb.co/show/00000000"))
+        self.assertTrue(Bigo.can_handle_url("http://bigoweb.co/show/00000000"))
+        self.assertTrue(Bigo.can_handle_url("https://bigoweb.co/show/00000000"))
+        self.assertTrue(Bigo.can_handle_url("http://bigo.tv/00000000"))
+        self.assertTrue(Bigo.can_handle_url("https://bigo.tv/00000000"))
+        self.assertTrue(Bigo.can_handle_url("https://www.bigo.tv/00000000"))
+        self.assertTrue(Bigo.can_handle_url("http://www.bigo.tv/00000000"))
+
+        # Old URLs don't work anymore
+        self.assertFalse(Bigo.can_handle_url("http://live.bigo.tv/00000000"))
+        self.assertFalse(Bigo.can_handle_url("https://live.bigo.tv/00000000"))
+
+        # Wrong URL structure
+        self.assertFalse(Bigo.can_handle_url("ftp://www.bigo.tv/00000000"))
+        self.assertFalse(Bigo.can_handle_url("https://www.bigo.tv/show/00000000"))
+        self.assertFalse(Bigo.can_handle_url("http://www.bigo.tv/show/00000000"))
+        self.assertFalse(Bigo.can_handle_url("http://bigo.tv/show/00000000"))
+        self.assertFalse(Bigo.can_handle_url("https://bigo.tv/show/00000000"))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_plugin_camsoda.py b/tests/test_plugin_camsoda.py
new file mode 100644
index 00000000..444ffbca
--- /dev/null
+++ b/tests/test_plugin_camsoda.py
@@ -0,0 +1,68 @@
+import unittest
+
+from streamlink import Streamlink
+
+try:
+    from unittest.mock import patch, Mock
+except ImportError:
+    from mock import patch, Mock
+
+from streamlink.plugins.camsoda import Camsoda
+from streamlink.stream import HLSStream
+
+
+class TestPluginCamsoda(unittest.TestCase):
+    def setUp(self):
+        self.session = Streamlink()
+        self.plugin = Camsoda("https://www.camsoda.com/stream-name")
+
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Camsoda.can_handle_url("https://www.camsoda.com/stream-name"))
+        self.assertTrue(Camsoda.can_handle_url("https://www.camsoda.com/streamname"))
+        self.assertTrue(Camsoda.can_handle_url("https://www.camsoda.com/username"))
+
+        # shouldn't match
+        self.assertFalse(Camsoda.can_handle_url("http://local.local/"))
+        self.assertFalse(Camsoda.can_handle_url("http://localhost.localhost/"))
+
+    def test_get_hls_url(self):
+        api_data_video = {
+            "token": "abcdefghijklmnopqrstuvwxyz123456",
+            "app": "cam",
+            "edge_servers": ["edge.server", "edge.server2"],
+            "private_servers": ["priv.server", "priv.server2"],
+            "mjpeg_server": "mjpeg.server",
+            "stream_name": "username_enc4"
+        }
+
+        values = [
+            {
+                "api_data_user": {"user": {"chatstatus": "online"}},
+                "server": "edge.server"
+            }, {
+                "api_data_user": {"user": {"chatstatus": "private"}},
+                "server": "priv.server"
+            }, {
+                "api_data_user": {"user": {"chatstatus": "foobar"}},
+                "server": "mjpeg.server"
+            }
+        ]
+        for data in values:
+            data_video = api_data_video
+            data_user = data["api_data_user"]
+            server = data["server"]
+
+            HLS_URL_VIDEO = self.plugin._get_hls_url(data_user, data_video)
+            HLS_URL_VIDEO_TEST = self.plugin.HLS_URL_VIDEO.format(server=server, app=data_video["app"], stream_name=data_video["stream_name"], token=data_video["token"])
+
+            self.assertEqual(HLS_URL_VIDEO, HLS_URL_VIDEO_TEST)
+
+    @patch('streamlink.plugins.camsoda.http')
+    @patch('streamlink.plugins.camsoda.HLSStream')
+    def test_get_streams(self, hlsstream, mock_http):
+        hlsstream.parse_variant_playlist.return_value = {"test": HLSStream(self.session, "http://test.se/stream1")}
+
+        streams = self.plugin.get_streams()
+
+        self.assertTrue("test" in streams)
diff --git a/tests/test_plugin_canlitv.py b/tests/test_plugin_canlitv.py
new file mode 100644
index 00000000..30c27155
--- /dev/null
+++ b/tests/test_plugin_canlitv.py
@@ -0,0 +1,18 @@
+import unittest
+
+from streamlink.plugins.canlitv import Canlitv
+
+
+class TestPluginCanlitv(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Canlitv.can_handle_url("http://www.canlitv.com/channel"))
+        self.assertTrue(Canlitv.can_handle_url("http://www.canlitv.life/channel"))
+        self.assertTrue(Canlitv.can_handle_url("http://www.canlitvlive.co/izle/channel.html"))
+        self.assertTrue(Canlitv.can_handle_url("http://www.canlitvlive.live/izle/channel.html"))
+        self.assertTrue(Canlitv.can_handle_url("http://www.ecanlitvizle.net/channel/"))
+        self.assertTrue(Canlitv.can_handle_url("http://www.ecanlitvizle.net/onizleme.php?kanal=channel"))
+        self.assertTrue(Canlitv.can_handle_url("http://www.ecanlitvizle.net/tv.php?kanal=channel"))
+        # shouldn't match
+        self.assertFalse(Canlitv.can_handle_url("http://www.canlitv.com"))
+        self.assertFalse(Canlitv.can_handle_url("http://www.ecanlitvizle.net"))
diff --git a/tests/test_plugin_garena.py b/tests/test_plugin_garena.py
new file mode 100644
index 00000000..05b82d2a
--- /dev/null
+++ b/tests/test_plugin_garena.py
@@ -0,0 +1,86 @@
+import json
+import unittest
+
+from streamlink import Streamlink
+
+try:
+    from unittest.mock import patch, Mock
+except ImportError:
+    from mock import patch, Mock
+
+from streamlink.plugins.garena import Garena
+
+
+class TestPluginGarena(unittest.TestCase):
+    def setUp(self):
+        self.session = Streamlink()
+
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Garena.can_handle_url("https://garena.live/LOLTW"))
+        self.assertTrue(Garena.can_handle_url("https://garena.live/358220"))
+
+        # shouldn't match
+        self.assertFalse(Garena.can_handle_url("http://local.local/"))
+        self.assertFalse(Garena.can_handle_url("http://localhost.localhost/"))
+
+    @patch('streamlink.plugins.garena.http')
+    def test_post_api_info(self, mock_http):
+        API_INFO = Garena.API_INFO
+        schema = Garena._info_schema
+
+        api_data = {
+            "reply": {
+                "channel_id": 358220,
+            },
+            "result": "success"
+        }
+
+        api_resp = Mock()
+        api_resp.text = json.dumps(api_data)
+        mock_http.post.return_value = api_resp
+        mock_http.json.return_value = api_data
+
+        payload = {"alias": "LOLTW"}
+
+        plugin = Garena("https://garena.live/LOLTW")
+
+        info_data = plugin._post_api(API_INFO, payload, schema)
+
+        self.assertEqual(info_data["channel_id"], 358220)
+
+        mock_http.post.assert_called_with(API_INFO, json=dict(alias="LOLTW"))
+
+    @patch('streamlink.plugins.garena.http')
+    def test_post_api_stream(self, mock_http):
+        API_STREAM = Garena.API_STREAM
+        schema = Garena._stream_schema
+
+        api_data = {
+            "reply": {
+                "streams": [
+                    {
+                        "url": "https://test.se/stream1",
+                        "bitrate": 0,
+                        "resolution": 1080,
+                        "format": 3
+                    },
+                ]
+            },
+            "result": "success"
+        }
+
+        api_resp = Mock()
+        api_resp.text = json.dumps(api_data)
+        mock_http.post.return_value = api_resp
+        mock_http.json.return_value = api_data
+
+        payload = {"channel_id": 358220}
+
+        plugin = Garena("https://garena.live/358220")
+
+        stream_data = plugin._post_api(API_STREAM, payload, schema)
+
+        self.assertEqual(stream_data["streams"], api_data["reply"]["streams"])
+
+        mock_http.post.assert_called_with(API_STREAM, json=dict(channel_id=358220))