How to download protected Vimeo videos

A website I recently purchased a membership to offers the option to download their course and lesson content. Unfortunately, the only download link they offer is at 1080p video resolution, which while nice, is much larger than necessary. The videos are offered through Vimeo, but are all unlisted, private, and can only be embedded on a single domain. Given that it would be entirely legal (and not outside of any particular moral code) for me to download the full HD version, downsample, and re-encode the video, I decided to write a script that would do this for me. Re-encoding takes forever, and I don’t have that kind of time.

The first thing we do is create an instance of Chrome that disables CORS. To start, create a shortcut to Chrome on your desktop. Name it something like “NO CORS” to distinguish that this shortcut will not be the one you want to use for your day-to-day browsing. Right click the shortcut and select Properties. In the Shortcut tab, change the Target field from:

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"

to:

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir="C:\Chrome_NO_CORS_safe-to-delete"

When you’re done, it should look something like this:

At the risk of sounding obvious, make sure your file paths are appropriate to your system. The user-data-dir path will be automatically created when this instance is run, so just make sure it points to an empty location. And as you can probably tell from the path I selected, the directory will be safe to delete whenever you feel like it. Re-running the shortcut will recreate it as needed.

When you run this instance of Chrome, you will be greeted by none of your extensions, configurations, or shortcuts, along with a nice warning bar:

You can see that I’ve already got a bookmarklet created titled “DL Vimeo” — To create this, right click the bookmarks bar and select “Add Page”

Name the bookmarklet whatever you like. For the script, you’ll be using the following code (don’t copy and paste just yet, though):

(function() {
    /* Let us begin... */
    console.log('Embed parser started...');
    $ = jQuery;
    var $players = $("iframe[src*='https://player.vimeo.com']");
    console.log($players.length + " vimeo embeds found. Scanning...");

    /* Seconds -> Xh Ym Zs */
    function timeFromSeconds(seconds)
    {
        var date = new Date(null);
        date.setSeconds(seconds);
        var utc = date.toUTCString();
        var time = ((date.getUTCHours() === 0) ? '' : date.getUTCHours() + 'h ') + date.getUTCMinutes() + 'm ' +  date.getUTCSeconds() + 's';
        return time;
    }
    /* Filenames don't like special characters */
    function safeFilename(filename)
    {
        return filename.replace(/[^a-z0-9_\-]/gi, '-').replace(/-{2,}/g, '-').toLowerCase();
    }
    /* Loop through each Vimeo iframe */
    $players.each(function(index) {
        
        /* We'll need these later. */
        var $embed = $(this);
        var xhr = new XMLHttpRequest();
        
        /* Let's make them a bit pretty */
        var styles = {
            "h1": "font-size: 1.8em; font-weight: bold; margin: 0 0 5px 5px; padding: 0;",
            "ul": "box-sizing: border-box; padding: 5px; margin: 5px 0; border: 1px solid red; background-color: #FFEFEF; width: " + $embed.width() + "px;",
            "li": "list-style: outside square; line-height: 1em; font-size: 1em; padding-left: 0; margin: 10px 0 10px 30px;",
            "a": "color: red;",
        }

        /* Get Vimeo ID */
        var src = $embed.attr("src");
        var pos = ((src.indexOf("?") !== -1) ? src.indexOf("?") : src.length);
        var id = src.substr(0, pos).split("/");
            id = id[id.length - 1];
        
        /* Here's the callback where the magic happens*/
        xhr.onreadystatechange = function() {
            if (xhr.readyState == XMLHttpRequest.DONE) {
                
                /* Begin processing your returned info; make sure the data matches the patterns below  */
                console.log("(" + (index + 1) + ") Content received for video " + id + ". Analyzing...");
                var text = xhr.responseText;
                if ((text.substring(0, 15) == "<!DOCTYPE html>") && (text.substr(text.length - 9) == '</script>')) {
                    
                    /* Response looks good. Let's get to work. */
                    script = text.substr(text.indexOf('<script>'));
                    
                    /* Extract the files JSON string */
                    var fileSearchFrom = '"request":';
                    var fileSearchTo   = '"player_url":';
                    var fileCode = "{" + script.substr(script.indexOf(fileSearchFrom), (script.indexOf(fileSearchTo) - script.indexOf(fileSearchFrom) - 1)) + "}";
                    
                    /* Extract the video JSON string */
                    var vidSearchFrom = '"video":';
                    var vidSearchTo   = '"build":';
                    var vidChunk = script.substr(script.indexOf(vidSearchFrom));
                    var vidCode = "{" + vidChunk.substr(0, (vidChunk.indexOf(vidSearchTo) - 1)) + "}";
                    
                    /* Parse our strings into objects we can use */
                    var fileObj = JSON.parse(fileCode);
                    var vidObj = JSON.parse(vidCode);
                    
                    /* Make sure you got a valid result */
                    if (fileObj.request)
                    {
                        /* Assign dynamic classname */
                        var className = "vimeo-download-links-" + id;
                        if ($("ul." + className).length) {
                            $("ul." + className).remove();
                        }
                        $ul = $('<ul class="' + className + '" style="' + styles.ul + '" />');
                        
                        /* Get the video info to build our links */
                        var files = fileObj.request.files.progressive;
                        var video = vidObj.video;
                        console.log(" > " + files.length + " options found... Generating download links...");
                        
                        /* Loop through the available files */
                        for (var i in files)
                        {
                            /* Get our vars ready */
                            f = files[i];
                            var videoMeta = f.quality + ', ' + f.fps + 'fps';
                            var anchor = "Download: " + f.mime + ' (' + videoMeta + ')';
                            var filename = safeFilename(video.title);
                            
                            /* Build our list items and links, attach to our <ul/> above */
                            $link = $('<li data-resolution-width="' + f.width + '" style="' + styles.li + '">' +
                                          '<a style="' + styles.a + '" href="' + f.url + '" title="Download \'' + filename + '\' (' + videoMeta + ')" download="' + filename + '" target="_blank">' + anchor + '</a>' +
                                      '</li>');
                            $link.appendTo($ul);
                        }
                        
                        /* Sort the list by video resolution (lowest -> higest) */
                        var listitems = $ul.children('li').get();
                        listitems.sort(function(a, b) {
                            return ($(b).attr("data-resolution-width") * 1) < ($(a).attr("data-resolution-width") * 1) ? 1 : -1;
                        })
                        $ul.empty().append(listitems);
                        
                        /* Add a title tag and stick the HTML under the scanned iframe */
                        $ul.prepend('<h1 style="' + styles.h1 + '">' + video.title + " (" + timeFromSeconds(video.duration) + ')</h1>');
                        $($embed).parent().after($ul);
                    }
                
                }
            }
        };
        
        /* Set up the rest of the xhr config and fire when ready */
        xhr.open('GET', src, true);
        xhr.send(null);
    });
})();

