Large

YouTube Center Download Button

By YePpHa Last update Aug 11, 2013 — Installed 19,274 times.

There are 12 previous versions of this script.

// ==UserScript==
// @name            YouTube Center Only Download
// @version         1.2
// @author          Jeppe Rune Mortensen (YePpHa)
// @description     This adds the download button from YouTube Center.
// @match           http://*.youtube.com/watch?*
// @match           https://*.youtube.com/watch?*
// @match           http://s.ytimg.com/yts/jsbin/*
// @match           https://s.ytimg.com/yts/jsbin/*
// @match           http://*.youtube.com/verify_age?next_url=/watch*
// @match           https://*.youtube.com/verify_age?next_url=/watch*
// @match           http://userscripts.org/scripts/source/124921.meta.js
// @updateVersion   9
// ==/UserScript==
(function(){
  function xhr(details) {
    if (typeof GM_xmlhttpRequest != "undefined") {
      GM_xmlhttpRequest(details);
      return true;
    } else {
      var xmlhttp;
      if (typeof XMLHttpRequest != "undefined") {
        xmlhttp = new XMLHttpRequest();
      } else if (typeof opera != "undefined" && typeof opera.XMLHttpRequest != "undefined") {
        xmlhttp = new opera.XMLHttpRequest();
      } else if (typeof uw != "undefined" && typeof uw.XMLHttpRequest != "undefined") {
        xmlhttp = new uw.XMLHttpRequest();
      } else {
        if (details["onerror"]) {
          details["onerror"]();
        }
        return false;
      }
      xmlhttp.onreadystatechange = function(){
        var responseState = {
          responseXML:(xmlhttp.readyState == 4 ? xmlhttp.responseXML : ''),
          responseText:(xmlhttp.readyState == 4 ? xmlhttp.responseText : ''),
          readyState:xmlhttp.readyState,
          responseHeaders:(xmlhttp.readyState == 4 ? xmlhttp.getAllResponseHeaders() : ''),
          status:(xmlhttp.readyState == 4 ? xmlhttp.status : 0),
          statusText:(xmlhttp.readyState == 4 ? xmlhttp.statusText : '')
        };
        if (details["onreadystatechange"]) {
          details["onreadystatechange"](responseState);
        }
        if (xmlhttp.readyState == 4) {
          if (details["onload"] && xmlhttp.status >= 200 && xmlhttp.status < 300) {
            details["onload"](responseState);
          }
          if (details["onerror"] && (xmlhttp.status < 200 || xmlhttp.status >= 300)) {
            details["onerror"](responseState);
          }
        }
      };
      try {
        xmlhttp.open(details.method, details.url);
      } catch(e) {
        if(details["onerror"]) {
          details["onerror"]({responseXML:'',responseText:'',readyState:4,responseHeaders:'',status:403,statusText:'Forbidden'});
        }
        return false;
      }
      if (details.headers) {
        for (var prop in details.headers) {
          xmlhttp.setRequestHeader(prop, details.headers[prop]);
        }
      }
      xmlhttp.send((typeof(details.data) != 'undefined') ? details.data : null);
      return true;
    }
    return false;
  }
  
  function getTitle() {
    var meta = document.getElementsByTagName("meta");
    for (var i = 0; i < meta.length; i++) {
      if (meta[i].getAttribute("name") == "title") {
        return meta[i].getAttribute("content");
      }
    }
  }
  
  function getFormatType(format) {
    if (streamTypes[format]) {
      return streamTypes[format];
    }
    return "Unknown";
  }
  
  function getQuality(quality) {
    if (streamQualities[quality]) {
      return streamQualities[quality];
    }
    return "Unknown";
  }
  
  function getQualityResolution(quality) {
    if (streamResolutions[quality]) {
      return streamResolutions[quality];
    }
    return "Unknown";
  }
  
  function getSignature(stream) {
    if (stream.sig) return stream.sig;
    if (stream.s) return decipherSignature(stream.s);
  }
   
  function updateSignatureDecipher() {
    var js = config.assets.js,
        regex = /function [a-zA-Z$0-9]+\(a\){a=a\.split\(""\);(.*?)return a\.join\(""\)}/g,
        regex2 = /function [a-zA-Z$0-9]+\(a\){a=a\.split\(""\);(((a=([a-zA-Z$0-9]+)\(a,([0-9]+)\);)|(a=a\.slice\([0-9]+\);)|(a=a\.reverse\(\);)|(var b=a\[0\];a\[0\]=a\[[0-9]+%a\.length\];a\[[0-9]+\]=b;)))*return a\.join\(""\)}/g;
      xhr({
        method: "GET",
        url: js,
        onload: function(response) {
          var a,i,b,v;
          
          if (response.responseText.match(regex2)) {
            a = regex2.exec(response.responseText)[0].split("{")[1].split("}")[0].split(";");
            _signatureDecipher = []; // Clearing signatureDecipher
            for (i = 1; i < a.length-1; i++) {
              b = a[i];
              if (b.indexOf("a.slice") !== -1) { // Slice
                v = b.split("(")[1].split(")")[0];
                _signatureDecipher.push({func: "slice", value: parseInt(v)});
              } else if (b.indexOf("a.reverse") !== -1) { // Reverse
                _signatureDecipher.push({func: "reverse", value: null});
              } else if ((a[i] + ";" + a[i+1] + ";" + a[i+2]).indexOf("var b=a[0];a[0]=a[") !== -1){ // swapHeadAndPosition
                v = (a[i] + ";" + a[i+1] + ";" + a[i+2]).split("var b=a[0];a[0]=a[")[1].split("%")[0];
                _signatureDecipher.push({func: "swapHeadAndPosition", value: parseInt(v)});
                i = i+2;
              } else { // swapHeadAndPosition (maybe it's deprecated by YouTube)
                v = b.split("(a,")[1].split(")")[0];
                _signatureDecipher.push({func: "swapHeadAndPosition", value: parseInt(v)});
              }
            }
          } else {
            a = regex.exec(response.responseText)[1];
            _signatureDecipher = []; // Clearing signatureDecoder
            _signatureDecipher.push({func: "code", value: a});
          }
        },
        onerror: function() {console.error("YTC[Only Download] => IO Error!");}
      });
  }
  function decipherSignature(signatureCipher, decipherRecipe){
    function swapHeadAndPosition(array, position) {
      var head = array[0];
      var other = array[position % array.length];
      array[0] = other;
      array[position] = head;
      return array;
    }
    if (!signatureCipher) return "";
    var cipherArray = signatureCipher.split(""), i;
    decipherRecipe = decipherRecipe || _signatureDecipher;
    
    for (i = 0; i < _signatureDecipher.length; i++) {
      if (_signatureDecipher[i].func === "code") {
        cipherArray = new Function("a", _signatureDecipher[i].value + "return a.join(\"\")")(cipherArray);
        if (!isArray(cipherArray) && decipherRecipe !== __signatureDecipher) {
          return signatureDecipher(signatureCipher, __signatureDecipher);
        }
      } else if (_signatureDecipher[i].func === "swapHeadAndPosition") {
        cipherArray = swapHeadAndPosition(cipherArray, _signatureDecipher[i].value);
      } else if (_signatureDecipher[i].func === "slice") {
        cipherArray = cipherArray.slice(_signatureDecipher[i].value);
      } else if (_signatureDecipher[i].func === "reverse") {
        cipherArray = cipherArray.reverse();
      }
    }
    
    return cipherArray.join("");
  }
  
  function downloadFile(itag) {
    if (!downloadIFrameElement) {
      initDownloadFrame();
    }
    
    for (var i = 0; i < streamInformation.length; i++) {
      if (streamInformation[i].itag == itag) {
        downloadIFrameElement.setAttribute("src", streamInformation[i].url + "&title=" + encodeURIComponent(title) + "&signature=" + encodeURIComponent(getSignature(streamInformation[i])));
        break;
      }
    }
  }
  
  function createYouTubeButton(title, html, menu, classNames, onclick) {
    var button = document.createElement("button");
    button.innerHTML = html;
    if (menu) {
      button.appendChild(menu);
    }
    button.className = "yt-uix-button yt-uix-button-text yt-uix-tooltip" + (classNames ? " " + classNames : "");
    button.style.marginTop = "1px";
    button.setAttribute("title", title);
    button.setAttribute("type", "button");
    button.setAttribute("aria-pressed", "false");
    button.setAttribute("onclick", ";return false;");
    button.setAttribute("role", "button");
    button.setAttribute("data-tooltip-text", title);
    if (onclick) {
      button.addEventListener("click", onclick, false);
    }
    return button;
  }
  
  function createYouTubeMenu(build) {
    var menu = document.createElement("div");
    menu.className = "yt-uix-button-menu hid";
    for (var i = 0; i < build.length; i++) {
      var item = document.createElement("span");
      item.className = (build[i].className || build[i].className == "" ? build[i].className : "yt-uix-button-menu-item");
      if (build[i].style) {
        item.setAttribute("style", build[i].style);
      }
      item.setAttribute("onclick", ";return false;");
      item.innerHTML = build[i].text;
      if (build[i].onclick) {
        item.addEventListener("click", build[i].onclick, false);
      }
      if (build[i].args) {
        for (var key in build[i].args) {
          item.setAttribute(key, build[i].args[key]);
        }
      }
      if (build[i].register) {
        build[i].register(item);
      }
      menu.appendChild(item);
    }
    return menu;
  }

  function splitYouTubeInformationByFormat(informations) {
    var collection = {};
    for (var i = 0; i < informations.length; i++) {
      if (!collection[informations[i].type.format]) {
        collection[informations[i].type.format] = [];
      }
      var l = collection[informations[i].type.format].length;
      collection[informations[i].type.format][l] = informations[i];
    }
    return collection;
  }
  
  function getFormatTitle(f) {
    return getQuality(f.quality) + ", " + getQualityResolution(f.quality) + " (" + f.dimension + ")" + (f.stereo3d && f.stereo3d == 1 ? "<span style=\"float:right\">&nbsp;3D</span>" : "");
  }
  
  function is3D(f) {
    return (f.stereo3d && f.stereo3d == 1 ? true : false);
  }
  
  function getYouTubePlayerConfigVariables() {
    var uw = (function(){
      var a;
      try {
        a = unsafeWindow === window ? false : unsafeWindow;
      } finally {
        return a || (function(){
          try {
            var e = document.createElement('p');
            e.setAttribute('onclick', 'return window;');
            return e.onclick();
          } catch (e) {
          }
        }());
      }
    })();
    if (uw && uw.ytplayer && uw.ytplayer.config) return uw.ytplayer.config;
    if (document.body.innerHTML.indexOf("<script>var ytplayer = ytplayer || {};ytplayer.config = ") !== -1) {
      return JSON.parse(document.body.innerHTML.split("<script>var ytplayer = ytplayer || {};ytplayer.config = ")[1].split(";</script>")[0]);
    }
  }

  function parseYouTubeFmtList(list) {
    var parts = list.split(",");
    var collection = [];
    for (var i = 0; i < parts.length; i++) {
      var a = parts[i].split("/");
      collection[collection.length] = {
        itag: a[0],
        dimension: a[1],
        unknown1: a[2],
        unknown2: a[3],
        unknown3: a[4]
      };
    }
    return collection;
  }

  function parseYouTubeStreamList(list) {
    var parts = list.split(",");
    var collection = [];
    for (var i = 0; i < parts.length; i++) {
      var coll = {};
      var args = parts[i].split("&");
      for (var j = 0; j < args.length; j++) {
        var keys = args[j].split("=");
        coll[keys[0]] = unescape(keys[1]);
      }
      if (coll.type.match(/[0-9a-zA-Z]+\/[0-9a-zA-Z]+;\+(.*)/)) {
        var tmp = coll.type.split(";");
        
        var ff = tmp[0];
        var fe = "";
        if (tmp.length > 2) {
          var t = "";
          for (var j = 1; j < tmp.length; j++) {
            t += tmp[j];
          }
          fe = t;
        } else {
          fe = tmp[1];
        }
        coll.type = {
          format: ff,
          extra: fe
        };
      } else {
        coll.type = {
          format: coll.type,
          extra: ""
        };
      }
      collection[collection.length] = coll;
    }
    return collection;
  }
  
  function checkForUpdates() {
    xhr({
      method: "GET",
      url: "http://userscripts.org/scripts/source/" + updateId + ".meta.js",
      headers: {
        "Content-Type": "text/plain"
      },
      onload: function (response) {
        var nv;
        if (response.readyState === 4 && response.status === 200) {
          nv = parseInt(/^\/\/ @updateVersion\s+([0-9]+)$/m.exec(response.responseText)[1], 10);
          if (nv && nv > updateVersion) {
            if (updateElement) {
              updateElement.style.display = "block";
            }
          }
        }
      }
    });
  }

  function parseYouTubeFormats(ytplayer_config) {
    var fmtList = parseYouTubeFmtList(ytplayer_config.args.fmt_list);
    var streamList = parseYouTubeStreamList(ytplayer_config.args.url_encoded_fmt_stream_map);
    var collection = [];
    for (var i = 0; i < streamList.length; i++) {
      var fl = null;
      for (var j = 0; j < fmtList.length; j++) {
        if (streamList[i].itag != fmtList[j].itag) continue;
        fl = fmtList[j];
        break;
      }
      if (fl == null) {
        collection[collection.length] = streamList[i];
      } else {
        var coll = streamList[i];
        coll.dimension = fl.dimension;
        collection[collection.length] = coll;
      }
    }
    return collection;
  }
  
  function initUI() {
    var b = [];
    for (var key in stream) {
      b[b.length] = {
        text: "<b>" + getFormatType(key) + "</b>",
        className: "",
        style: "color:#666;font-size:0.9166em;padding-left:9px;"
      };
      for (var i = 0; i < stream[key].length; i++) {
        b[b.length] = {
          text: getFormatTitle(stream[key][i]),
          onclick: function() {
            downloadFile(this.getAttribute("itag"));
          },
          args: {
            itag: stream[key][i].itag
          }
        };
      }
    }
    b[b.length] = {
      text: "New update available, <a href=\"http://userscripts.org/scripts/show/" + updateId + "\">Install</a>.",
      className: "",
      style: "padding-top:7px;display:none;border-top:1px solid #666;text-align:center;",
      register: function(elm){
        updateElement = elm;
      }
    };
    var downloadMenu = createYouTubeMenu(b);
    var downloadButton = downloadButton = createYouTubeButton("Download List", "<span class=\"yt-uix-button-content\">Download</span><img class=\"yt-uix-button-arrow\" src=\"//s.ytimg.com/yt/img/pixel-vfl3z5WfW.gif\" alt=\"\">", downloadMenu, null, null);
    document.getElementById("watch7-sentiment-actions").appendChild(downloadButton);
  }
  
  function initDownloadFrame() {
    downloadIFrameElement = document.createElement("iframe");
    downloadIFrameElement.style.position = "absolute";
    downloadIFrameElement.style.top = "-100px";
    downloadIFrameElement.style.left = "-100px";
    downloadIFrameElement.style.width = "1px";
    downloadIFrameElement.style.height = "1px";
    downloadIFrameElement.style.border = "0";
    downloadIFrameElement.style.margin = "0";
    downloadIFrameElement.style.padding = "0";
    document.body.appendChild(downloadIFrameElement);
  }
  
  function init() {
    if (streamInformation.length > 0 && streamInformation[0].s)
      updateSignatureDecipher();
    initUI();
    checkForUpdates();
  }
  if (!location.href.match(/^(http|https)\:\/\/(.*?)\.youtube\.com\/watch\?/))
    return;
  var updateVersion = 9,
      updateId = "124921",
      streamTypes = {
        'video/x-flv': 'FLV',
        'video/mp4': 'MP4',
        'video/webm': 'WebM',
        'video/3gpp': '3GP'
      },
      streamQualities = {
        'small': 'Low Definition',
        'medium': 'Standard Definition',
        'large': 'High Definition',
        'hd720': 'HD Definition',
        'hd1080': 'Full HD Definition',
        'highres': 'Original Definition'
      },
      streamResolutions = {
        'small': '240p',
        'medium': '360p',
        'large': '480p',
        'hd720': '720p',
        'hd1080': '1080p',
        'highres': 'Original'
      },
      downloadIFrameElement = null,
      updateElement = null,
      __signatureDecipher = [
        {func: "slice", value: 3},
        {func: "reverse", value: null},
        {func: "swapHeadAndPosition", value: 63},
        {func: "slice", value: 2},
        {func: "reverse", value: null},
        {func: "slice", value: 1}
      ],
      _signatureDecipher = __signatureDecipher,
      title = getTitle(),
      config = getYouTubePlayerConfigVariables(),
      streamInformation = parseYouTubeFormats(config),
      stream = splitYouTubeInformationByFormat(streamInformation);
  init();
})();