Repository: streamlink/streamlink
Pull request #3326 (2020-11-08 11:52:20)

plugins.cdnbg: simplify and fix iframes without schema
Currently `plugins.cdnbg` has a bug where `urlparse.scheme` is being passed as arg to `urlparse`  (used in  `update_schema` ), but `urlparse` treats it as path (as its simply the protocol name, not a URL). Thus for a URL without a specified protocol, the script fails.

Changes:
- Remove a redundant `if` statement as `update_scheme` handles both cases (with/without scheme)
- Remove a redundant `if` statement as `iframe_url` is always defined
- Use `html.unescape` to unescape HTML-encoded strings (should me more of a robust)
- More robust check (via parsing), whether the supplied URL is already a target URL


Patch:
diff --git a/src/streamlink/plugins/cdnbg.py b/src/streamlink/plugins/cdnbg.py
index 38f77cd3..58351a5d 100644
--- a/src/streamlink/plugins/cdnbg.py
+++ b/src/streamlink/plugins/cdnbg.py
@@ -1,5 +1,6 @@
 import logging
 import re
+from html import unescape as html_unescape
 from urllib.parse import urlparse
 
 from streamlink.plugin import Plugin
@@ -43,29 +44,24 @@ class CDNBG(Plugin):
     def find_iframe(self, url):
         self.session.http.headers.update({"User-Agent": useragents.CHROME})
         res = self.session.http.get(self.url)
-        p = urlparse(url)
         for iframe_url in self.iframe_re.findall(res.text):
             if "googletagmanager" not in iframe_url:
-                log.debug(f"Found iframe: {iframe_url}")
-                iframe_url = iframe_url.replace("&#58;", ":")
-                if iframe_url.startswith("//"):
-                    return update_scheme(p.scheme, iframe_url)
-                else:
-                    return iframe_url
+                iframe_url = html_unescape(iframe_url)
+                return update_scheme(self.url, iframe_url)
 
     def _get_streams(self):
-        if "i.cdn.bg/live/" in self.url:
+        if "cdn.bg" in urlparse(self.url).netloc:
             iframe_url = self.url
         else:
             iframe_url = self.find_iframe(self.url)
 
-        if iframe_url:
-            res = self.session.http.get(iframe_url, headers={"Referer": self.url})
-            stream_url = update_scheme(self.url, self.stream_schema.validate(res.text))
-            log.warning("SSL Verification disabled.")
-            return HLSStream.parse_variant_playlist(self.session,
-                                                    stream_url,
-                                                    verify=False)
+        log.debug(f"Found iframe: {iframe_url}")
+        res = self.session.http.get(iframe_url, headers={"Referer": self.url})
+        stream_url = update_scheme(self.url, self.stream_schema.validate(res.text))
+        log.warning("SSL Verification disabled.")
+        return HLSStream.parse_variant_playlist(self.session,
+                                                stream_url,
+                                                verify=False)
 
 
 __plugin__ = CDNBG
diff --git a/src/streamlink/plugins/vlive.py b/src/streamlink/plugins/vlive.py
index 77cd3f9a..ea64311a 100644
--- a/src/streamlink/plugins/vlive.py
+++ b/src/streamlink/plugins/vlive.py
@@ -1,58 +1,106 @@
-import json
 import logging
 import re
 
-from streamlink.plugin import Plugin, PluginError
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream
+from streamlink.utils import parse_json
 
 log = logging.getLogger(__name__)
 
 
 class Vlive(Plugin):
