Repository: streamlink/streamlink
Pull request #3012 (2020-06-05 15:17:12)

ci: no test/documentation jobs on scheduled run
These jobs are unnecessary on scheduled builds, since only commits get merged into master which have passed the tests previously.
Also avoids recurring code coverage uploads if no commits get merged into master between two scheduled builds.

----

Ideally we should cancel the whole workflow run if there were no new commits since the last scheduled build, but that would require adding an additional job that runs before the test, documentation and installer jobs.

----

Duplicate code coverage upload:
- https://github.com/streamlink/streamlink/runs/740545127?check_suite_focus=true#step:7:40
- https://codecov.io/gh/streamlink/streamlink/commit/90e634ec17cd7b90f062f25f2b620e50285dd9c0/build

codecov[bot]: # [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/3012?src=pr&el=h1) Report
> Merging [#3012](https://codecov.io/gh/streamlink/streamlink/pull/3012?src=pr&el=desc) into [master](https://codecov.io/gh/streamlink/streamlink/commit/90e634ec17cd7b90f062f25f2b620e50285dd9c0&el=desc) will **decrease** coverage by `0.02%`.
> The diff coverage is `n/a`.


```diff
@@            Coverage Diff             @@
##           master    #3012      +/-   ##
==========================================
- Coverage   52.64%   52.62%   -0.03%     
==========================================
  Files         251      251              
  Lines       15767    15767              
==========================================
- Hits         8301     8297       -4     
- Misses       7466     7470       +4     
```


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 378222a8..6cbd9dbb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -15,6 +15,7 @@ env:
 jobs:
   test:
     name: Test
+    if: github.event_name != 'schedule'
     strategy:
       fail-fast: false
       # when changing the build matrix, the `after_n_builds` value in codecov.yml may need to be updated
@@ -48,6 +49,7 @@ jobs:
 
   documentation:
     name: Test docs
+    if: github.event_name != 'schedule'
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
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
diff --git a/src/streamlink/plugins/foxtr.py b/src/streamlink/plugins/foxtr.py
index 5aae960c..3a6cc5e0 100644
--- a/src/streamlink/plugins/foxtr.py
+++ b/src/streamlink/plugins/foxtr.py
@@ -16,7 +16,7 @@ class FoxTR(Plugin):
            foxplay.com.tr/.*)
     """, re.VERBOSE)
 
-    playervars_re = re.compile(r"source\s*:\s*\[\s*\{\s*videoSrc\s*:\s*'(.*?)'", re.DOTALL)
+    playervars_re = re.compile(r"source\s*:\s*\[\s*\{\s*videoSrc\s*:\s*(?:mobilecheck\(\)\s*\?\s*)?'([^']+)'")
 
     @classmethod
     def can_handle_url(cls, url):
diff --git a/src/streamlink/plugins/huya.py b/src/streamlink/plugins/huya.py
index 4109f6e1..05383304 100644
--- a/src/streamlink/plugins/huya.py
+++ b/src/streamlink/plugins/huya.py
@@ -8,20 +8,14 @@ from streamlink.utils import update_scheme
 
 HUYA_URL = "http://m.huya.com/%s"
 
-_url_re = re.compile(r'http(s)?://(www\.)?huya.com/(?P<channel>[^/]+)', re.VERBOSE)
-_hls_re = re.compile(r'^\s*<video\s+id="html5player-video"\s+src="(?P<url>[^"]+)"', re.MULTILINE)
+_url_re = re.compile(r'https?://(www\.)?huya.com/(?P<channel>[^/]+)')
+_hls_re = re.compile(r'liveLineUrl\s*=\s*"(?P<url>[^"]+)"')
 
 _hls_schema = validate.Schema(
-    validate.all(
-        validate.transform(_hls_re.search),
-        validate.any(
-            None,
-            validate.all(
-                validate.get('url'),
-                validate.transform(str)
-            )
-        )
-    )
+    validate.transform(_hls_re.search),
+    validate.any(None, validate.get("url")),
+    validate.transform(lambda v: update_scheme("http://", v)),
+    validate.url()
 )
 
 
@@ -38,7 +32,7 @@ class Huya(Plugin):
         # Some problem with SSL on huya.com now, do not use https
 
         hls_url = self.session.http.get(HUYA_URL % channel, schema=_hls_schema)
-        yield "live", HLSStream(self.session, update_scheme("http://", hls_url))
+        yield "live", HLSStream(self.session, hls_url)
 
 
 __plugin__ = Huya


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/"))