Repository: streamlink/streamlink
Pull request #392 (2017-01-09 17:45:02)

Add support for some Bulgarian live streams
As requested in #374.

BTV requires a login, the username and password can be set with `--btv-username` and `--btv-password`. 

Should support for the following sites:
- eurocom.bg/live
- bgonair.bg/tvonline
- bitelevision.com/live
- kanal3.bg/live
- nova.bg/live
- tv.bnt.bg/bntworld
- tv.bnt.bg/bnt1/hd
- tv.bnt.bg/bnt1/16x9
- tv.bnt.bg/bnt2/16x9
- btv.bg/live (requires login)


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 846f39e2..1c3d1481 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -30,7 +30,13 @@ bigo                - live.bigo.tv       Yes   --
                     - bigoweb.co
 bilibili            live.bilibili.com    Yes   ?
 bliptv              blip.tv              --    Yes
+bnt                 - tv.bnt.bg          Yes   No    Streams may be geo-restricted to Bulgaria.
+                    - bgonair.com
+                    - kanal3.bg
+                    - bitelevision.com
+                    - nova.bg
 bongacams           bongacams.com        Yes   No    Only RTMP streams are available.
+btv                 btv.bg               Yes   No    Requires login, and geo-restricted to Bulgaria.
 chaturbate          chaturbate.com       Yes   No
 cinergroup          - showtv.com.tr      Yes   No
                     - haberturk.com
@@ -61,6 +67,7 @@ dplay               - dplay.se           --    Yes   Streams may be geo-restrict
                     - dplay.no
                     - dplay.dk
 drdk                dr.dk                Yes   Yes   Streams may be geo-restricted to Denmark.
+eurocom             eurocom.bg           Yes   No
 euronews            euronews.com         Yes   No
 expressen           expressen.se         Yes   Yes
 filmon              filmon.com           Yes   Yes   Only SD quality streams.