You can tell I’ve left a lot of comments scattered throughout there, mostly for my own ability to keep track of things as I modify them. In order to run JavaScript from a bookmarklet, you’ll need to prepend javascript: at the beginning of the code string. I also recommend running the above code through a minifier, although it’s not strictly necessary. Doing that will result in the following string:

javascript:!function(){function e(e){var t=new Date(null);t.setSeconds(e);t.toUTCString();return(0===t.getUTCHours()?"":t.getUTCHours()+"h ")+t.getUTCMinutes()+"m "+t.getUTCSeconds()+"s"}function t(e){return e.replace(/[^a-z0-9_\-]/gi,"-").replace(/-{2,}/g,"-").toLowerCase()}console.log("Embed parser started..."),$=jQuery;var r=$("iframe[src*='https://player.vimeo.com']");console.log(r.length+" vimeo embeds found. Scanning..."),r.each(function(r){var n=$(this),i=new XMLHttpRequest,s={h1:"font-size: 1.8em; font-weight: bold; margin: 0 0 5px 5px; padding: 0;",ul:"box-sizing: border-box; padding: 5px; margin: 5px 0; border: 1px solid red; background-color: #FFEFEF; width: "+n.width()+"px;",li:"list-style: outside square; line-height: 1em; font-size: 1em; padding-left: 0; margin: 10px 0 10px 30px;",a:"color: red;"},o=n.attr("src"),l=-1!==o.indexOf("?")?o.indexOf("?"):o.length,a=o.substr(0,l).split("/");a=a[a.length-1],i.onreadystatechange=function(){if(i.readyState==XMLHttpRequest.DONE){console.log("("+(r+1)+") Content received for video "+a+". Analyzing...");var o=i.responseText;if("<!DOCTYPE html>"==o.substring(0,15)&&"<\/script>"==o.substr(o.length-9)){script=o.substr(o.indexOf("<script>"));var l="{"+script.substr(script.indexOf('"request":'),script.indexOf('"player_url":')-script.indexOf('"request":')-1)+"}",d=script.substr(script.indexOf('"video":')),u="{"+d.substr(0,d.indexOf('"build":')-1)+"}",p=JSON.parse(l),c=JSON.parse(u);if(p.request){var g="vimeo-download-links-"+a;$("ul."+g).length&&$("ul."+g).remove(),$ul=$('<ul class="'+g+'" style="'+s.ul+'" />');var h=p.request.files.progressive,x=c.video;console.log(" > "+h.length+" options found... Generating download links...");for(var m in h){f=h[m];var v=f.quality+", "+f.fps+"fps",b="Download: "+f.mime+" ("+v+")",w=t(x.title);$link=$('<li data-resolution-width="'+f.width+'" style="'+s.li+'"><a style="'+s.a+'" href="'+f.url+'" title="Download \''+w+"' ("+v+')" download="'+w+'" target="_blank">'+b+"</a></li>"),$link.appendTo($ul)}var y=$ul.children("li").get();y.sort(function(e,t){return 1*$(t).attr("data-resolution-width")<1*$(e).attr("data-resolution-width")?1:-1}),$ul.empty().append(y),$ul.prepend('<h1 style="'+s.h1+'">'+x.title+" ("+e(x.duration)+")</h1>"),$(n).parent().after($ul)}}}},i.open("GET",o,!0),i.send(null)})}();

