Repository: streamlink/streamlink

Issue:
Bigo TV cannot successfully get the videoSrc URL
<!--
Thanks for reporting a plugin issue!
USE THE TEMPLATE. Otherwise your plugin issue may be rejected.

First, see the contribution guidelines:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink

Also check the list of open and closed plugin issues:
https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22

Please see the text preview to avoid unnecessary formatting errors.
-->


## Plugin Issue

<!-- Replace [ ] with [x] in order to check the box -->
- [x] This is a plugin issue and I have read the contribution guidelines.


### Description

<!-- Explain the plugin issue as thoroughly as you can. -->
I try to use streamlink to watch the bigo live tv, but the command line pops up the error message as "[plugin.bigo][error] No playlist found."

### Reproduction steps / Explicit stream URLs to test

<!-- How can we reproduce this? Please note the exact steps below using the list format supplied. If you need more steps please add them. -->

1. streamlink  http://www.bigo.tv/11819287 best &


### Log output

<!--
TEXT LOG OUTPUT IS REQUIRED for a plugin issue!
Use the `--loglevel debug` parameter and avoid using parameters which suppress log output.
https://streamlink.github.io/cli.html#cmdoption-l

Make sure to **remove usernames and passwords**
You can copy the output to https://gist.github.com/ or paste it below.
-->

```
[cli][debug] OS:         Linux-5.4.0-33-generic-x86_64-with-glibc2.29
[cli][debug] Python:     3.8.2
[cli][debug] Streamlink: 1.4.1
[cli][debug] Requests(2.22.0), Socks(1.6.7), Websocket(0.53.0)
[cli][info] Found matching plugin bigo for URL http://www.bigo.tv/11819287
[plugin.bigo][error] No playlist found.
error: No playable streams found on this URL: http://www.bigo.tv/11819287
```


### Additional comments, screenshots, etc.

After reviewing the bigo.py in streamlink/plugin, I found that the original method attempts to get the videoSrc in the webpage. However, recently the web page excluded the parameter in the page. 

So, here is the alternative
1. Add new API URL. 
```
api_url = "http://www.bigo.tv/OInterface/getVideoParam?bigoId="
```
2. Extract bigo_id from self.url. 
```
extract_id = self.url.split('/')[-1]
```
3. Use composed url to get the videoSrc. 
```
page = self.session.http.get(api_url + extract_id, ...)
```
4. Change the videomatch & videourl. 
```
videomatch = page.json()['data']['videoSrc']; videourl = videomatch
```


Comments:

Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 0d003504..6b15eb5b 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -38,7 +38,7 @@ bilibili                live.bilibili.com    Yes   ?
 bloomberg               bloomberg.com        Yes   Yes
 brightcove              players.brig... [6]_ Yes   Yes
 btsports                sport.bt.com         Yes   Yes   Requires subscription account
-btv                     btv.bg               Yes   No    Requires login, and geo-restricted to Bulgaria.
+btv                     btvplus.bg           Yes   No    Streams are geo-restricted to Bulgaria.
 canalplus               mycanal.fr           No    Yes   Streams may be geo-restricted to France.
 cdnbg                   - tv.bnt.bg          Yes   No    Streams may be geo-restricted to Bulgaria.
                         - bgonair.bg
diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py
index 9e8471cf..696a0dc6 100644
--- a/src/streamlink/plugin/plugin.py
+++ b/src/streamlink/plugin/plugin.py
@@ -377,7 +377,7 @@ class Plugin(object):
             # Force lowercase name and replace space with underscore.
             streams[name.lower()] = stream
 
-        # Create the best/worst synonmys
+        # Create the best/worst synonyms
         def stream_weight_only(s):
             return (self.stream_weight(s)[0] or (len(streams) == 1 and 1))
 
diff --git a/src/streamlink/plugins/bigo.py b/src/streamlink/plugins/bigo.py
index 9b895071..7d0e9286 100644
--- a/src/streamlink/plugins/bigo.py
+++ b/src/streamlink/plugins/bigo.py
@@ -1,37 +1,37 @@
-import logging
 import re
 
 from streamlink.plugin import Plugin
-from streamlink.plugin.api import useragents
+from streamlink.plugin.api import useragents, validate
 from streamlink.stream import HLSStream
 
-log = logging.getLogger(__name__)
-
 
 class Bigo(Plugin):
-    _url_re = re.compile(r"^https?://(?:www\.)?bigo\.tv/[^/]+$")
-    _video_re = re.compile(
-        r"""videoSrc:\s?["'](?P<url>[^"']+)["']""",
-        re.M)
+    _url_re = re.compile(r"https?://(?:www\.)?bigo\.tv/([^/]+)$")
+    _api_url = "https://www.bigo.tv/OInterface/getVideoParam?bigoId={0}"
+
+    _video_info_schema = validate.Schema({
+        "code": 0,
+        "msg": "success",
+        "data": {
+            "videoSrc": validate.any(None, "", validate.url())
+        }
+    })
 
     @classmethod
     def can_handle_url(cls, url):
         return cls._url_re.match(url) is not None
 
     def _get_streams(self):
