Repository: streamlink/streamlink
Pull request #1353 (2017-12-02 18:59:59)

Add plugin for olympicchannel.com
This plugin works for both non-premium live streams, and vods on the site. I'm kinda new to this so if the naming conventions or other things like that are off let me know and I'll do my best to fix them,

gravyboat: Can you please rebase? #1349 is merged. Also would you mind adding some tests for this plugin if possible?
codecov[bot]: # [Codecov](https://codecov.io/gh/streamlink/streamlink/pull/1353?src=pr&el=h1) Report
> Merging [#1353](https://codecov.io/gh/streamlink/streamlink/pull/1353?src=pr&el=desc) into [master](https://codecov.io/gh/streamlink/streamlink/commit/f4ad8ebd37f9673a938050c4aecd5ace9286dd67?src=pr&el=desc) will **increase** coverage by `0.07%`.
> The diff coverage is `n/a`.


```diff
@@            Coverage Diff             @@
##           master    #1353      +/-   ##
==========================================
+ Coverage   47.11%   47.19%   +0.07%     
==========================================
  Files          99       98       -1     
  Lines        7178     7130      -48     
==========================================
- Hits         3382     3365      -17     
+ Misses       3796     3765      -31
```

xela722: Sorry, I'm a little new at git and everything. Also, I'm not quite sure how the test plugins work so I'm not sure I could apply one reliably.
gravyboat: @xela722 No worries, if you feel like doing tests there are a couple ways, you can do something more thorough like https://github.com/streamlink/streamlink/blob/master/tests/test_plugin_adultswim.py, or something simple like https://github.com/streamlink/streamlink/blob/master/tests/test_plugin_bloomberg.py where you just feed urls to the functions and assert that they are true/false.

Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 8aab1490..fee4b3a5 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -144,6 +144,7 @@ nrk                 - tv.nrk.no          Yes   Yes   Streams may be geo-restrict
                     - radio.nrk.no
 oldlivestream       - original.li.. [3]_ Yes   No    Only mobile streams are supported.
                     - cdn.livestream.com
+olympicchannel      olympicchannel.com   Yes   Yes   Only non-premium content is available.
 openrectv           openrec.tv           Yes   Yes
 orf_tvthek          tvthek.orf.at        Yes   Yes
 ovvatv              ovva.tv              Yes   No
diff --git a/src/streamlink/plugins/btv.py b/src/streamlink/plugins/btv.py
index 0784e084..9109c025 100644
--- a/src/streamlink/plugins/btv.py
+++ b/src/streamlink/plugins/btv.py
@@ -19,7 +19,7 @@ class BTV(Plugin):
 
     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"
+    login_url = "https://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.*?)\"")
@@ -41,7 +41,10 @@ class BTV(Plugin):
 
     def login(self, username, password):
         res = http.post(self.login_url, data={"username": username, "password": password})
-        return res.text.startswith("success")
+        if "success_logged_in" in res.text:
+            return True
+        else:
+            return False
 
     def get_hls_url(self, media_id):
         res = http.get(self.api_url, params=dict(media_id=media_id))
@@ -51,7 +54,7 @@ class BTV(Plugin):
             return
 
     def _get_streams(self):