Dense, right? That’s ok, it’s not for humans to read. The point is, saving the above code block as the URL field in your new bookmarklet will allow you to go from this (names and titles blurred to protect the innocent):

…to this (you have to click the bookmarklet when you’re viewing the page with embedded videos, but that’s it):

Much better. Clicking any of the files will start automatically downloading a nicely-titled video file at your selected resolution.

Enjoy.

UPDATE 2019-12-19

The old code doesn’t work anymore. Use this snippet instead:

javascript:(function(){console.log('Embed parser started...');$=jQuery;var $players=$("iframe[src*='//player.vimeo.com']");console.log($players.length+" vimeo embeds found. Scanning...");function timeFromSeconds(seconds)
{var date=new Date(null);date.setSeconds(seconds);var time=((date.getUTCHours()===0)?'':date.getUTCHours()+'h ')+date.getUTCMinutes()+'m '+date.getUTCSeconds()+'s';return time}
function safeFilename(filename)
{return filename.replace(/[^a-z0-9_-]/gi,'-').replace(/-{2,}/g,'-').toLowerCase()}
$players.each(function(index){var $embed=$(this);var xhr=new XMLHttpRequest();var styles={"h1":"font-size: 1.8em; font-weight: bold; margin: 0 0 5px 5px; padding: 0;","ul":"box-sizing: border-box; padding: 5px; margin: 5px 0; border: 1px solid red; background-color: #FFEFEF; width: "+$embed.width()+"px;","li":"list-style: outside square; line-height: 1em; font-size: 1em; padding-left: 0; margin: 10px 0 10px 30px;","a":"color: red;",};var src=$embed.attr("src");var pos=((src.indexOf("?")!==-1)?src.indexOf("?"):src.length);var id=src.substr(0,pos).split("/");id=id[id.length-1];xhr.onreadystatechange=function(){if(xhr.readyState==XMLHttpRequest.DONE){var text=xhr.responseText;var scripts=$($.parseHTML(text,document,!0)).filter("script");var objects={};$.each(scripts,function(index,el){if(el.innerHTML.includes("var config"))
{var code=el.innerHTML;var length=code.length;var needle=code.indexOf("video/mp4");var reversed=code.split("").reverse().join("");var firstbracket=length-reversed.indexOf("[",(length-needle));var lastbracket=code.indexOf("]",needle);objects=JSON.parse("["+code.substring(firstbracket,lastbracket)+"]")}}.bind(this));var className="vimeo-download-links-"+id;if($("ul."+className).length){$("ul."+className).remove()}
$ul=$('<ul class="'+className+'" style="'+styles.ul+'" />');$.each(objects,function(index,el){var videoMeta=el.quality+', '+el.fps+'fps';$link=$('<li data-resolution-width="'+el.width+'" style="'+styles.li+'">'+'<a style="'+styles.a+'" href="'+el.url+'" title="Download ('+videoMeta+')" target="_blank">Download '+videoMeta+'</a>'+'</li>');$link.appendTo($ul)});var listitems=$ul.children('li').get();listitems.sort(function(a,b){return($(b).attr("data-resolution-width")*1)<($(a).attr("data-resolution-width")*1)?1:-1});$ul.empty().append(listitems);$($embed).parent().after($ul)}};xhr.open('GET',src,!0);xhr.send(null)})})()

Way more better! Er, I mean, at least it works again.