Repository: streamlink/streamlink

Issue:
[Plugin Request] Vlive.tv
<!--
Thanks for requesting a plugin!
USE THE TEMPLATE. Otherwise your plugin request may be rejected.

First, see the contribution guidelines and plugin request requirements:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink

Plugin requests which fall into the categories we will not implement will be closed immediately.

Also check the list of open and closed plugin requests:
https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+request%22

Please see the text preview to avoid unnecessary formatting errors.
-->


## Plugin Request

<!-- Replace [ ] with [x] in order to check the box -->
- [x] This is a plugin request and I have read the contribution guidelines and plugin request requirements.


### Description
This is a live streaming site.
This site was originally mainly for idols who want to interact with fans, but now it has grown more, many people have registered and broadcast a number of more diverse programs.
<!-- Explain the plugin and site as clearly as you can. What is the site about? Who runs it? What content does it provide? What value does it bring to Streamlink? Etc. -->


### Example stream URLs

<!-- Example URLs for streams are required. Plugin requests which do not have example URLs will be closed. -->

1. https://www.vlive.tv/ (Home page)
2. https://www.vlive.tv/upcoming (Upcoming broadcasts will be show here)


### Additional comments, screenshots, etc.
This site has no live streaming 24/7 so you can visit the homepage and look down, sometimes seeing some people broadcasting there (IMG: https://i.imgur.com/rHBMmli.jpg).

Or visit this page to see upcoming shows: https://www.vlive.tv/upcoming

Hope you can support this page, thanks so much.

[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)


Comments:

Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index b88febc7..1423894b 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -110,7 +110,7 @@ itvplayer               itv.com/itvplayer    Yes   Yes   Streams may be geo-rest
 kanal7                  - kanal7.com         Yes   No
                         - tvt.tv.tr
 kingkong                kingkong.com.tw      Yes   --
-linelive                live.line.me         Yes   --
+linelive                live.line.me         Yes   Yes
 live_russia_tv          live.russia.tv       Yes   --
 liveedu                 - liveedu.tv         Yes   --    Some streams require a login.
                         - livecoding.tv
@@ -267,6 +267,7 @@ vimeo                   vimeo.com            Yes   Yes   Password-protected vide
 vinhlongtv              thvli.vn             Yes   No    Streams are geo-restricted to Vietnam
 viutv                   viu.tv               Yes   No    Streams are geo-restricted to Hong Kong
 vk                      vk.com               Yes   Yes
+vlive                   vlive.tv             Yes   No    Embedded Naver VODs are not supported.
 vrtbe                   vrt.be/vrtnu         Yes   Yes
 vtvgo                   vtvgo.vn             Yes   No
 webcast_india_gov       webcast.gov.in       Yes   No    You can use #Channel to indicate CH number.
diff --git a/src/streamlink/plugins/linelive.py b/src/streamlink/plugins/linelive.py
index 24a2c33b..e524a32c 100644
--- a/src/streamlink/plugins/linelive.py
+++ b/src/streamlink/plugins/linelive.py
@@ -23,26 +23,43 @@ class LineLive(Plugin):
                 "360": validate.any(None, validate.url(scheme="http", path=validate.endswith(".m3u8"))),
                 "240": validate.any(None, validate.url(scheme="http", path=validate.endswith(".m3u8"))),
                 "144": validate.any(None, validate.url(scheme="http", path=validate.endswith(".m3u8"))),
-            })
-        }
-     )
+            }),
+            "archivedHLSURLs": validate.any(None, {
+                "720": validate.any(None, validate.url(scheme="http", path=validate.endswith(".m3u8"))),
+                "480": validate.any(None, validate.url(scheme="http", path=validate.endswith(".m3u8"))),
+                "360": validate.any(None, validate.url(scheme="http", path=validate.endswith(".m3u8"))),
+                "240": validate.any(None, validate.url(scheme="http", path=validate.endswith(".m3u8"))),
+                "144": validate.any(None, validate.url(scheme="http", path=validate.endswith(".m3u8"))),
+            }),
+        })
 
     @classmethod
     def can_handle_url(cls, url):
         return cls._url_re.match(url) is not None
 
+    def _get_live_streams(self, json):
+        for stream in json["liveHLSURLs"]:
+            url = json["liveHLSURLs"][stream]
+            if url is not None:
+                yield "{0}p.".format(stream), HLSStream(self.session, url)
+
+    def _get_vod_streams(self, json):
+        for stream in json["archivedHLSURLs"]:
+            url = json["archivedHLSURLs"][stream]
+            if url is not None:
+                yield "{0}p.".format(stream), HLSStream(self.session, url)
+
     def _get_streams(self):
         match = self._url_re.match(self.url)
         channel = match.group("channel")
         broadcast = match.group("broadcast")
         res = self.session.http.get(self._api_url.format(channel, broadcast))
         json = self.session.http.json(res, schema=self._player_status_schema)
-        if json["liveStatus"] != "LIVE":
-            return
-        for stream in json["liveHLSURLs"]:
-            url = json["liveHLSURLs"][stream]
-            if url != None:
-                yield "{0}p".format(stream), HLSStream(self.session, url)
+        if json["liveStatus"] == "LIVE":
+            return self._get_live_streams(json)
+        elif json["liveStatus"] == "FINISHED":
+            return self._get_vod_streams(json)
+        return
 
 
 __plugin__ = LineLive
diff --git a/src/streamlink/plugins/vlive.py b/src/streamlink/plugins/vlive.py
new file mode 100644
index 00000000..71a3f645
--- /dev/null
+++ b/src/streamlink/plugins/vlive.py
@@ -0,0 +1,55 @@
+import re
+import json
+
+from streamlink.plugin import Plugin, PluginError
+from streamlink.stream import HLSStream
+
+
+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"
+
+    @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')
+
+        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'])
+            if len(res_streams.values()) > 1:
+                self.logger.warning('More than one stream in variant playlist, using first entry!')
+
+            streams[i['name']] = res_streams.popitem()[1]
+
+        return streams
+
+
+__plugin__ = Vlive


Test patch:
diff --git a/tests/plugins/test_vlive.py b/tests/plugins/test_vlive.py
new file mode 100644
index 00000000..31f7c8f8
--- /dev/null
+++ b/tests/plugins/test_vlive.py
@@ -0,0 +1,23 @@
+import unittest
+
+import pytest
+
+from streamlink.plugins.vlive import Vlive
+
+
+class TestPluginVlive:
+    valid_urls = [
+        ("https://www.vlive.tv/video/156824",),
+    ]
+    invalid_urls = [
+        ("https://www.vlive.tv/events/2019vheartbeat?lang=en",),
+        ("https://twitch.tv/",)
+    ]
+
+    @pytest.mark.parametrize(["url"], valid_urls)
+    def test_can_handle_url(self, url):
+        assert Vlive.can_handle_url(url), "url should be handled"
+
+    @pytest.mark.parametrize(["url"], invalid_urls)
+    def test_can_handle_url_negative(self, url):
+        assert not Vlive.can_handle_url(url), "url should not be handled"