Repository: streamlink/streamlink
Pull request #2131 (2018-10-23 17:02:43)

plugins.vimeo: add support for vimeo.com
This plugin adds support for Vimeo. It supports both VOD and live streams. It also supports subtitles.


Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 664e80f6..a5ebb3c5 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -245,6 +245,7 @@ viasat                  - juicyplay.dk       Yes   Yes   Streams may be geo-rest
                         - viafree.no
                         - viafree.se
 vidio                   vidio.com            Yes   Yes
+vimeo                   vimeo.com            Yes   Yes   Password-protected videos are not supported.
 vinhlongtv              thvli.vn             Yes   No    Streams are geo-restricted to Vietnam
 vk                      vk.com               Yes   Yes
 vrtbe                   vrt.be/vrtnu         Yes   Yes
diff --git a/src/streamlink/plugins/vimeo.py b/src/streamlink/plugins/vimeo.py
new file mode 100644
index 00000000..cc5a519f
--- /dev/null
+++ b/src/streamlink/plugins/vimeo.py
@@ -0,0 +1,116 @@
+import logging
+import re
+
+from streamlink.compat import html_unescape, urlparse
+from streamlink.plugin import Plugin, PluginArguments, PluginArgument
+from streamlink.plugin.api import validate
+from streamlink.stream import DASHStream, HLSStream, HTTPStream
+from streamlink.stream.ffmpegmux import MuxedStream
+from streamlink.utils import parse_json
+
+log = logging.getLogger(__name__)
+
+
+class Vimeo(Plugin):
+    _url_re = re.compile(r"https?://(player\.vimeo\.com/video/\d+|(www\.)?vimeo\.com/.+)")
+    _config_url_re = re.compile(r'(?:"config_url"|\bdata-config-url)\s*[:=]\s*(".+?")')
+    _config_re = re.compile(r"var\s+config\s*=\s*({.+?})\s*;")
+    _config_url_schema = validate.Schema(
+        validate.transform(_config_url_re.search),
+        validate.any(
+            None,
+            validate.Schema(
+                validate.get(1),
+                validate.transform(parse_json),
+                validate.transform(html_unescape),
+                validate.url(),
+            ),
+        ),
+    )
+    _config_schema = validate.Schema(
+        validate.transform(parse_json),
+        {
+            "request": {
+                "files": {
+                    validate.optional("dash"): {"cdns": {validate.text: {"url": validate.url()}}},
+                    validate.optional("hls"): {"cdns": {validate.text: {"url": validate.url()}}},
+                    validate.optional("progressive"): validate.all(
+                        [{"url": validate.url(), "quality": validate.text}]
+                    ),
+                },
+                validate.optional("text_tracks"): validate.all(
+                    [{"url": validate.text, "lang": validate.text}]
+                ),
+            }
+        },
+    )
+    _player_schema = validate.Schema(
+        validate.transform(_config_re.search),
+        validate.any(None, validate.Schema(validate.get(1), _config_schema)),
+    )
+
+    arguments = PluginArguments(
+        PluginArgument(
+            "mux-subtitles",
+            action="store_true",
+            help="Automatically mux available subtitles in to the output stream.",
+        )
+    )
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url)
+
+    def _get_streams(self):
+        if "player.vimeo.com" in self.url:
+            data = self.session.http.get(self.url, schema=self._player_schema)
+        else:
+            api_url = self.session.http.get(self.url, schema=self._config_url_schema)
+            if not api_url:
+                return
+            data = self.session.http.get(api_url, schema=self._config_schema)
+
+        videos = data["request"]["files"]
+        streams = []
+
+        for stream_type in ("hls", "dash"):
+            if stream_type not in videos:
+                continue
+            for _, video_data in videos[stream_type]["cdns"].items():
+                log.trace("{0!r}".format(video_data))
+                url = video_data.get("url")
+                if stream_type == "hls":
+                    for stream in HLSStream.parse_variant_playlist(self.session, url).items():
+                        streams.append(stream)
+                elif stream_type == "dash":
+                    p = urlparse(url)
+                    if p.path.endswith("dash.mpd"):
+                        # LIVE
+                        url = self.session.http.get(url).json()["url"]
+                    elif p.path.endswith("master.json"):
+                        # VOD
+                        url = url.replace("master.json", "master.mpd")
+                    else:
+                        log.error("Unsupported DASH path: {0}".format(p.path))
+                        continue
+
+                    for stream in DASHStream.parse_manifest(self.session, url).items():
+                        streams.append(stream)
+
+        for stream in videos.get("progressive", []):
+            streams.append((stream["quality"], HTTPStream(self.session, stream["url"])))
+
+        if self.get_option("mux_subtitles") and data["request"].get("text_tracks"):
+            substreams = {
+                s["lang"]: HTTPStream(self.session, "https://vimeo.com" + s["url"])
+                for s in data["request"]["text_tracks"]
+            }
+            for quality, stream in streams:
+                yield quality, MuxedStream(self.session, stream, subtitles=substreams)
+        else:
+            for stream in streams:
+                yield stream
+
+
+__plugin__ = Vimeo
+


Test patch:
diff --git a/tests/plugins/test_vimeo.py b/tests/plugins/test_vimeo.py
new file mode 100644
index 00000000..050a19ef
--- /dev/null
+++ b/tests/plugins/test_vimeo.py
@@ -0,0 +1,19 @@
+import unittest
+
+from streamlink.plugins.vimeo import Vimeo
+
+
+class TestPluginVimeo(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Vimeo.can_handle_url("https://vimeo.com/237163735"))
+        self.assertTrue(Vimeo.can_handle_url("https://vimeo.com/channels/music/176894130"))
+        self.assertTrue(Vimeo.can_handle_url("https://vimeo.com/album/3706071/video/148903960"))
+        self.assertTrue(Vimeo.can_handle_url("https://vimeo.com/ondemand/surveyopenspace/92630739"))
+        self.assertTrue(Vimeo.can_handle_url("https://vimeo.com/ondemand/100footsurfingdays"))
+        self.assertTrue(Vimeo.can_handle_url("https://player.vimeo.com/video/176894130"))
+
+        # shouldn't match
+        self.assertFalse(Vimeo.can_handle_url("https://www.vimeo.com/"))
+        self.assertFalse(Vimeo.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(Vimeo.can_handle_url("http://www.youtube.com/"))