Greasy Fork is available in English.

YouTube Links

Tags every video link on a YouTube page so that they can be downloaded easily. On the watch page, it also shows all the available video formats at the top of the page. Now supports 256kbps MP4 audio!

2014-10-27 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

// ==UserScript==
// @name           YouTube Links
// @namespace      http://www.smallapple.net/
// @description    Tags every video link on a YouTube page so that they can be downloaded easily. On the watch page, it also shows all the available video formats at the top of the page. Now supports 256kbps MP4 audio!
// @author         Ng Hun Yang
// @include        http://*.youtube.com/*
// @include        http://youtube.com/*
// @include        https://*.youtube.com/*
// @include        https://youtube.com/*
// @match          *://*.youtube.com/*
// @match          *://*.googlevideo.com/*
// @match          *://s.ytimg.com/yts/jsbin/*
// @grant          GM_xmlhttpRequest
// @version        1.70
// ==/UserScript==

/* This is based on YouTube HD Suite 3.4.1 */

/* Tested on Firefox 5.0, Chrome 13 and Opera 11.50 */

(function() {

// =============================================================================

var win = typeof(unsafeWindow) !== "undefined" ? unsafeWindow : window;
var doc = win.document;
var loc = win.location;

var unsafeWin = win;

// Hack to get unsafe window in Chrome
(function() {

var isChrome = navigator.userAgent.toLowerCase().indexOf("chrome") >= 0;

if(!isChrome)
  return;

// Chrome 27 fixed this exploit, but luckily, its unsafeWin now works for us
try {
  var div = doc.createElement("div");
  div.setAttribute("onclick", "return window;");
  unsafeWin = div.onclick();
} catch(e) {
  }

}) ();

// =============================================================================

var SCRIPT_NAME = "YouTube Links";

var relInfo = {
  ver: 17000,
  ts: 2014102700,
  desc: "Add 256kbps M4A"
  };

var SCRIPT_UPDATE_LINK = loc.protocol + "//greasyfork.org/scripts/5565-youtube-links-updater/code/YouTube Links Updater.user.js";
var SCRIPT_LINK = loc.protocol + "//greasyfork.org/scripts/5566-youtube-links/code/YouTube Links.user.js";

// =============================================================================

var dom = {};

dom.gE = function(id) {
  return doc.getElementById(id);
  };

dom.gT = function(dom, tag) {
  if(arguments.length == 1) {
    tag = dom;
    dom = doc;
    }

  return dom.getElementsByTagName(tag);
  };

dom.cE = function(tag) {
  return document.createElement(tag);
  };

dom.cT = function(s) {
  return doc.createTextNode(s);
  };

dom.attr = function(obj, k, v) {
  if(arguments.length == 2)
    return obj.getAttribute(k);

  obj.setAttribute(k, v);
  };

dom.prepend = function(obj, child) {
  obj.insertBefore(child, obj.firstChild);
  };

dom.append = function(obj, child) {
  obj.appendChild(child);
  };

dom.offset = function(obj) {
  var x = 0;
  var y = 0;

  if(obj.getBoundingClientRect) {
    var box = obj.getBoundingClientRect();
    var owner = obj.ownerDocument;

    x = box.left + Math.max(owner.documentElement.scrollLeft, owner.body.scrollLeft) - owner.documentElement.clientLeft;
    y = box.top + Math.max(owner.documentElement.scrollTop, owner.body.scrollTop) - owner.documentElement.clientTop;

    return { left: x, top: y };
    }

  if(obj.offsetParent) {
    do {
      x += obj.offsetLeft - obj.scrollLeft;
      y += obj.offsetTop - obj.scrollTop;
      obj = obj.offsetParent;
    } while(obj);
    }

  return { left: x, top: y };
  };

dom.inViewport = function(el) {
  var rect = el.getBoundingClientRect();

  return rect.bottom >= 0 &&
   rect.right >= 0 &&
   rect.top < (win.innerHeight || doc.documentElement.clientHeight) &&
   rect.left < (win.innerWidth || doc.documentElement.clientWidth);
  };

dom.html = function(obj, s) {
  if(arguments.length == 1)
    return obj.innerHTML;

  obj.innerHTML = s;
  };

dom.emitHtml = function(tag, attrs, body) {
  if(arguments.length == 2) {
    if(typeof(attrs) == "string") {
      body = attrs;
      attrs = {};
      }
    }

  var list = [];

  for(var k in attrs) {
    list.push(k + "='" + attrs[k].replace(/'/g, "\\'") + "'");
    }

  var s = "<" + tag + " " + list.join(" ") + ">";

  if(body != null)
    s += body + "</" + tag + ">";

  return s;
  };

dom.emitCssStyles = function(styles) {
  var list = [];

  for(var k in styles) {
    list.push(k + ": " + styles[k] + ";");
    }

  return " { " + list.join(" ") + " }";
  };

dom.ajax = function(opts) {
  function newXhr() {
    if(window.ActiveXObject) {
      try {
        return new ActiveXObject("Msxml2.XMLHTTP");
        } catch(e) {
          }

      try {
        return new ActiveXObject("Microsoft.XMLHTTP");
        } catch(e) {
          return null;
          }
      }

    if(window.XMLHttpRequest)
      return new XMLHttpRequest();

    return null;
    }

  function nop() {
    }

  // Entry point
  var xhr = newXhr();

  opts = addProp({
    type: "GET",
    async: true,
    success: nop,
    error: nop,
    complete: nop
    }, opts);

  xhr.open(opts.type, opts.url, opts.async);

  xhr.onreadystatechange = function() {
    if(xhr.readyState == 4) {
      var status = +xhr.status;

      if(status >= 200 && status < 300) {
        opts.success(xhr.responseText, "success", xhr);
        }
      else {
        opts.error(xhr, "error");
        }

      opts.complete(xhr);
      }
    };

  xhr.send("");
  };

dom.crossAjax = function(opts) {
  function wrapXhr(xhr) {
    var headers = xhr.responseHeaders.replace("\r", "").split("\n");

    var obj = {};

    forEach(headers, function(idx, elm) {
      var nv = elm.split(":");
      if(nv[1] != null)
        obj[nv[0].toLowerCase()] = nv[1].replace(/^\s+/, "").replace(/\s+$/, "");
      });

    var responseXML = null;

    if(opts.dataType == "xml")
      responseXML = new DOMParser().parseFromString(xhr.responseText, "text/xml");

    return {
      responseText: xhr.responseText,
      responseXML: responseXML,
      status: xhr.status,

      getAllResponseHeaders: function() {
        return xhr.responseHeaders;
        },

      getResponseHeader: function(name) {
        return obj[name.toLowerCase()];
        }
      };
    }

  function nop() {
    }

  // Entry point
  opts = addProp({
    type: "GET",
    async: true,
    success: nop,
    error: nop,
    complete: nop
    }, opts);

  if(typeof GM_xmlhttpRequest === "undefined") {
    setTimeout(function() {
      var xhr = {};
      opts.error(xhr, "error");
      opts.complete(xhr);
      }, 0);
    return;
    }

  GM_xmlhttpRequest({
    method: opts.type,
    url: opts.url,
    synchronous: !opts.async,

    onload: function(xhr) {
      xhr = wrapXhr(xhr);

      if(xhr.status >= 200 && xhr.status < 300)
        opts.success(xhr.responseXML ? xhr.responseXML : xhr.responseText, "success", xhr);
      else
        opts.error(xhr, "error");

      opts.complete(xhr);
      },

    onerror: function(xhr) {
      xhr = wrapXhr(xhr);
      opts.error(xhr, "error");
      opts.complete(xhr);
      }
    });
  };

dom.addEvent = function(e, type, fn) {
  function mouseEvent(event) {
    if(this != event.relatedTarget && !dom.isAChildOf(this, event.relatedTarget))
      fn.call(this, event);
    }

  // Entry point
  if(e.addEventListener) {
    var effFn = fn;

    if(type == "mouseenter") {
      type = "mouseover";
      effFn = mouseEvent;
      }
    else if(type == "mouseleave") {
      type = "mouseout";
      effFn = mouseEvent;
      }

    e.addEventListener(type, effFn, /*capturePhase*/ false);
    }
  else
    e.attachEvent("on" + type, function() { fn(win.event); });
  };

dom.insertCss = function (styles) {
  var ss = dom.cE("style");
  dom.attr(ss, "type", "text/css");

  var hh = dom.gT("head") [0];
  dom.append(hh, ss);
  dom.append(ss, dom.cT(styles));
  };

dom.isAChildOf = function(parent, child) {
  if(parent === child)
    return false;

  while(child && child !== parent) {
    child = child.parentNode;
    }

  return child === parent;
  };

// -----------------------------------------------------------------------------

function timeNowInSec() {
  return Math.round(+new Date() / 1000);
  }

function forLoop(opts, fn) {
  opts = addProp({ start: 0, inc: 1 }, opts);

  for(var idx = opts.start; idx < opts.num; idx += opts.inc) {
    if(fn.call(opts, idx, opts) === false)
      break;
    }
  }

function forEach(list, fn) {
  forLoop({ num: list.length }, function(idx) {
    return fn.call(list[idx], idx, list[idx]);
    });
  }

function addProp(dest, src) {
  for(var k in src) {
    if(src[k] != null)
      dest[k] = src[k];
    }

  return dest;
  }

function unescHtmlEntities(s) {
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
  }

function logMsg(s) {
  win.console.log(s);
  }

function cnvSafeFname(s) {
  s = s.replace(/:/g, "-").replace(/"/g, "'").replace(/[\\/|*?]/g, "_");
  return encodeURIComponent(s).replace(/'/g, "%27");
  }

function getVideoName(s) {
  var list = [
    { name: "3GP", codec: "video/3gpp" },
    { name: "FLV", codec: "video/x-flv" },
    { name: "M4V", codec: "video/x-m4v" },
    { name: "MP3", codec: "audio/mpeg" },
    { name: "MP4", codec: "video/mp4" },
    { name: "M4A", codec: "audio/mp4" },
    { name: "QT", codec: "video/quicktime" },
    { name: "WEBM", codec: "audio/webm" },
    { name: "WEBM", codec: "video/webm" },
    { name: "WMV", codec: "video/ms-wmv" }
    ];

  var name = "?";

  forEach(list, function(idx, elm) {
    if(s.match("^" + elm.codec)) {
      name = elm.name;
      return false;
      }
    });

  return name;
  }

function snapToStdRes(res) {
  var horzResList = [ 3840, 2048, 1440, 960, 640, 480, 320, 176 ];
  var horzWideResList = [ 2880, 1920, 1280, 854, 640, 426, 256 ];
  var vertResList = [ 2160, 1536, 1080, 720, 480, 360, 240, 144 ];

  if(!res.match(/^(\d+)x(\d+)/))
    return res;

  var wd = +RegExp.$1;
  var ht = +RegExp.$2;
  var foundIdx;

  // Snap to the nearest vert res first
  forEach(vertResList, function(idx, elm) {
    var tolerance = elm * 0.1;
    if(ht >= elm - tolerance && ht <= elm + tolerance) {
      foundIdx = idx;
      return false;
      }
    });

  if(!foundIdx)
    return res;

  var aspectRatio = wd / ht;

  ht = vertResList[foundIdx];
  wd = Math.round(ht * aspectRatio);

  // Snap to the nearest horz res
  forEach(aspectRatio < 1.5 ? horzResList : horzWideResList, function(idx, elm) {
    var tolerance = elm * 0.1;
    if(wd >= elm - tolerance && wd <= elm + tolerance) {
      wd = elm;
      return false;
      }
    });

  return wd + "x" + ht;
  }

function cnvResName(res) {
  var resMap = {
    "audio": "Audio"
    };

  if(resMap[res])
    return resMap[res];

  if(!res.match(/^(\d+)x(\d+)/))
    return res;

  var wd = +RegExp.$1;
  var ht = +RegExp.$2;

  var vertResMap = {
    "2160": "2k",
    "1536": "1.5k",
    "240": "240v",
    "144": "144v"
    };

  if(vertResMap[ht])
    return vertResMap[ht];

  return String(ht) + (wd / ht < 1.5 ? "f" : "p");
  }

function mapResToQuality(res) {
  if(!res.match(/^[0-9]+x([0-9]+)$/))
    return res;

  var resList = [
    { res: 1536, q : "highres" },
    { res: 1080, q: "hd1080" },
    { res: 720, q : "hd720" },
    { res: 480, q : "large" },
    { res: 360, q : "medium" }
    ];

  var res = +RegExp.$1;

  for(var i = 0; i < resList.length; ++i) {
    if(res >= resList[i].res)
      return resList[i].q;
    }

  return "small";
  }

function getQualityIdx(quality) {
  var list = [ "small", "medium", "large", "hd720", "hd1080", "highres" ];

  for(var i = 0; i < list.length; ++i) {
    if(list[i] == quality)
      return i;
    }

  return -1;
  }

// =============================================================================

RegExp.escape = function(s) {
  return String(s).replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
  };

var decryptSig = {
  store: {}
  };

(function () {

var SIG_STORE_ID = "ujsYtLinksSig";

var CHK_SIG_INTERVAL = 3 * 86400;

decryptSig.load = function() {
  var obj = localStorage[SIG_STORE_ID];
  if(obj == null)
    return;

  decryptSig.store = JSON.parse(obj);
  };

decryptSig.save = function() {
  localStorage[SIG_STORE_ID] = JSON.stringify(decryptSig.store);
  };

decryptSig.extractScriptUrl = function(data) {
  if(data.match(/ytplayer.config\s*=.*\"assets"\s*:\s*{.*"js"\s*:\s*(".+?")/))
    return JSON.parse(RegExp.$1);
  else
    return false;
  };

decryptSig.getScriptName = function(url) {
  if(url.match(/\/yts\/jsbin\/html5player-(.*)\/html5player\.js$/))
    return RegExp.$1;

  if(url.match(/\/html5player-(.*)\.js$/))
    return RegExp.$1;

  return url;
  };

decryptSig.fetchScript = function(scriptName, url) {
  function success(data) {
    if(!data.match(/\.signature\s*=\s*(\w+)\(\w+\)/))
      return;

    //console.log(scriptName + " sig fn: " + RegExp.$1);

    if(!data.match(new RegExp("function " + RegExp.$1 + '\\s*\\((\\w+)\\)\\s*{(\\w+=\\w+\\.split\\(""\\);.+?;return \\w+\\.join\\(""\\))')))
      return;

    var fnParam = RegExp.$1;
    var fnBody = RegExp.$2;

    var fnHlp = {};
    var objHlp = {};

    //console.log("param: " + fnParam);
    //console.log(fnBody);

    fnBody = fnBody.split(";");

    forEach(fnBody, function(idx, elm) {
      // its own property
      if(elm.match(new RegExp("^" + fnParam + "=" + fnParam + "\\.")))
        return;

      // global fn
      if(elm.match(new RegExp("^" + fnParam + "=([a-zA-Z_$][a-zA-Z0-9_$]*)\\("))) {
        var name = RegExp.$1;
        //console.log("fnHlp: " + name);

        if(fnHlp[name])
          return;

        if(data.match(new RegExp("(function " + RegExp.escape(RegExp.$1) + ".+?;return \\w+})")))
          fnHlp[name] = RegExp.$1;

        return;
        }

      // object fn
      if(elm.match(new RegExp("^([a-zA-Z_$][a-zA-Z0-9_$]*)\.([a-zA-Z_$][a-zA-Z0-9_$]*)\\("))) {
        var name = RegExp.$1;
        //console.log("objHlp: " + name);

        if(objHlp[name])
          return;

        if(data.match(new RegExp("(var " + RegExp.escape(RegExp.$1) + "={.+?};)")))
          objHlp[name] = RegExp.$1;

        return;
        }
      });

    //console.log(fnHlp);
    //console.log(objHlp);

    var fnHlpStr = "";

    for(var k in fnHlp)
      fnHlpStr += fnHlp[k];

    for(var k in objHlp)
      fnHlpStr += objHlp[k];

    var fullFn = "function(" + fnParam + "){" + fnHlpStr + fnBody.join(";") + "}";
    //console.log(fullFn);

    decryptSig.store[scriptName] = { ver: relInfo.ver, ts: timeNowInSec(), fn: fullFn };
    //console.log(decryptSig);

    decryptSig.save();
    }

  // Entry point
  dom.crossAjax({ url: url, success: success });
  };

decryptSig.condFetchScript = function(url) {
  var scriptName = decryptSig.getScriptName(url);
  var store = decryptSig.store[scriptName];
  var now = timeNowInSec();

  if(store && now - store.ts < CHK_SIG_INTERVAL && store.ver == relInfo.ver)
    return;

  decryptSig.fetchScript(scriptName, url);
  };

}) ();

function deobfuscateVideoSig(scriptName, sig) {
  if(!decryptSig.store[scriptName])
    return sig;

  //console.log(decryptSig.store[scriptName].fn);

  try {
    sig = eval("(" + decryptSig.store[scriptName].fn + ") (\"" + sig + "\")");
  } catch(e) {
    }

  return sig;
  }

// =============================================================================

function parseStreamMap(map, value) {
  var fmtUrlList = [];

  forEach(value.split(","), function(idx, elm) {
    var elms = elm.replace(/\\\//g, "/").replace(/\\u0026/g, "&").split("&");
    var obj = {};

    forEach(elms, function(idx, elm) {
      var kv = elm.split("=");
      obj[kv[0]] = decodeURIComponent(kv[1]);
      });

    obj.itag = +obj.itag;

    if(obj.conn != null && obj.conn.match(/^rtmpe:\/\//))
      obj.isDrm = true;

    if(obj.s != null && obj.sig == null) {
      var sig = deobfuscateVideoSig(map.scriptName, obj.s);
      if(sig != obj.s) {
        obj.sig = sig;
        delete obj.s;
        }
      }

    fmtUrlList.push(obj);
    });

  //logMsg(fmtUrlList);

  map.fmtUrlList = fmtUrlList;
  }

function parseAdaptiveStreamMap(map, value) {
  var fmtUrlList = [];

  forEach(value.split(","), function(idx, elm) {
    var elms = elm.replace(/\\\//g, "/").replace(/\\u0026/g, "&").split("&");
    var obj = {};

    forEach(elms, function(idx, elm) {
      var kv = elm.split("=");
      obj[kv[0]] = decodeURIComponent(kv[1]);
      });

    obj.itag = +obj.itag;

    if(obj.bitrate != null)
      obj.bitrate = +obj.bitrate;

    //console.log(obj);
    //console.log(map.videoId + ": " + obj.index + " " + obj.init + " " + obj.itag + " " + obj.size + " " + obj.bitrate + " " + obj.type);

    if(obj.type.match(/^video\/mp4/))
      obj.effType = "video/x-m4v";

    if(obj.type.match(/^audio\//))
      obj.size = "audio";

    var stdRes = snapToStdRes(obj.size);
    obj.quality = mapResToQuality(stdRes);

    if(obj.s != null && obj.sig == null) {
      var sig = deobfuscateVideoSig(map.scriptName, obj.s);
      if(sig != obj.s) {
        obj.sig = sig;
        delete obj.s;
        }
      }

    fmtUrlList.push(obj);

    map.fmtMap[obj.itag] = { res: cnvResName(stdRes) };
    });

  map.fmtUrlList = map.fmtUrlList.concat(fmtUrlList);
  }

function parseFmtList(map, value) {
  var list = value.split(",");
  var fmtMap = {};

  forEach(list, function(idx, elm) {
    var elms = elm.replace(/\\\//g, "/").split("/");

    var fmtId = elms[0];
    var res = elms[1];
    elms.splice(/*idx*/ 0, /*rm*/ 2);

    fmtMap[fmtId] = { res: cnvResName(res), vars: elms };
    });

  map.fmtMap = fmtMap;
  }

function getExt(elm) {
  return "";
  }

function getVideoInfo(url, callback) {
  function success(data) {
    var map = {};

    if(data.match(/<div\s+id="verify-details">/)) {
      logMsg("Skipping " + url);
      return;
      }

    if(data.match(/<h1\s+id="unavailable-message">/)) {
      logMsg("Not avail " + url);
      return;
      }

    if(data.match(/"t":\s?"(.+?)"/))
      map.t = RegExp.$1;

    if(data.match(/"video_id":\s?"(.+?)"/))
      map.videoId = RegExp.$1;

    map.scriptUrl = decryptSig.extractScriptUrl(data);
    if(map.scriptUrl) {
      //console.log(map.videoId + " script: " + map.scriptUrl);
      map.scriptName = decryptSig.getScriptName(map.scriptUrl);
      decryptSig.condFetchScript(map.scriptUrl);
      }

    if(data.match(/<meta\s+itemprop="name"\s*content="(.+)"\s*>\s*\n/))
      map.title = unescHtmlEntities(RegExp.$1);

    if(map.title == null && data.match(/<meta\s+name="title"\s*content="(.+)"\s*>/))
      map.title = unescHtmlEntities(RegExp.$1);

    if(data.match(/"url_encoded_fmt_stream_map":\s?"(.+?)"/))
      parseStreamMap(map, RegExp.$1);

    if(data.match(/"fmt_list":\s?"(.+?)"/))
      parseFmtList(map, RegExp.$1);

    if(data.match(/"adaptive_fmts":\s?"(.+?)"/))
      parseAdaptiveStreamMap(map, RegExp.$1);

    if(data.match(/"dashmpd":\s?"(.+?)"/))
      map.dashmpd = decodeURIComponent(RegExp.$1.replace(/\\\//g, "/"));

    var hasHighRes = false;

    forEach(map.fmtUrlList, function(idx, elm) {
      hasHighRes |= elm.quality == "hd720" || elm.quality == "hd1080";
      });

    if(hasHighRes) {
      for(var i = 0; i < map.fmtUrlList.length; ++i) {
        if(map.fmtUrlList[i].quality == "small") {
          map.fmtUrlList.splice(i, /*len*/ 1);
          --i;
          continue;
          }
        }
      }

    map.fmtUrlList.sort(function(a, b) {
      return getQualityIdx(b.quality) - getQualityIdx(a.quality);
      });

    callback(map);
    }

  // Entry point
  dom.ajax({ url: url, success: success });
  }

// -----------------------------------------------------------------------------

var CSS_PREFIX = "ujs-";

var HDR_LINKS_HTML_ID = CSS_PREFIX + "hdr-links-div";
var LINKS_HTML_ID = CSS_PREFIX + "links-cls";
var LINKS_TP_HTML_ID = CSS_PREFIX + "links-tp-div";
var UPDATE_HTML_ID = CSS_PREFIX + "update-div";
var VID_FMT_BTN_ID = CSS_PREFIX + "vid-fmt-btn";

/* The !important attr is to override the page's specificity. */
var CSS_STYLES =
  "#" + VID_FMT_BTN_ID + dom.emitCssStyles({
    "margin": "0 0.333em"
    }) + "\n" +
  "#" + UPDATE_HTML_ID + dom.emitCssStyles({
    "background-color": "#f00",
    "border-radius": "2px",
    "color": "#fff",
    "padding": "5px",
    "text-align": "center",
    "text-decoration": "none",
    "position": "fixed",
    "top": "0.5em",
    "right": "0.5em",
    "z-index": "100"
    }) + "\n" +
  "#" + UPDATE_HTML_ID + ":hover" + dom.emitCssStyles({
    "background-color": "#0d0"
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + dom.emitCssStyles({
    "background-color": "#eee",
    "border": "#ccc 1px solid",
    //"border-radius": "3px",
    "color": "#333",
    "font-size": "90%",
    "margin": "5px",
    "padding": "5px"
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + " ." + CSS_PREFIX + "group" + dom.emitCssStyles({
    "background-color": "#fff",
    "color": "#000 !important",
    "border": "#ccc 1px solid",
    "border-radius": "3px",
    "display": "inline-block",
    "margin": "3px",
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + " a" + dom.emitCssStyles({
    "display": "table-cell",
    "padding": "3px",
    "text-decoration": "none"
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + " a:hover" + dom.emitCssStyles({
    "background-color": "#d1e1fa"
    }) + "\n" +
  "div." + LINKS_HTML_ID + dom.emitCssStyles({
    "border-radius": "3px",
    "font-size": "90%",
    "line-height": "1em",
    "position": "absolute",
    "left": "0",
    "top": "0",
    "z-index": "100"
    }) + "\n" +
  "#" + LINKS_TP_HTML_ID + dom.emitCssStyles({
    "background-color": "#eee",
    "border": "#aaa 1px solid",
    "padding": "3px 0",
    "text-decoration": "none",
    "white-space": "nowrap",
    "z-index": "110"
    }) + "\n" +
  "div." + LINKS_HTML_ID + " a" + dom.emitCssStyles({
    "display": "inline-block",
    "margin": "1px",
    "text-decoration": "none"
    }) + "\n" +
  "div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "video" + dom.emitCssStyles({
    "display": "inline-block",
    "text-align": "center",
    "width": "3.5em"
    }) + "\n" +
  "div." + LINKS_HTML_ID + " ." + CSS_PREFIX + "quality" + dom.emitCssStyles({
    "display": "inline-block",
    "text-align": "center",
    "width": "5.5em"
    }) + "\n" +
  "." + CSS_PREFIX + "video" + dom.emitCssStyles({
    "color": "#fff !important",
    "padding": "1px 3px",
    "text-align": "center"
    }) + "\n" +
  "." + CSS_PREFIX + "quality" + dom.emitCssStyles({
    "color": "#000 !important",
    "display": "table-cell",
    "padding": "1px 3px",
    "vertical-align": "middle"
    }) + "\n" +
  "." + CSS_PREFIX + "filesize" + dom.emitCssStyles({
    "font-size": "90%",
    "margin-top": "2px",
    "padding": "1px 3px",
    "text-align": "center"
    }) + "\n" +
  "." + CSS_PREFIX + "filesize-err" + dom.emitCssStyles({
    "color": "#f00",
    "font-size": "90%",
    "margin-top": "2px",
    "padding": "1px 3px",
    "text-align": "center"
    }) + "\n" +
  "." + CSS_PREFIX + "not-avail" + dom.emitCssStyles({
    "background-color": "#700",
    "color": "#fff",
    "padding": "3px",
    }) + "\n" +
  "." + CSS_PREFIX + "3gp" + dom.emitCssStyles({
    "background-color": "#bbb"
    }) + "\n" +
  "." + CSS_PREFIX + "flv" + dom.emitCssStyles({
    "background-color": "#0dd"
    }) + "\n" +
  "." + CSS_PREFIX + "m4a" + dom.emitCssStyles({
    "background-color": "#07e"
    }) + "\n" +
  "." + CSS_PREFIX + "m4v" + dom.emitCssStyles({
    "background-color": "#07e"
    }) + "\n" +
  "." + CSS_PREFIX + "mp3" + dom.emitCssStyles({
    "background-color": "#7ba"
    }) + "\n" +
  "." + CSS_PREFIX + "mp4" + dom.emitCssStyles({
    "background-color": "#777"
    }) + "\n" +
  "." + CSS_PREFIX + "qt" + dom.emitCssStyles({
    "background-color": "#f08"
    }) + "\n" +
  "." + CSS_PREFIX + "webm" + dom.emitCssStyles({
    "background-color": "#e0e"
    }) + "\n" +
  "." + CSS_PREFIX + "wmv" + dom.emitCssStyles({
    "background-color": "#c75"
    }) + "\n" +
  "." + CSS_PREFIX + "small" + dom.emitCssStyles({
    "color": "#888 !important",
    }) + "\n" +
  "." + CSS_PREFIX + "medium" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#0d0"
    }) + "\n" +
  "." + CSS_PREFIX + "large" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#00d",
    "background-image": "linear-gradient(to right, #00d, #00a)"
    }) + "\n" +
  "." + CSS_PREFIX + "hd720" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#f90",
    "background-image": "linear-gradient(to right, #f90, #d70)"
    }) + "\n" +
  "." + CSS_PREFIX + "hd1080" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#f00",
    "background-image": "linear-gradient(to right, #f00, #c00)"
    }) + "\n" +
  "." + CSS_PREFIX + "highres" + dom.emitCssStyles({
    "color": "#fff !important",
    "background-color": "#c0f",
    "background-image": "linear-gradient(to right, #c0f, #90f)"
    }) + "\n" +
  "." + CSS_PREFIX + "pos-rel" + dom.emitCssStyles({
    "position": "relative"
    }) + "\n" +
  "";

function condInsertHdr(divId) {
  if(dom.gE(HDR_LINKS_HTML_ID))
    return true;

  var insertPtNode = dom.gE(divId);
  if(!insertPtNode)
    return false;

  var divNode = dom.cE("div");
  divNode.id = HDR_LINKS_HTML_ID;

  insertPtNode.parentNode.insertBefore(divNode, insertPtNode);
  return true;
  }

function condInsertTooltip() {
  if(dom.gE(LINKS_TP_HTML_ID))
    return true;

  var toolTipNode = dom.cE("div");
  toolTipNode.id = LINKS_TP_HTML_ID;

  dom.attr(toolTipNode, "class", LINKS_HTML_ID);
  dom.attr(toolTipNode, "style", "display: none;");

  dom.append(doc.body, toolTipNode);

  dom.addEvent(toolTipNode, "mouseleave", function(event) {
    //logMsg("mouse leave");
    dom.attr(toolTipNode, "style", "display: none;");
    });
  }

function condInsertUpdateIcon() {
  if(dom.gE(UPDATE_HTML_ID))
    return;

  var divNode = dom.cE("a");
  divNode.id = UPDATE_HTML_ID;
  dom.append(doc.body, divNode);
  }

// -----------------------------------------------------------------------------

var STORE_ID = "ujsYtLinks";
var JSONP_ID = "ujsYtLinks";

var userConfig = {
  showVideoFormats: true,
  showVideoSize: true,
  tagLinks: true
  };

var videoInfoCache = {};

var TAG_LINK_NUM_PER_BATCH = 5;
var INI_TAG_LINK_DELAY_MS = 200;
var SUB_TAG_LINK_DELAY_MS = 500;

// -----------------------------------------------------------------------------

function Links() {
  }

Links.prototype.init = function() {
  };

Links.prototype.getPreferredFmt = function(map) {
  var selElm = map.fmtUrlList[0];

  forEach(map.fmtUrlList, function(idx, elm) {
    if(getVideoName(elm.type).toLowerCase() != "webm") {
      selElm = elm;
      return false;
      }
    });

  return selElm;
  };

Links.prototype.parseDashManifest = function(map) {
  function parse(xml) {
    //logMsg(xml);

    var dashList = [];

    var adaptationSetDom = xml.getElementsByTagName("AdaptationSet");
    //logMsg(adaptationSetDom);

    forEach(adaptationSetDom, function(i, adaptationElm) {
      var mimeType = adaptationElm.getAttribute("mimeType");
      //logMsg(i + " " + mimeType);

      var representationDom = adaptationElm.getElementsByTagName("Representation");
      forEach(representationDom, function(j, repElm) {
        var dashElm = { mimeType: mimeType };

        forEach([ "codecs" ], function(idx, elm) {
          var v = repElm.getAttribute(elm);
          if(v != null)
            dashElm[elm] = v;
          });

        forEach([ "id", "audioSamplingRate", "width", "height", "bandwidth" ], function(idx, elm) {
          var v = repElm.getAttribute(elm);
          if(v != null)
            dashElm[elm] = +v;
          });

        var baseUrlDom = repElm.getElementsByTagName("BaseURL");
        dashElm.len = +baseUrlDom[0].getAttribute("yt:contentLength");
        dashElm.url = baseUrlDom[0].textContent;

        dashList.push(dashElm);
        });
      });

    //logMsg(map);
    //logMsg(dashList);

    forEach(dashList, function(idx, dashElm) {
      var foundIdx;

      // We are only interested in high-res audio
      if(dashElm.audioSamplingRate == null || dashElm.audioSamplingRate < 44100)
        return;

      if(dashElm.mimeType != "audio/mp4")
        return;

      forEach(map.fmtUrlList, function(idx, mapElm) {
        if(dashElm.id == mapElm.itag) {
          foundIdx = idx;
          return false;
          }
        });

      if(foundIdx != null)
        return;

      //logMsg(dashElm);

      if(map.fmtMap[dashElm.id] == null) {
        map.fmtMap[dashElm.id] = { res: "Audio" };
        }

      map.fmtUrlList.push({
        bitrate: dashElm.bandwidth,
        effType: dashElm.mimeType,
        fileSize: dashElm.len,
        itag: dashElm.id,
        quality: "audio",
        type: dashElm.mimeType + ";+codecs=\"" + dashElm.codecs + "\"",
        url: dashElm.url
        });
      });

    if(condInsertHdr("page"))
      dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map));
    }

  // Entry point
  var me = this;

  if(!map.dashmpd)
    return;

  //logMsg(map.dashmpd);

  if(map.dashmpd.match(/\/s\/([a-zA-Z0-9.]+)\//)) {
    var sig = deobfuscateVideoSig(map.scriptName, RegExp.$1);
    map.dashmpd = map.dashmpd.replace(/\/s\/[a-zA-Z0-9.]+\//, "/signature/" + sig + "/");
    }

  dom.crossAjax({
    url: map.dashmpd,
    dataType: "xml",

    success: function(data, status, xhr) {
      parse(data);
      },

    error: function(xhr, status) {
      },

    complete: function(xhr) {
      }
    });
  };

Links.prototype.checkFmts = function(forceFlag) {
  var me = this;

  if(!userConfig.showVideoFormats)
    return;

  if(!forceFlag && userConfig.showVideoFormats == "btn") {
    if(dom.gE(VID_FMT_BTN_ID))
      return;

    var btn = dom.cE("button");
    dom.attr(btn, "id", VID_FMT_BTN_ID);
    dom.attr(btn, "class", "yt-uix-button yt-uix-button-default");
    btn.innerHTML = "VidFmts";

    var mastH = dom.gE("yt-masthead-signin") || dom.gE("yt-masthead-user");
    if(!mastH)
      return;

    dom.prepend(mastH, btn);

    dom.addEvent(btn, "click", function(event) {
      me.checkFmts(/*force*/ true);
      });

    return;
    }

  if(!loc.href.match(/watch\?v=([a-zA-Z0-9_-]*)/))
    return false;

  var videoId = RegExp.$1;

  var url = loc.protocol + "//" + loc.host + "/watch?v=" + videoId;

  getVideoInfo(url, function(map) {
    me.parseDashManifest(map);
    me.showLinks("page", map);
    });
  };

Links.prototype.genUrl = function(map, elm) {
  var url = elm.url + "&title=" + cnvSafeFname(map.title + getExt(elm));

  if(elm.sig != null)
    url += "&signature=" + elm.sig;

  return url;
  };

Links.prototype.emitLinks = function(map) {
  function fmtSize(size) {
    var units = [ "kB", "MB", "GB" ];
    var idx = 0;

    for(idx = 0; idx < units.length; ++idx) {
      size /= 1024;

      if(size < 10)
        return Math.round(size * 100) / 100 + units[idx];

      if(size < 100)
        return Math.round(size * 10) / 10 + units[idx];

      if(size < 1024 || idx == units.length - 1)
        return Math.round(size) + units[idx];
      }
    }

  // Entry point
  var me = this;
  var s = [];

  var resMap = {};

  forEach(map.fmtUrlList, function(idx, elm) {
    var fmtMap = map.fmtMap[elm.itag];

    if(!resMap[fmtMap.res]) {
      resMap[fmtMap.res] = [];
      resMap[fmtMap.res].quality = elm.quality;
      }

    resMap[fmtMap.res].push(elm);
    });

  for(var res in resMap) {
    var qFields = [];

    qFields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "quality " + CSS_PREFIX + resMap[res].quality }, res));

    forEach(resMap[res], function(idx, elm) {
      var fields = [];
      var fmtMap = map.fmtMap[elm.itag];
      var videoName = getVideoName(elm.effType || elm.type);

      var varMsg = "";

      if(elm.bitrate != null)
        varMsg = fmtSize(elm.bitrate) + "/s";
      else if(fmtMap.vars != null)
        varMsg = fmtMap.vars.join();

      var addMsg = [ elm.itag, elm.type, elm.size || elm.quality, varMsg ];

      if(elm.s != null)
        addMsg.push("sig-" + elm.s.length);

      if(elm.fileSize != null && elm.fileSize >= 0)
        addMsg.push(fmtSize(elm.fileSize));

      var vidSuffix = "";

      if(elm.itag == 82 || elm.itag == 83 || elm.itag == 84 || elm.itag == 100 || elm.itag == 101 || elm.itag == 102)
        vidSuffix = " (3D)";

      fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "video " + CSS_PREFIX + videoName.toLowerCase() }, videoName + vidSuffix));

      if(elm.fileSize != null) {
        if(elm.fileSize >= 0) {
          fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize" }, fmtSize(elm.fileSize)));
          }
        else {
          var msg;

          if(elm.isDrm)
            msg = "DRM";
          else if(elm.s != null)
            msg = "sig-" + elm.s.length;
          else
            msg = "Err";

          fields.push(dom.emitHtml("div", { "class": CSS_PREFIX + "filesize-err" }, msg));
          }
        }

      var url;

      if(elm.isDrm)
        url = elm.conn + "?" + elm.stream;
      else
        url = me.genUrl(map, elm);

      var ahref = dom.emitHtml("a", {
        href: url,
        title: addMsg.join(" | ")
        }, fields.join(""));

      qFields.push(ahref);
      });

    s.push(dom.emitHtml("div", { "class": CSS_PREFIX + "group" }, qFields.join("")));
    }

  return s.join("");
  };

var INI_SHOW_FILESIZE_DELAY_MS = 500;
var SUB_SHOW_FILESIZE_DELAY_MS = 200;

Links.prototype.showLinks = function(divId, map) {
  function updateLinks() {
    //!! Hack to update file size
    if(condInsertHdr(divId))
      dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map));
    }

  // Entry point
  var me = this;

  // video is not avail
  if(!map.fmtUrlList)
    return;

  //logMsg(JSON.stringify(map));

  if(!condInsertHdr(divId))
    return;

  dom.html(dom.gE(HDR_LINKS_HTML_ID), me.emitLinks(map));

  if(!userConfig.showVideoSize)
    return;

  forEach(map.fmtUrlList, function(idx, elm) {
    //logMsg(elm.itag + " " + elm.url);

    // We just fail outright for protected/obfuscated videos
    if(elm.isDrm || elm.s != null) {
      elm.fileSize = -1;
      updateLinks();
      return;
      }

    if(elm.clen != null) {
      elm.fileSize = +elm.clen;
      updateLinks();
      return;
      }

    setTimeout(function() {
      dom.crossAjax({
        type: "HEAD",
        url: me.genUrl(map, elm),

        success: function(data, status, xhr) {
          var fileSize = xhr.getResponseHeader("Content-Length");
          if(fileSize == null)
            return;

          //logMsg(map.title + " " + elm.itag + ": " + fileSize);
          elm.fileSize = +fileSize;

          updateLinks();
          },

        error: function(xhr, status) {
          //logMsg(map.fmtMap[elm.itag].res + " " + getVideoName(elm.type) + ": " + xhr.status);

          if(xhr.status != 403)
            return;

          elm.fileSize = -1;

          updateLinks();
          },

        complete: function(xhr) {
          //logMsg(map.title + ": " + xhr.getAllResponseHeaders());
          }
        });
       }, INI_SHOW_FILESIZE_DELAY_MS + idx * SUB_SHOW_FILESIZE_DELAY_MS);
    });
  };

Links.prototype.tagLinks = function() {
  var SCANNED = 1;
  var REQ_INFO = 2;
  var ADDED_INFO = 3;

  function prepareTagHtml(node, map) {
    var elm = me.getPreferredFmt(map);
    var fmtMap = map.fmtMap[elm.itag];

    dom.attr(node, "class", LINKS_HTML_ID + " " + CSS_PREFIX + "quality " + CSS_PREFIX + elm.quality);

    dom.addEvent(node, "mouseenter", function(event) {
      //logMsg("mouse enter " + map.videoId);
      var pos = dom.offset(node);
      //logMsg("mouse enter: x " + pos.left + ", y " + pos.top);

      var toolTipNode = dom.gE(LINKS_TP_HTML_ID);

      dom.attr(toolTipNode, "style", "position: absolute; left: " + pos.left + "px; top: " + pos.top + "px");

      dom.html(toolTipNode, me.emitLinks(map));
      });

    node.href = elm.url + "&title=" + cnvSafeFname(map.title + getExt(elm));

    return fmtMap.res;
    }

  function addTag(hNode, map) {
    //logMsg(dom.html(hNode));
    //logMsg("hNode " + dom.attr(hNode, "class"));
    //var img = dom.gT(hNode, "img") [0];
    //logMsg(dom.attr(img, "src"));
    //logMsg(dom.attr(img, "class"));

    dom.attr(hNode, CSS_PREFIX + "processed", ADDED_INFO);

    var node = dom.cE("div");

    if(map.fmtUrlList) {
      tagHtml = prepareTagHtml(node, map);
      }
    else {
      dom.attr(node, "class", LINKS_HTML_ID + " " + CSS_PREFIX + "not-avail");
      tagHtml = "NA";
      }

    var parentNode = hNode.parentNode;
    var parentCssPositionStyle = window.getComputedStyle(parentNode, null).getPropertyValue("position");

    if(parentCssPositionStyle != "absolute" && parentCssPositionStyle != "relative")
      dom.attr(parentNode, "class", dom.attr(parentNode, "class") + " " + CSS_PREFIX + "pos-rel");

    hNode.parentNode.insertBefore(node, hNode);

    dom.html(node, tagHtml);
    }

  function getFmt(videoId, hNode) {
    if(videoInfoCache[videoId]) {
      addTag(hNode, videoInfoCache[videoId]);
      return;
      }

    var url;

    if(videoId.match(/.+==$/))
      url = loc.protocol + "//" + loc.host + "/cthru?key=" + videoId;
    else
      url = loc.protocol + "//" + loc.host + "/watch?v=" + videoId;

    getVideoInfo(url, function(map) {
      videoInfoCache[videoId] = map;
      addTag(hNode, map);
      });
    }

  // Entry point
  var me = this;

  var list = [];

  forEach(dom.gT("a"), function(idx, hNode) {
    if(dom.attr(hNode, CSS_PREFIX + "processed"))
      return;

    if(!dom.inViewport(hNode))
      return;

    dom.attr(hNode, CSS_PREFIX + "processed", SCANNED);

    if(!hNode.href.match(/watch\?v=([a-zA-Z0-9_-]*)/) &&
     !hNode.href.match(/watch_videos.+?&video_ids=([a-zA-Z0-9_-]*)/))
      return;

    var videoId = RegExp.$1;

    var cls = dom.attr(hNode, "class") || "";
    if(cls == "yt-button" || cls.match(/yt-uix-button/))
      return;

    if(dom.attr(hNode.parentNode, "class") == "video-time")
      return;

    if(dom.html(hNode).match(/video-logo/i))
      return;

    var img = dom.gT(hNode, "img");
    if(img == null || img.length == 0)
      return;

    img = img[0];

    var imgSrc = dom.attr(img, "src") || "";
    if(imgSrc.indexOf("ytimg.com") < 0)
      return;

    var tnSrc = dom.attr(img, "thumb") || "";

    if(imgSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/default\.jpg$/))
      videoId = RegExp.$1;
    else if(tnSrc.match(/.+?\/([a-zA-Z0-9_-]*)\/default\.jpg$/))
      videoId = RegExp.$1;

    //logMsg(idx + " " + hNode.href);
    //logMsg("videoId: " + videoId);

    list.push({ videoId: videoId, hNode: hNode });

    dom.attr(hNode, CSS_PREFIX + "processed", REQ_INFO);
    });

  forLoop({ num: list.length, inc: TAG_LINK_NUM_PER_BATCH, batchIdx: 0 }, function(idx) {
    var batchIdx = this.batchIdx++;
    var batchList = list.slice(idx, idx + TAG_LINK_NUM_PER_BATCH);

    setTimeout(function() {
      forEach(batchList, function(idx, elm) {
        //logMsg(batchIdx + " " + idx + " " + elm.hNode.href);
        getFmt(elm.videoId, elm.hNode);
        });
      }, INI_TAG_LINK_DELAY_MS + batchIdx * SUB_TAG_LINK_DELAY_MS);
    });
  };

Links.prototype.periodicTagLinks = function(delayMs) {
  function poll() {
    me.tagLinks();
    me.tagLinksTimerId = setTimeout(poll, 3000);
    }

  // Entry point
  if(!userConfig.tagLinks)
    return;

  var me = this;

  delayMs = delayMs || 0;

  if(me.tagLinksTimerId != null) {
    clearTimeout(me.tagLinksTimerId);
    delete me.tagLinksTimerId;
    }

  setTimeout(poll, delayMs);
  };

// -----------------------------------------------------------------------------

Links.prototype.loadSettings = function() {
  var obj = localStorage[STORE_ID];
  if(obj == null)
    return;

  obj = JSON.parse(obj);

  this.lastChkReqTs = +obj.lastChkReqTs;
  this.lastChkTs = +obj.lastChkTs;
  this.lastChkVer = +obj.lastChkVer;
  };

Links.prototype.storeSettings = function() {
  localStorage[STORE_ID] = JSON.stringify({
    lastChkReqTs: this.lastChkReqTs,
    lastChkTs: this.lastChkTs,
    lastChkVer: this.lastChkVer
    });
  };

// -----------------------------------------------------------------------------

var UPDATE_CHK_INTERVAL = 5 * 86400;
var FAIL_TO_CHK_UPDATE_INTERVAL = 14 * 86400;

Links.prototype.chkVer = function(forceFlag) {
  if(this.lastChkVer > relInfo.ver) {
    this.showNewVer({ ver: this.lastChkVer });
    return;
    }

  var now = timeNowInSec();

  //logMsg("lastChkReqTs " + this.lastChkReqTs + ", diff " + (now - this.lastChkReqTs));
  //logMsg("lastChkTs " + this.lastChkTs);
  //logMsg("lastChkVer " + this.lastChkVer);

  if(this.lastChkReqTs == null || now < this.lastChkReqTs) {
    this.lastChkReqTs = now;
    this.storeSettings();
    return;
    }

  if(now - this.lastChkReqTs < UPDATE_CHK_INTERVAL)
    return;

  if(this.lastChkReqTs - this.lastChkTs > FAIL_TO_CHK_UPDATE_INTERVAL)
    logMsg("Failed to check ver for " + ((this.lastChkReqTs - this.lastChkTs) / 86400) + " days");

  this.lastChkReqTs = now;
  this.storeSettings();

  unsafeWin[JSONP_ID] = this;

  var script = dom.cE("script");
  script.type = "text/javascript";
  script.src  = SCRIPT_UPDATE_LINK;
  dom.append(doc.body, script);
  };

Links.prototype.chkVerCallback = function(data) {
  delete unsafeWin[JSONP_ID];

  this.lastChkTs = timeNowInSec();
  this.storeSettings();

  //logMsg(JSON.stringify(data));

  var latestElm = data[0];

  if(latestElm.ver <= relInfo.ver)
    return;

  this.showNewVer(latestElm);
  };

Links.prototype.showNewVer = function(latestElm) {
  function getVerStr(ver) {
    var verStr = "" + ver;

    var majorV = verStr.substr(0, verStr.length - 4) || "0";
    var minorV = verStr.substr(verStr.length - 4, 2);
    return majorV + "." + minorV;
    }

  // Entry point
  this.lastChkVer = latestElm.ver;
  this.storeSettings();

  condInsertUpdateIcon();

  var aNode = dom.gE(UPDATE_HTML_ID);

  aNode.href = SCRIPT_LINK;

  if(latestElm.desc != null)
    dom.attr(aNode, "title", latestElm.desc);

  dom.html(aNode, dom.emitHtml("b", SCRIPT_NAME + " " + getVerStr(relInfo.ver)) +
   "<br>Click to update to " + getVerStr(latestElm.ver));
  };

// -----------------------------------------------------------------------------

var inst = new Links();

inst.init();
inst.loadSettings();
decryptSig.load();

dom.insertCss(CSS_STYLES);

condInsertTooltip();

if(loc.pathname.match(/\/watch/)) {
  inst.checkFmts();
  }

inst.periodicTagLinks();

var scrollTop = win.pageYOffset || doc.documentElement.scrollTop;

dom.addEvent(win, "scroll", function(e) {
  var newScrollTop = win.pageYOffset || doc.documentElement.scrollTop;

  if(Math.abs(newScrollTop - scrollTop) < 100)
    return;

  //logMsg("scroll by " + (newScrollTop - scrollTop));

  scrollTop = newScrollTop;

  inst.periodicTagLinks(200);
  });

inst.chkVer();

// -----------------------------------------------------------------------------

/* YouTube reuses the current page when the user clicks on a new video. We need
 to detect it and reload the formats. */

(function() {

var PERIODIC_CHK_VIDEO_URL_MS = 1000;

var curVideoUrl = loc.toString();

function periodicChkVideoUrl() {
  var newVideoUrl = loc.toString();

  if(curVideoUrl != newVideoUrl) {
    //console.log(curVideoUrl + " -> " + newVideoUrl);

    curVideoUrl = newVideoUrl;

    if(loc.pathname.match(/\/watch/))
      inst.checkFmts();
    }

  setTimeout(periodicChkVideoUrl, PERIODIC_CHK_VIDEO_URL_MS);
  }

periodicChkVideoUrl();

}) ();

// -----------------------------------------------------------------------------

}) ();