Repository: streamlink/streamlink
Pull request #2187 (2018-12-01 22:52:49)

plugins.sbscokr: New Plugin for http://play.sbs.co.kr/onair/pc/index.html
SBS is a national South Korean television and radio network company.
- https://en.wikipedia.org/wiki/Seoul_Broadcasting_System

closes https://github.com/streamlink/streamlink/issues/1106


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 81d3ec1d..6f61a4ed 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -165,6 +165,7 @@ rtpplay                 rtp.pt/play          Yes   Yes   Streams may be geo-rest
 rtve                    rtve.es              Yes   No
 rtvs                    rtvs.sk              Yes   No    Streams may be geo-restricted to Slovakia.
 ruv                     ruv.is               Yes   Yes   Streams may be geo-restricted to Iceland.
+sbscokr                 play.sbs.co.kr       Yes   No    Streams may be geo-restricted to South Korea.
 schoolism               schoolism.com        --    Yes   Requires a login and a subscription.
 senategov               senate.gov           --    Yes   Supports hearing streams.
 showroom                showroom-live.com    Yes   No    Only RTMP streams are available.
diff --git a/src/streamlink/plugins/sbscokr.py b/src/streamlink/plugins/sbscokr.py
new file mode 100644
index 00000000..52865869
--- /dev/null
+++ b/src/streamlink/plugins/sbscokr.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+import logging
+import random
+import re
+
+from streamlink.plugin import Plugin, PluginArguments, PluginArgument
+from streamlink.plugin.api import useragents, validate
+from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+class SBScokr(Plugin):
+
+    api_channel = 'http://apis.sbs.co.kr/play-api/1.0/onair/channel/{0}'
+    api_channels = 'http://static.apis.sbs.co.kr/play-api/1.0/onair/channels'
+
+    _url_re = re.compile(r'https?://play\.sbs\.co\.kr/onair/pc/index.html')
+
+    _channels_schema = validate.Schema({
+        'list': [{
+            'channelname': validate.all(
+                validate.text,
+            ),
+            'channelid': validate.text,
+            validate.optional('type'): validate.text,
+        }]},
+        validate.get('list'),
+    )
+
+    _channel_schema = validate.Schema(
+        {
+            'onair': {
+                'info': {
+                    'onair_yn': validate.text,
+                    'overseas_yn': validate.text,
+                    'overseas_text': validate.text,
+                },
+                'source': {
+                    'mediasourcelist': validate.any([{
+                        validate.optional('default'): validate.text,
+                        'mediaurl': validate.text,
+                    }], [])
+                },
+            }
+        },
+        validate.get('onair'),
+    )
+
+    arguments = PluginArguments(
+        PluginArgument(
+            'id',
+            metavar='CHANNELID',
+            type=str.upper,
+            help='''
+            Channel ID to play.
+
+            Example:
+
+                %(prog)s http://play.sbs.co.kr/onair/pc/index.html best --sbscokr-id S01
+
+            '''
+        )
+    )
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def _get_streams(self):
+        user_channel_id = self.get_option('id')
+
+        self.session.http.headers.update({'User-Agent': useragents.FIREFOX})
+        res = self.session.http.get(self.api_channels)
+        res = self.session.http.json(res, schema=self._channels_schema)
+
+        channels = {}
+        for channel in sorted(res, key=lambda x: x['channelid']):
+            if channel.get('type') in ('TV', 'Radio'):
+                channels[channel['channelid']] = channel['channelname']
+
+        log.info('Available IDs: {0}'.format(', '.join(
+            '{0} ({1})'.format(key, value) for key, value in channels.items())))
+        if not user_channel_id:
+            log.error('No channel selected, use --sbscokr-id CHANNELID')
+            return
+        elif user_channel_id and user_channel_id not in channels.keys():
+            log.error('Channel ID "{0}" is not available.'.format(user_channel_id))
+            return
+
+        params = {
+            'v_type': '2',
+            'platform': 'pcweb',
+            'protocol': 'hls',
+            'jwt-token': '',
+            'rnd': random.randint(50, 300)
+        }
+
+        res = self.session.http.get(self.api_channel.format(user_channel_id),
+                                    params=params)
+        res = self.session.http.json(res, schema=self._channel_schema)
+
+        for media in res['source']['mediasourcelist']:
+            if media['mediaurl']:
+                for s in HLSStream.parse_variant_playlist(self.session,
+                                                          media['mediaurl']).items():
+                    yield s
+        else:
+            if res['info']['onair_yn'] != 'Y':
+                log.error('This channel is currently unavailable')
+            elif res['info']['overseas_yn'] != 'Y':
+                log.error(res['info']['overseas_text'])
+
+
+__plugin__ = SBScokr


Test patch:
diff --git a/tests/plugins/test_sbscokr.py b/tests/plugins/test_sbscokr.py
new file mode 100644
index 00000000..1dff264e
--- /dev/null
+++ b/tests/plugins/test_sbscokr.py
@@ -0,0 +1,19 @@
+import unittest
+
+from streamlink.plugins.sbscokr import SBScokr
+
+
+class TestPluginSBScokr(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'https://play.sbs.co.kr/onair/pc/index.html',
+            'http://play.sbs.co.kr/onair/pc/index.html',
+        ]
+        for url in should_match:
+            self.assertTrue(SBScokr.can_handle_url(url))
+
+        should_not_match = [
+            'https://example.com/index.html',
+        ]
+        for url in should_not_match:
+            self.assertFalse(SBScokr.can_handle_url(url))