diff --git a/src/streamlink/plugins/bnt.py b/src/streamlink/plugins/bnt.py
new file mode 100644
index 00000000..99963242
--- /dev/null
+++ b/src/streamlink/plugins/bnt.py
@@ -0,0 +1,66 @@
+from __future__ import print_function
+import re
+
+from streamlink import PluginError
+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.compat import urlparse
+
+
+class BNT(Plugin):
+    url_re = re.compile(r"""
+        https?://(?:www\.)?(?:
+            tv\.bnt\.bg/\w+(?:/\w+)?|
+            bitelevision\.com/live|
+            nova\.bg/live|
+            kanal3\.bg/live|
+            bgonair\.bg/tvonline
+        )/?
+    """, re.VERBOSE)
+    iframe_re = re.compile(r"iframe .*?src=\"([^\"]+)\"", re.DOTALL)
+    sdata_re = re.compile(r"sdata\.src.*?=.*?(?P<q>[\"'])(?P<url>http.*?)(?P=q)")
+    hls_file_re = re.compile(r"file: (?P<q>[\"'])(?P<url>http.+?m3u8.*?)(?P=q)")
+    hls_src_re = re.compile(r"video src=(?P<url>http[^ ]+m3u8[^ ]*)")
+    stream_schema = validate.Schema(
+        validate.any(
+            validate.all(validate.transform(sdata_re.search), validate.get("url")),
+            validate.all(validate.transform(hls_file_re.search), validate.get("url")),
+            validate.all(validate.transform(hls_src_re.search), validate.get("url")),
+        )
+    )
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def find_iframe(self, res):
+        p = urlparse(self.url)
+        for url in self.iframe_re.findall(res.text):
+            if "googletagmanager" not in url:
+                if url.startswith("//"):
+                    return "{0}:{1}".format(p.scheme, url)
+                else:
+                    return url
+
+    def _get_streams(self):
+        http.headers = {"User-Agent": useragents.FIREFOX}
+        res = http.get(self.url)
+        iframe_url = self.find_iframe(res)
+
+        if iframe_url:
+            self.logger.debug("Found iframe: {0}", iframe_url)
+            res = http.get(iframe_url, headers={"Referer": self.url})
+            try:
+                stream_url = self.stream_schema.validate(res.text)
+            except PluginError:
+                return
+
+            if stream_url:
+                return HLSStream.parse_variant_playlist(self.session, stream_url, headers={"Referer": iframe_url})
+
+
+
+__plugin__ = BNT
diff --git a/src/streamlink/plugins/btv.py b/src/streamlink/plugins/btv.py
new file mode 100644
index 00000000..e2ba2442
--- /dev/null
+++ b/src/streamlink/plugins/btv.py
@@ -0,0 +1,70 @@
+from __future__ import print_function
+import re
+
+from streamlink import PluginError
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.plugin.api import validate
+from streamlink.stream import HLSStream
+from streamlink.utils import parse_json
+from streamlink.plugin import PluginOptions
+
+
+class BTV(Plugin):
+    options = PluginOptions({
+        "username": None,
+        "password": None
+    })
+    url_re = re.compile(r"https?://(?:www\.)?btv\.bg/live/?")
+
+    api_url = "http://www.btv.bg/lbin/global/player_config.php"
+    check_login_url = "http://www.btv.bg/lbin/userRegistration/check_user_login.php"
+    login_url = "http://www.btv.bg/bin/registration2/login.php?action=login&settings=0"
+
+    media_id_re = re.compile(r"media_id=(\d+)")
+    src_re = re.compile(r"src: \"(http.*?)\"")
+    api_schema = validate.Schema(
+        validate.all(
+            {"status": "ok", "config": validate.text},
+            validate.get("config"),
+            validate.all(validate.transform(src_re.search),
+                         validate.any(
+                             None,
+                             validate.get(1), validate.url()
+                         ))
+        )
+    )
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def login(self, username, password):
+        res = http.post(self.login_url, data={"username": username, "password": password})
+        return res.text.startswith("success")
+
+    def get_hls_url(self, media_id):
+        res = http.get(self.api_url, params=dict(media_id=media_id))
+        try:
+            return parse_json(res.text, schema=self.api_schema)
+        except PluginError:
+            return
+
+    def _get_streams(self):
+        if not self.options.get("username") or not self.options.get("username"):
+            self.logger.error("BTV requires registration, set the username and password"
+                              " with --btv-username and --btv-password")
+        elif self.login(self.options.get("username"), self.options.get("password")):
+            res = http.get(self.url)
+            media_match = self.media_id_re.search(res.text)
+            media_id = media_match and media_match.group(1)
+            if media_id:
+                self.logger.debug("Found media id: {0}", media_id)
+                stream_url = self.get_hls_url(media_id)
+                if stream_url:
+                    return HLSStream.parse_variant_playlist(self.session, stream_url)
+        else:
+            self.logger.error("Login failed, a valid username and password is required")
+
+
+__plugin__ = BTV
diff --git a/src/streamlink/plugins/eurocom.py b/src/streamlink/plugins/eurocom.py
new file mode 100644
index 00000000..65a5cf2f
--- /dev/null
+++ b/src/streamlink/plugins/eurocom.py
@@ -0,0 +1,25 @@
+from __future__ import print_function
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.stream import RTMPStream
+
+
+class Eurocom(Plugin):
+    url_re = re.compile(r"https?://(?:www\.)?eurocom.bg/live/?")
+    file_re = re.compile(r"file:.*?(?P<q>['\"])(?P<url>rtmp://[^\"']+)(?P=q),")
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def _get_streams(self):
+        res = http.get(self.url)
+        m = self.file_re.search(res.text)
+        if m:
+            stream_url = m.group("url")
+            yield "live", RTMPStream(self.session, params={"rtmp": stream_url})
+
+
+__plugin__ = Eurocom
diff --git a/src/streamlink/plugins/stream.py b/src/streamlink/plugins/stream.py
index db987456..0733df5c 100644
--- a/src/streamlink/plugins/stream.py
+++ b/src/streamlink/plugins/stream.py
@@ -20,6 +20,7 @@ PROTOCOL_MAP = {
     "rtmpte": RTMPStream
 }
 PARAMS_REGEX = r"(\w+)=({.+?}|\[.+?\]|\(.+?\)|'(?:[^'\\]|\\')*'|\"(?:[^\"\\]|\\\")*\"|\S+)"
+SCHEME_REGEX = re.compile(r"^\w+://(.+)")
 
 class StreamURL(Plugin):
     @classmethod
@@ -51,10 +52,10 @@ class StreamURL(Plugin):
 
         split = self.url.split(" ")
         url = split[0]
-        urlnoproto = re.match("^\w+://(.+)", url).group(1)
+        urlnoproto = SCHEME_REGEX.match(url).group(1)
 
         # Prepend http:// if needed.