-        page = self.session.http.get(
-            self.url,
+        match = self._url_re.match(self.url)
+        res = self.session.http.get(
+            self._api_url.format(match.group(1)),
             allow_redirects=True,
             headers={"User-Agent": useragents.IPHONE_6}
         )
-        videomatch = self._video_re.search(page.text)
-        if not videomatch:
-            log.error("No playlist found.")
-            return
-
-        videourl = videomatch.group(1)
-        log.debug("URL={0}".format(videourl))
-        yield "live", HLSStream(self.session, videourl)
+        data = self.session.http.json(res, schema=self._video_info_schema)
+        videourl = data["data"]["videoSrc"]
+        if videourl:
+            yield "live", HLSStream(self.session, videourl)
 
 
 __plugin__ = Bigo
diff --git a/src/streamlink/plugins/btv.py b/src/streamlink/plugins/btv.py
index a026c641..680711cc 100644
--- a/src/streamlink/plugins/btv.py
+++ b/src/streamlink/plugins/btv.py
@@ -1,38 +1,30 @@
-from __future__ import print_function
+import argparse
+import logging
 import re
 
-from streamlink import PluginError
-from streamlink.plugin import Plugin
+from streamlink.plugin import Plugin, PluginArguments, PluginArgument
 from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream
 from streamlink.utils import parse_json
-from streamlink.plugin import PluginArgument, PluginArguments
+
+log = logging.getLogger(__name__)
 
 
 class BTV(Plugin):
     arguments = PluginArguments(
         PluginArgument(
             "username",
-            metavar="USERNAME",
-            requires=["password"],
-            help="""
-        A BTV username required to access any stream.
-        """
+            help=argparse.SUPPRESS
         ),
         PluginArgument(
             "password",
             sensitive=True,
-            metavar="PASSWORD",
-            help="""
-        A BTV account password to use with --btv-username.
-        """
+            help=argparse.SUPPRESS
         )
     )
-    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 = "https://www.btv.bg/bin/registration2/login.php?action=login&settings=0"
+    url_re = re.compile(r"https?://(?:www\.)?btvplus\.bg/live/?")
+    api_url = "https://btvplus.bg/lbin/v3/btvplus/player_config.php"
 
     media_id_re = re.compile(r"media_id=(\d+)")
     src_re = re.compile(r"src: \"(http.*?)\"")
@@ -55,35 +47,19 @@ class BTV(Plugin):
     def can_handle_url(cls, url):
         return cls.url_re.match(url) is not None
 
-    def login(self, username, password):
-        res = self.session.http.post(self.login_url, data={"username": username, "password": password})
-        if "success_logged_in" in res.text:
-            return True
-        else:
-            return False
-
     def get_hls_url(self, media_id):
         res = self.session.http.get(self.api_url, params=dict(media_id=media_id))
-        try:
-            return parse_json(res.text, schema=self.api_schema)
-        except PluginError:
-            return
+        return parse_json(res.text, schema=self.api_schema)
 
     def _get_streams(self):
-        if not self.options.get("username") or not self.options.get("password"):
-            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 = self.session.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")
+        res = self.session.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:
+            log.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)
 
 
 __plugin__ = BTV


Test patch:
diff --git a/tests/plugins/test_bigo.py b/tests/plugins/test_bigo.py
index 313ec7e8..11eb435e 100644
--- a/tests/plugins/test_bigo.py
+++ b/tests/plugins/test_bigo.py
@@ -5,26 +5,33 @@ from streamlink.plugins.bigo import Bigo
 
 class TestPluginBigo(unittest.TestCase):
     def test_can_handle_url(self):
-        # Correct urls
-        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"))
-        self.assertTrue(Bigo.can_handle_url("http://www.bigo.tv/fancy1234"))
-        self.assertTrue(Bigo.can_handle_url("http://www.bigo.tv/abc.123"))
-        self.assertTrue(Bigo.can_handle_url("http://www.bigo.tv/000000.00"))
+        should_match = [
+            "http://bigo.tv/00000000",
+            "https://bigo.tv/00000000",
+            "https://www.bigo.tv/00000000",
+            "http://www.bigo.tv/00000000",
+            "http://www.bigo.tv/fancy1234",
+            "http://www.bigo.tv/abc.123",
+            "http://www.bigo.tv/000000.00"
+        ]
+        for url in should_match:
+            self.assertTrue(Bigo.can_handle_url(url), url)
 
-        # 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"))
-        self.assertFalse(Bigo.can_handle_url("http://www.bigoweb.co/show/00000000"))
-        self.assertFalse(Bigo.can_handle_url("https://www.bigoweb.co/show/00000000"))
-        self.assertFalse(Bigo.can_handle_url("http://bigoweb.co/show/00000000"))
-        self.assertFalse(Bigo.can_handle_url("https://bigoweb.co/show/00000000"))
+    def test_can_handle_url_negative(self):
+        should_not_match = [
+            # Old URLs don't work anymore
+            "http://live.bigo.tv/00000000",
+            "https://live.bigo.tv/00000000",
+            "http://www.bigoweb.co/show/00000000",
+            "https://www.bigoweb.co/show/00000000",
+            "http://bigoweb.co/show/00000000",
+            "https://bigoweb.co/show/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"))
+            # Wrong URL structure
+            "https://www.bigo.tv/show/00000000",
+            "http://www.bigo.tv/show/00000000",
+            "http://bigo.tv/show/00000000",
+            "https://bigo.tv/show/00000000"
+        ]
+        for url in should_not_match:
+            self.assertFalse(Bigo.can_handle_url(url), url)
diff --git a/tests/plugins/test_btv.py b/tests/plugins/test_btv.py
index b04e1933..32304e92 100644
--- a/tests/plugins/test_btv.py
+++ b/tests/plugins/test_btv.py
@@ -6,9 +6,9 @@ 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/"))
+        self.assertTrue(BTV.can_handle_url("http://btvplus.bg/live"))
+        self.assertTrue(BTV.can_handle_url("http://btvplus.bg/live/"))
+        self.assertTrue(BTV.can_handle_url("http://www.btvplus.bg/live/"))
 
         # shouldn't match
         self.assertFalse(BTV.can_handle_url("http://www.tvcatchup.com/"))