-    _url_re = re.compile(r"https?://(?:www.)vlive\.tv/video/(\d+)")
-    _video_status = re.compile(r'oVideoStatus = (.+)<', re.DOTALL)
-    _video_init_url = "https://www.vlive.tv/video/init/view"
+    _url_re = re.compile(r"https://www\.vlive\.tv/(?P<format>video|post)/(?P<id>[0-9\-]+)")
+    _page_info = re.compile(r'window\.__PRELOADED_STATE__\s*=\s*({.*})\s*<', re.DOTALL)
+    _playinfo_url = "https://www.vlive.tv/globalv-web/vam-web/old/v3/live/{0}/playInfo"
+
+    _schema_video = validate.Schema(
+        validate.transform(_page_info.search),
+        validate.any(None, validate.all(
+            validate.get(1),
+            validate.transform(parse_json),
+            validate.any(validate.all(
+                {"postDetail": {"post": {"officialVideo": {
+                    "type": str,
+                    "videoSeq": int,
+                    validate.optional("status"): str,
+                }}}},
+                validate.get("postDetail"),
+                validate.get("post"),
+                validate.get("officialVideo")),
+                validate.all(
+                    {"postDetail": {"error": {
+                        "errorCode": str,
+                    }}},
+                    validate.get("postDetail"),
+                    validate.get("error")))
+        ))
+    )
+
+    _schema_stream = validate.Schema(
+        validate.transform(parse_json),
+        validate.all(
+            {"result": {"streamList": [{
+                "streamName": str,
+                "serviceUrl": str,
+            }]}},
+            validate.get("result"),
+            validate.get("streamList")
+        )
+    )
 
     @classmethod
     def can_handle_url(cls, url):
         return cls._url_re.match(url) is not None
 
-    @property
-    def video_id(self):
-        return self._url_re.match(self.url).group(1)
-
     def _get_streams(self):
-        vinit_req = self.session.http.get(self._video_init_url,
-                                          params=dict(videoSeq=self.video_id),
-                                          headers=dict(referer=self.url))
-        if vinit_req.status_code != 200:
-            raise PluginError('Could not get video init page (HTTP Status {})'
-                              .format(vinit_req.status_code))
-
-        video_status_js = self._video_status.search(vinit_req.text)
-        if not video_status_js:
-            raise PluginError('Could not find video status information!')
-
-        video_status = json.loads(video_status_js.group(1))
-
-        if video_status['viewType'] == 'vod':
-            raise PluginError('VODs are not supported')
+        video_json = self.session.http.get(self.url, headers={"Referer": self.url},
+                                           schema=self._schema_video)
+        if video_json is None:
+            log.error('Could not parse video page')
+            return
+
+        err = video_json.get('errorCode')
+        if err == 'common_700':
+            log.error('Available only to members of the channel')
+            return
+        elif err == 'common_702':
+            log.error('Vlive+ VODs are not supported')
+            return
+        elif err == 'common_404':
+            log.error('Could not find video page')
+            return
+        elif err is not None:
+            log.error('Unknown error code: {0}'.format(err))
+            return
+
+        if video_json['type'] == 'VOD':
+            log.error('VODs are not supported')
+            return
+
+        url_format, video_id = self._url_re.match(self.url).groups()
+        if url_format == 'post':
+            video_id = str(video_json['videoSeq'])
+
+        video_status = video_json.get('status')
+        if video_status == 'ENDED':
+            log.error('Stream has ended')
+            return
+        elif video_status != 'ON_AIR':
+            log.error('Unknown video status: {0}'.format(video_status))
+            return
+
+        stream_info = self.session.http.get(self._playinfo_url.format(video_id),
+                                            headers={"Referer": self.url},
+                                            schema=self._schema_stream)
 
-        if 'liveStreamInfo' not in video_status:
-            raise PluginError('Stream is offline')
-
-        stream_info = json.loads(video_status['liveStreamInfo'])
-
-        streams = dict()
         # All "resolutions" have a variant playlist with only one entry, so just combine them
-        for i in stream_info['resolutions']:
-            res_streams = HLSStream.parse_variant_playlist(self.session, i['cdnUrl'])
+        for i in stream_info:
+            res_streams = HLSStream.parse_variant_playlist(self.session, i['serviceUrl'])
             if len(res_streams.values()) > 1:
                 log.warning('More than one stream in variant playlist, using first entry!')
 
-            streams[i['name']] = res_streams.popitem()[1]
-
-        return streams
+            yield i['streamName'], res_streams.popitem()[1]
 
 
 __plugin__ = Vlive


Test patch:
diff --git a/tests/plugins/test_vlive.py b/tests/plugins/test_vlive.py
index 37b32be1..5e98e309 100644
--- a/tests/plugins/test_vlive.py
+++ b/tests/plugins/test_vlive.py
@@ -8,6 +8,7 @@ from streamlink.plugins.vlive import Vlive
 class TestPluginVlive:
     valid_urls = [
         ("https://www.vlive.tv/video/156824",),
+        ("https://www.vlive.tv/post/0-19740901",)
     ]
     invalid_urls = [
         ("https://www.vlive.tv/events/2019vheartbeat?lang=en",),