Repository: streamlink/streamlink
Pull request #1063 (2017-07-05 09:25:26)

plugins.vk: add support for vk.com vod/livestreams
Adds support for vk.com VoD and Livestream services.

Should solve #879.


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 1e09b3e8..c4ed1bb9 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -255,6 +255,7 @@ viasat              - tv3play.se         Yes   Yes   Streams may be geo-restrict
                     - play.tv3.lt
                     - juicyplay.se
 vidio               vidio.com            Yes   Yes
+vk                  vk.com               Yes   Yes
 vrtbe               vrt.be/vrtnu         Yes   Yes
 wattv               wat.tv               --    Yes
 webtv               web.tv               Yes   --
diff --git a/src/streamlink/plugins/vk.py b/src/streamlink/plugins/vk.py
new file mode 100755
index 00000000..e5a3b089
--- /dev/null
+++ b/src/streamlink/plugins/vk.py
@@ -0,0 +1,97 @@
+import json
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.plugin.api import validate
+from streamlink.compat import urlparse, unquote
+from streamlink.stream import HTTPStream, HLSStream
+from streamlink.utils import parse_json, update_scheme
+from streamlink.utils.crypto import unpad_pkcs5
+
+class VK(Plugin):
+    _url_re = re.compile(r"http(?:s)?://(\w+\.)?vk.com/video-[0-9]*_[0-9]*")
+    _url_catalog_re = re.compile(r"http(?:s)?://(\w+\.)?vk.com/videos-[0-9]*")
+    _livestream_sources_re = re.compile(r"<source src=\\\"(.*?)\\\" type=\\\"application\\\/vnd\.apple\.mpegurl\\\">")
+    _vod_sources_re = re.compile(r"<source src=\\\"(.*?)\\\" type=\\\"video\\\/mp4\\\">")
+    _vod_quality_re = re.compile(r"\.([0-9]*?)\.mp4")
+
+    @classmethod
+    def can_handle_url(cls, url):
+        if cls._url_catalog_re.match(url) is not None:
+            url = cls.follow_vk_redirect(url)
+            if url is None:
+                return False
+        return cls._url_re.match(url) is not None
+
+    @classmethod
+    def follow_vk_redirect(cls, url):
+        # If this is a 'videos' catalog URL with an video ID in the GET request, get that instead
+        parsed_url = urlparse(url)
+        if parsed_url.path.startswith('/videos-'):
+            query = {v[0]:v[1] for v in [q.split('=') for q in parsed_url.query.split('&')] if v[0] == 'z'}
+            try:
+                true_path = unquote(query['z']).split('/')[0]
+                return parsed_url.scheme + '://' + parsed_url.netloc + '/' + true_path
+            except KeyError:
+                # No redirect found in query string, so return the catalog url and fail later
+                return url
+        else:
+            return url
+
+    def _get_streams(self):
+        """
+        Find the streams for vk.com
+        :return:
+        """
+        # If this is a 'videos' catalog URL with an video ID in the GET request, get that instead
+        url = self.follow_vk_redirect(self.url)
+        if url != self.url:
+            self.logger.debug('Grabbing data from real video page at {}', url)
+
+        headers = {}
+        res = http.get(url, headers=headers)
+        headers["Referer"] = url
+
+        # Try and find an HLS source (livestream)
+        stream_urls = self._livestream_sources_re.findall(res.text)
+        if len(stream_urls):
+            stream_url = stream_urls[0].replace('\/', '/')
+            self.logger.debug("Found live stream at {}", stream_url)
+            try:
+                # try to parse the stream as a variant playlist
+                variant = HLSStream.parse_variant_playlist(self.session, stream_url, headers=headers)
+                if variant:
+                    for q, s in variant.items():
+                        yield q, s
+                else:
+                    # and if that fails, try it as a plain HLS stream
+                    yield 'live', HLSStream(self.session, stream_url, headers=headers)
+            except IOError:
+                self.logger.warning("Could not open the stream, perhaps the channel is offline")
+            return
+
+        # Try and find a set of MP4 sources (VOD)
+        vod_urls = self._vod_sources_re.findall(res.text)
+        if len(vod_urls):
+            vod_urls = [v.replace('\/', '/') for v in vod_urls]
+            # Try to get quality from URL
+            qualities = {}
+            for s in vod_urls:
+                q = self._vod_quality_re.findall(s)
+                if not len(q):
+                    break
+                qualities[s] = q[0]
+
+            try:
+                if len(qualities) == len(vod_urls):
+                    for s in vod_urls:
+                        yield qualities[s] + 'p', HTTPStream(self.session, s)
+                else:
+                    # Fall back to numerical ranking
+                    for q, s in enumerate(vod_urls):
+                        yield str(q), HTTPStream(self.session, s)
+            except IOError:
+                self.logger.warning("Could not open the stream, perhaps the channel is offline")
+
+__plugin__ = VK


Test patch:
diff --git a/tests/test_plugin_vk.py b/tests/test_plugin_vk.py
new file mode 100755
index 00000000..e0d3e06f
--- /dev/null
+++ b/tests/test_plugin_vk.py
@@ -0,0 +1,35 @@
+import unittest
+
+from streamlink.plugins.vk import VK
+
+
+class TestPluginVK(unittest.TestCase):
+    def test_follow_vk_redirect(self):
+        # should redirect
+        self.assertEqual(VK.follow_vk_redirect(
+            "https://vk.com/videos-24136539?z=video-24136539_456241176%2Fclub24136539%2Fpl_-24136539_-2"),
+            "https://vk.com/video-24136539_456241176"
+        )
+        self.assertEqual(VK.follow_vk_redirect(
+            "https://vk.com/videos-24136539?z=video-24136539_456241181%2Fpl_-24136539_-2"),
+            "https://vk.com/video-24136539_456241181"
+        )
+
+        # shouldn't redirect
+        self.assertEqual(VK.follow_vk_redirect("http://vk.com/"), "http://vk.com/")
+        self.assertEqual(VK.follow_vk_redirect("http://vk.com/videos-24136539"), "http://vk.com/videos-24136539")
+        self.assertEqual(VK.follow_vk_redirect("http://www.youtube.com/"), "http://www.youtube.com/")
+
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(VK.can_handle_url("https://vk.com/video-9944999_456239622"))
+        self.assertTrue(VK.can_handle_url("http://vk.com/video-24136539_456239830"))
+        self.assertTrue(VK.can_handle_url("https://www.vk.com/video-34453259_456240574"))
+        self.assertTrue(VK.can_handle_url("https://vk.com/videos-24136539?z=video-24136539_456241155%2Fpl_-24136539_-2"))
+
+        # shouldn't match
+        self.assertFalse(VK.can_handle_url("https://vk.com/"))
+        self.assertFalse(VK.can_handle_url("https://vk.com/restore"))
+        self.assertFalse(VK.can_handle_url("https://www.vk.com/videos-24136539"))
+        self.assertFalse(VK.can_handle_url("http://vk.com/videos-24136539"))
+        self.assertFalse(VK.can_handle_url("http://www.youtube.com/"))