-        if cls != RTMPStream and not len(urlparse(urlnoproto).scheme):
+        if cls != RTMPStream and not SCHEME_REGEX.match(urlnoproto):
             urlnoproto = "http://{0}".format(urlnoproto)
 
         params = (" ").join(split[1:])
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index 94288c97..49a6c111 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -975,6 +975,20 @@ plugin.add_argument(
     A Livestation account password to use with --livestation-email.
     """
 )
+plugin.add_argument(
+    "--btv-username",
+    metavar="USERNAME",
+    help="""
+    A BTV username required to access any stream.
+    """
+)
+plugin.add_argument(
+    "--btv-password",
+    metavar="PASSWORD",
+    help="""
+    A BTV account password to use with --btv-username.
+    """
+)
 
 
 # Deprecated options
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 44ae745d..b53a793e 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -810,6 +810,17 @@ def setup_plugin_options():
         streamlink.set_plugin_option("livestation", "password",
                                        args.livestation_password)
 
+    if args.btv_username:
+        streamlink.set_plugin_option("btv", "username", args.btv_username)
+
+    if args.btv_username and not args.btv_password:
+        btv_password = console.askpass("Enter BTV password: ")
+    else:
+        btv_password = args.btv_password
+
+    if btv_password:
+        streamlink.set_plugin_option("btv", "password", btv_password)
+
     # Deprecated options
     if args.jtv_legacy_names:
         console.logger.warning("The option --jtv/twitch-legacy-names is "


Test patch:
diff --git a/tests/test_plugin_bnt.py b/tests/test_plugin_bnt.py
new file mode 100644
index 00000000..de897ee5
--- /dev/null
+++ b/tests/test_plugin_bnt.py
@@ -0,0 +1,25 @@
+import unittest
+
+from streamlink.plugins.bnt import BNT
+
+
+class TestPluginBNT(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(BNT.can_handle_url("http://bgonair.bg/tvonline"))
+        self.assertTrue(BNT.can_handle_url("http://bgonair.bg/tvonline/"))
+        self.assertTrue(BNT.can_handle_url("http://bitelevision.com/live"))
+        self.assertTrue(BNT.can_handle_url("http://www.kanal3.bg/live"))
+        self.assertTrue(BNT.can_handle_url("http://www.nova.bg/live"))
+        self.assertTrue(BNT.can_handle_url("http://nova.bg/live"))
+        self.assertTrue(BNT.can_handle_url("http://tv.bnt.bg/bntworld/"))
+        self.assertTrue(BNT.can_handle_url("http://tv.bnt.bg/bnt1/hd/"))
+        self.assertTrue(BNT.can_handle_url("http://tv.bnt.bg/bnt1/hd"))
+        self.assertTrue(BNT.can_handle_url("http://tv.bnt.bg/bnt1/16x9/"))
+        self.assertTrue(BNT.can_handle_url("http://tv.bnt.bg/bnt1/16x9"))
+        self.assertTrue(BNT.can_handle_url("http://tv.bnt.bg/bnt2/16x9/"))
+        self.assertTrue(BNT.can_handle_url("http://tv.bnt.bg/bnt2/16x9"))
+
+        # shouldn't match
+        self.assertFalse(BNT.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(BNT.can_handle_url("http://www.youtube.com/"))
diff --git a/tests/test_plugin_btv.py b/tests/test_plugin_btv.py
new file mode 100644
index 00000000..b04e1933
--- /dev/null
+++ b/tests/test_plugin_btv.py
@@ -0,0 +1,15 @@
+import unittest
+
+from streamlink.plugins.btv import BTV
+
+
+class TestPluginBTV(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(BTV.can_handle_url("http://btv.bg/live"))
+        self.assertTrue(BTV.can_handle_url("http://btv.bg/live/"))
+        self.assertTrue(BTV.can_handle_url("http://www.btv.bg/live/"))
+
+        # shouldn't match
+        self.assertFalse(BTV.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(BTV.can_handle_url("http://www.youtube.com/"))
diff --git a/tests/test_plugin_eurocom.py b/tests/test_plugin_eurocom.py
new file mode 100644
index 00000000..bdf83c0e
--- /dev/null
+++ b/tests/test_plugin_eurocom.py
@@ -0,0 +1,16 @@
+import unittest
+
+from streamlink.plugins.eurocom import Eurocom
+
+
+class TestPluginEurocom(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Eurocom.can_handle_url("http://eurocom.bg/live"))
+        self.assertTrue(Eurocom.can_handle_url("http://eurocom.bg/live/"))
+        self.assertTrue(Eurocom.can_handle_url("http://www.eurocom.bg/live/"))
+        self.assertTrue(Eurocom.can_handle_url("http://www.eurocom.bg/live"))
+
+        # shouldn't match
+        self.assertFalse(Eurocom.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(Eurocom.can_handle_url("http://www.youtube.com/"))
diff --git a/tests/test_plugin_streamme.py b/tests/test_plugin_streamme.py
new file mode 100644
index 00000000..fb06503e
--- /dev/null
+++ b/tests/test_plugin_streamme.py
@@ -0,0 +1,15 @@
+import unittest
+
+from streamlink.plugins.streamme import StreamMe
+
+
+class TestPluginStreamMe(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(StreamMe.can_handle_url("http://www.stream.me/nameofstream"))
+
+        # shouldn't match
+        self.assertFalse(StreamMe.can_handle_url("http://www.livestream.me/nameofstream"))
+        self.assertFalse(StreamMe.can_handle_url("http://www.streamme.com/nameofstream"))
+        self.assertFalse(StreamMe.can_handle_url("http://www.streamme.me/nameofstream"))
+        self.assertFalse(StreamMe.can_handle_url("http://www.youtube.com/"))