-        if not self.options.get("username") or not self.options.get("username"):
+        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")):
diff --git a/src/streamlink/plugins/olympicchannel.py b/src/streamlink/plugins/olympicchannel.py
new file mode 100644
index 00000000..f7c6e605
--- /dev/null
+++ b/src/streamlink/plugins/olympicchannel.py
@@ -0,0 +1,63 @@
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.plugin.api import validate
+from streamlink.stream import HLSStream, HTTPStream
+
+
+class OlympicChannel(Plugin):
+    _url_re = re.compile(r"http(?:s)?://(\w+)\.?olympicchannel.com/../(?P<type>tv|playback)/(livestream-.\d|.*)/")
+    _live_api_url = "https://www.olympicchannel.com{0}api/v2/metadata/{1}"
+    _stream_get_url = "https://www.olympicchannel.com/en/proxy/viewings/"
+    _stream_api_schema = validate.Schema({
+        u'status': u'ok',
+        u'primary': validate.url(),
+        validate.optional(u'backup'): validate.url()
+    })
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url)
+
+    def _get_vod_streams(self):
+        page = http.get(self.url)
+        asset = re.search(r'asse_.{32}', str(page._content)).group(0)
+        post_data = '{"asset_url":"/api/assets/%s/"}' % asset
+        stream_data = http.json(http.post(self._stream_get_url, data=post_data))['objects'][0]['level3']['streaming_url']
+        return HLSStream.parse_variant_playlist(self.session, stream_data)
+
+    def _get_live_streams(self, lang, path):
+        """
+        Get the live stream in a particular language
+        :param lang:
+        :param path:
+        :return:
+        """
+        res = http.get(self._live_api_url.format(lang, path))
+        live_res = http.json(res)['default']['uid']
+        post_data = '{"channel_url":"/api/channels/%s/"}' % live_res
+        try:
+            stream_data = http.json(http.post(self._stream_get_url, data=post_data))['stream_url']
+        except:
+            stream_data = http.json(http.post(self._stream_get_url, data=post_data))['channel_url']
+        return HLSStream.parse_variant_playlist(self.session, stream_data)
+
+    def _get_streams(self):
+        """
+        Find the streams for OlympicChannel
+        :return:
+        """
+        match = self._url_re.match(self.url)
+        type_of_stream = match.group('type')
+        lang = re.search(r"/../", self.url).group(0)
+        
+        if type_of_stream == 'tv':
+            path = re.search(r"tv/.*-\d/$", self.url).group(0)
+            
+            return self._get_live_streams(lang, path)
+        elif type_of_stream == 'playback':
+            path = re.search(r"/playback/.*/$", self.url).group(0)
+            return self._get_vod_streams()
+
+__plugin__ = OlympicChannel
diff --git a/src/streamlink/plugins/picarto.py b/src/streamlink/plugins/picarto.py
index 06d018a9..4cde64aa 100644
--- a/src/streamlink/plugins/picarto.py
+++ b/src/streamlink/plugins/picarto.py
@@ -1,6 +1,7 @@
 from __future__ import print_function
 
 import re
+import json
 
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import http
@@ -9,50 +10,30 @@ from streamlink.stream import RTMPStream
 
 
 class Picarto(Plugin):
-    API_CHANNEL_INFO = "https://picarto.tv/process/channel"
+    CHANNEL_API_URL = "https://api.picarto.tv/v1/channel/name/{channel}"
+    VIDEO_API_URL = "https://picarto.tv/process/channel"
     RTMP_URL = "rtmp://{server}:1935/play/"
     RTMP_PLAYPATH = "golive+{channel}?token={token}"
     HLS_URL = "https://{server}/hls/{channel}/index.m3u8?token={token}"
 
+    # Regex for all usable URLs
     _url_re = re.compile(r"""
         https?://(?:\w+\.)?picarto\.tv/(?:videopopout/)?([^&?/]+)
     """, re.VERBOSE)
 
-    # divs with tech_switch class
-    _tech_switch_re = re.compile(r"""
-    <div\s+class=".*?tech_switch.*?"(.*?)>
-    """, re.VERBOSE)
-    # placeStream(channel, playerID, product, offlineImage, online, token, tech)
-    _place_stream_re = re.compile(r"""
-        <script>\s*placeStream\s*\((.*?)\);?\s*</script>
-    """, re.VERBOSE)
-    # <source ...>
-    _source_re = re.compile(r'''source src="(http[^"]+)"''')
+    # Regex for VOD extraction
+    _vod_re = re.compile(r'''vod: "(https?://[\S]+?/index.m3u8)",''')
 
     @classmethod
     def can_handle_url(cls, url):
         return cls._url_re.match(url) is not None
 
-    @classmethod
-    def _stream_online(cls, page):
-        match = cls._place_stream_re.search(page.text)
-        if match:
-            return match.group(1).split(",")[4].strip() == "1"
-        return False
-
-    @classmethod
-    def _get_steam_list(cls, page):
-        for match in cls._tech_switch_re.findall(page.text):
-            args = {}
-            for attr in match.strip().split(" "):
-                key, value = attr.split("=")
-                _, key = key.split("-")
-                args[key] = value.strip('"')
-            yield args
-
-    def _create_hls_stream(self, server, args):
+    def _create_hls_stream(self, server, channel, token):
         streams = HLSStream.parse_variant_playlist(self.session,
-                                                   self.HLS_URL.format(server=server, **args),
+                                                   self.HLS_URL.format(
+                                                       server=server,
+                                                       channel=channel,
+                                                       token=token),
                                                    verify=False)
         if len(streams) > 1:
             self.logger.debug("Multiple HLS streams found")
@@ -64,67 +45,68 @@ class Picarto(Plugin):
             # one HLS streams, rename it to live
             return {"live": list(streams.values())[0]}
 
-    def _create_flash_stream(self, server, args):
+    def _create_flash_stream(self, server, channel, token):
         params = {
             "rtmp": self.RTMP_URL.format(server=server),
-            "playpath": self.RTMP_PLAYPATH.format(**args)
+            "playpath": self.RTMP_PLAYPATH.format(token=token, channel=channel)
         }
         return RTMPStream(self.session, params=params)
 
     def _get_vod_stream(self, page):
