Repository: streamlink/streamlink
Pull request #2434 (2019-04-19 14:36:39)

plugins.reuters: New Plugin
closes https://github.com/streamlink/streamlink/issues/2427


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index be20b685..249d97c6 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -163,6 +163,7 @@ radionet                - radio.net          Yes   --
                         - radio.se
 raiplay                 raiplay.it           Yes   No    Most streams are geo-restricted to Italy.
 reshet                  reshet.tv            Yes   Yes   Streams may be geo-restricted to Israel.
+reuters                 reuters.tv           Yes   Yes
 rtbf                    - rtbf.be/auvio      Yes   Yes   Streams may be geo-restricted to Belgium or Europe.
                         - rtbfradioplayer.be
 rtlxl                   rtlxl.nl             No    Yes   Streams may be geo-restricted to The Netherlands. Livestreams not supported.
diff --git a/src/streamlink/plugins/goodgame.py b/src/streamlink/plugins/goodgame.py
index db326bf5..209aef6f 100644
--- a/src/streamlink/plugins/goodgame.py
+++ b/src/streamlink/plugins/goodgame.py
@@ -1,12 +1,9 @@
 import re
-import logging
 
 from streamlink.plugin import Plugin
 from streamlink.stream import HLSStream
 from streamlink.utils import parse_json
 
-log = logging.getLogger(__name__)
-
 HLS_URL_FORMAT = "https://hls.goodgame.ru/hls/{0}{1}.m3u8"
 QUALITIES = {
     "1080p": "",
@@ -38,7 +35,7 @@ class GoodGame(Plugin):
 
         match = _ddos_re.search(res.text)
         if match:
-            log.debug("Anti-DDOS bypass...")
+            self.logger.debug("Anti-DDOS bypass...")
             headers["Cookie"] = match.group(1)
             res = self.session.http.get(self.url, headers=headers)
 
@@ -48,9 +45,10 @@ class GoodGame(Plugin):
             self.logger.error("Could not find channel info")
             return
 
-        log.debug("Found channel info: id={id} channelkey={channelkey} pid={streamkey} online={status}".format(**channel_info))
+        self.logger.debug("Found channel info: channelkey={channelkey} pid={streamkey} online={status}",
+                          **channel_info)
         if not channel_info['status']:
-            log.debug("Channel appears to be offline")
+            self.logger.debug("Channel appears to be offline")
 
         streams = {}
         for name, url_suffix in QUALITIES.items():
diff --git a/src/streamlink/plugins/reuters.py b/src/streamlink/plugins/reuters.py
new file mode 100644
index 00000000..75aaa306
--- /dev/null
+++ b/src/streamlink/plugins/reuters.py
@@ -0,0 +1,92 @@
+import logging
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import useragents, validate
+from streamlink.plugin.api.utils import itertags
+from streamlink.stream import HLSStream
+from streamlink.utils import parse_json
+
+log = logging.getLogger(__name__)
+
+
+class Reuters(Plugin):
+    _url_re = re.compile(r'https?://(?:www\.)?reuters\.tv')
+    _hls_re = re.compile(r'''(?<!')https://[^"';!<>]+\.m3u8''')
+    _json_re = re.compile(r'''(?P<data>{.*});''')
+    _data_schema = validate.Schema(
+        validate.transform(_json_re.search),
+        validate.any(
+            None,
+            validate.all(
+                validate.get('data'),
+                validate.transform(parse_json),
+                {
+                    'title': validate.text,
+                    'items': [
+                        {
+                            'title': validate.text,
+                            'type': validate.text,
+                            'resources': [
+                                {
+                                    'mimeType': validate.text,
+                                    'uri': validate.url(),
+                                    validate.optional('protocol'): validate.text,
+                                    validate.optional('entityType'): validate.text,
+                                }
+                            ]
+                        }
+                    ],
+                }
+            )
+        )
+    )
+
+    def __init__(self, url):
+        super(Reuters, self).__init__(url)
+        self.session.http.headers.update({'User-Agent': useragents.FIREFOX})
+        self.title = None
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def get_title(self):
+        if not self.title:
+            self._get_data()
+        return self.title
+
+    def _get_data(self):
+        res = self.session.http.get(self.url)
+        for script in itertags(res.text, 'script'):
+            if script.attributes.get('type') == 'text/javascript' and 'RTVJson' in script.text:
+                data = self._data_schema.validate(script.text)
+                if not data:
+                    continue
+                self.title = data['title']
+                for item in data['items']:
+                    if data['title'] == item['title']:
+                        log.trace('{0!r}'.format(item))
+                        log.debug('Type: {0}'.format(item['type']))
+                        for res in item['resources']:
+                            if res['mimeType'] == 'application/x-mpegURL':
+                                return res['uri']
+
+        # fallback
+        for title in itertags(res.text, 'title'):
+            self.title = title.text
+        m = self._hls_re.search(res.text)
+        if not m:
+            log.error('Unsupported PageType.')
+            return
+        return m.group(0)
+
+    def _get_streams(self):
+        hls_url = self._get_data()
+        if not hls_url:
+            return
+        log.debug('URL={0}'.format(hls_url))
+        return HLSStream.parse_variant_playlist(self.session, hls_url)
+
+
+__plugin__ = Reuters


Test patch:
diff --git a/tests/plugins/test_reuters.py b/tests/plugins/test_reuters.py
new file mode 100644
index 00000000..bdde76a6
--- /dev/null
+++ b/tests/plugins/test_reuters.py
@@ -0,0 +1,20 @@
+import unittest
+
+from streamlink.plugins.reuters import Reuters
+
+
+class TestPluginReuters(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'https://www.reuters.tv/l/PFJx/2019/04/19/way-of-the-cross-ritual-around-notre-dame-cathedral',
+            'https://www.reuters.tv/l/PFcO/2019/04/10/first-ever-black-hole-image-released-astrophysics-milestone',
+            'https://www.reuters.tv/p/WoRwM1a00y8',
+        ]
+        for url in should_match:
+            self.assertTrue(Reuters.can_handle_url(url), url)
+
+        should_not_match = [
+            'https://example.com/index.html',
+        ]
+        for url in should_not_match:
+            self.assertFalse(Reuters.can_handle_url(url), url)