-        m = self._source_re.search(page.text)
+        m = self._vod_re.search(page.text)
         if m:
             return HLSStream.parse_variant_playlist(self.session, m.group(1))
 
     def _get_streams(self):
-        page = http.get(self.url)
+        url_channel_name = self._url_re.match(self.url).group(1)
 
-        page_channel = self._url_re.match(self.url).group(1)
-        if page_channel.endswith(".flv"):
+        # Handle VODs first, since their "channel name" is different
+        if url_channel_name.endswith(".flv"):
             self.logger.debug("Possible VOD stream...")
+            page = http.get(self.url)
             vod_streams = self._get_vod_stream(page)
             if vod_streams:
                 for s in vod_streams.items():
                     yield s
                 return
+            else:
+                self.logger.warning("Probably a VOD stream but no VOD found?")
+
+        ci = http.get(self.CHANNEL_API_URL.format(channel=url_channel_name), raise_for_status=False)
 
-        if "This channel does not exist" in page.text:
-            self.logger.error("The channel {0} does not exist".format(page_channel))
+        if ci.status_code == 404:
+            self.logger.error("The channel {0} does not exist".format(url_channel_name))
             return
 
-        if not self._stream_online(page):
-            self.logger.error("The channel {0} is currently offline".format(page_channel))
+        channel_api_json = json.loads(ci.text)
+
+        if channel_api_json["online"] != True:
+            self.logger.error("The channel {0} is currently offline".format(url_channel_name))
             return
 
         server = None
-        streams = list(self._get_steam_list(page))
-        multi = False
-
-        for args in streams:
-            channel, tech, token = args["channel"], args["tech"], args["token"]
-            if channel.lower() != page_channel.lower():
-                if not multi:
-                    self.logger.info("Skipping multi-channel stream for: {0}".format(channel))
-                    multi = True
-                continue
-
-            self.logger.debug("Found stream for {channel}; tech=\"{tech}\", token=\"{token}\"", **args)
-
-            # cache the load balancing info
-            if not server:
-                channel_server_res = http.post(self.API_CHANNEL_INFO, data={"loadbalancinginfo": channel})
-                server = channel_server_res.text
-                self.logger.debug("Using load balancing server {0} for channel {1}",
-                                  server,
-                                  channel)
-
-            # generate all the streams, for multi-channel streams also append the channel name
-            if tech == "hls":
-                for s in self._create_hls_stream(server, args).items():
+        token = "public"
+        channel = channel_api_json["name"]
+
+        # Extract preferred edge server and available techs from the undocumented channel API
+        channel_server_res = http.post(self.VIDEO_API_URL, data={"loadbalancinginfo": channel})
+        info_json = json.loads(channel_server_res.text)
+        pref = info_json["preferedEdge"]
+        for i in info_json["edges"]:
+            if i["id"] == pref:
+                server = i["ep"]
+                break
+        self.logger.debug("Using load balancing server {0} : {1} for channel {2}",
+                          pref,
+                          server,
+                          channel)
+
+        for i in info_json["techs"]:
+            if i["label"] == "HLS":
+                for s in self._create_hls_stream(server, channel, token).items():
                     yield s
-
-            elif tech == "flash":
-                stream = self._create_flash_stream(server, args)
+            elif i["label"] == "RTMP Flash":
+                stream = self._create_flash_stream(server, channel, token)
                 yield "live", stream
 
 


Test patch:
diff --git a/tests/test_plugin_olympicchannel.py b/tests/test_plugin_olympicchannel.py
new file mode 100644
index 00000000..94f8c56f
--- /dev/null
+++ b/tests/test_plugin_olympicchannel.py
@@ -0,0 +1,16 @@
+import unittest
+
+from streamlink.plugins.olympicchannel import OlympicChannel
+
+
+class TestPluginOlympicChannel(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(OlympicChannel.can_handle_url("https://www.olympicchannel.com/en/playback/listen-up-olympic-dreamers/"))
+        self.assertTrue(OlympicChannel.can_handle_url("https://www.olympicchannel.com/en/tv/overflow-6/"))
+        self.assertTrue(OlympicChannel.can_handle_url("https://www.olympicchannel.com/en/tv/livestream-5/"))
+        
+
+        # shouldn't match
+        self.assertFalse(OlympicChannel.can_handle_url("https://www.olympicchannel.com/en/"))
+        self.assertFalse(OlympicChannel.can_handle_url("https://www.olympicchannel.com/en/channel/olympic-channel/"))