Greasy Fork is available in English.

Blip.tv Links

Shows all the available video formats at the top of the page on the blip.tv watch page so that they can be saved easily.

// ==UserScript==
// @name           Blip.tv Links
// @namespace      http://www.smallapple.net/
// @description    Shows all the available video formats at the top of the page on the blip.tv watch page so that they can be saved easily.
// @author         Ng Hun Yang
// @include        http://*.blip.tv/*
// @include        http://blip.tv/*
// @include        https://*.blip.tv/*
// @include        https://blip.tv/*
// @match          *://*.blip.tv/*
// @version        1.05
// ==/UserScript==

/* This is based on Blip.tv Video Download 1.4 */

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

(function() {

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

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

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

var SCRIPT_NAME = "BlipTv Links";

var SCRIPT_UPDATE_LINK = loc.protocol + "//greasyfork.org/scripts/5665-blip-tv-links-updater/code/Bliptv Links Updater.user.js";
var SCRIPT_LINK = loc.protocol + "//greasyfork.org/scripts/5666-blip-tv-links/code/Bliptv Links.user.js";

var relInfo = {
  ver: 10500,
  ts: 2014101200,
  desc: "Change link to Greasy Fork"
  };

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

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 doc.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.append = function(obj, child) {
  obj.appendChild(child);
  };

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");
        }
      else {
        opts.error(xhr, "error");
        }

      opts.complete(xhr);
      }
    };

  xhr.send("");
  };

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 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: "FLV", codec: "video/x-flv" },
    { name: "M4V", codec: "video/x-m4v" },
    { name: "MP3", codec: "audio/mpeg" },
    { name: "MP4", codec: "video/mp4" },
    { name: "QT", codec: "video/quicktime" },
    { 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;
  }

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

var CSS_PREFIX = "ujs-";

var HDR_LINKS_HTML_ID = CSS_PREFIX + "links-div";
var UPDATE_HTML_ID = CSS_PREFIX + "update-div";

var CSS_STYLES =
  "#" + 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 + " a" + dom.emitCssStyles({
    "background-color": "#fff",
    "border": "#ccc 1px solid",
    "border-radius": "3px",
    "color": "#000 !important",
    "display": "inline-block",
    "margin": "3px",
    "padding": "5px",
    "text-decoration": "none"
    }) + "\n" +
  "#" + HDR_LINKS_HTML_ID + " a:hover" + dom.emitCssStyles({
    "background-color": "#d1e1fa"
    }) + "\n" +
  "." + CSS_PREFIX + "video" + dom.emitCssStyles({
    "color": "#fff !important",
    "padding": "1px 3px"
    }) + "\n" +
  "." + CSS_PREFIX + "quality" + dom.emitCssStyles({
    "color": "#fff !important",
    "padding": "1px 3px"
    }) + "\n" +
  "." + CSS_PREFIX + "flv" + dom.emitCssStyles({
    "background-color": "#0dd"
    }) + "\n" +
  "." + CSS_PREFIX + "m4v" + dom.emitCssStyles({
    "background-color": "#777"
    }) + "\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": "#000 !important"
    }) + "\n" +
  "." + CSS_PREFIX + "medium" + dom.emitCssStyles({
    "background-color": "#0d0"
    }) + "\n" +
  "." + CSS_PREFIX + "large" + dom.emitCssStyles({
    "background-color": "#00d"
    }) + "\n" +
  "." + CSS_PREFIX + "hd720" + dom.emitCssStyles({
    "background-color": "#f90"
    }) + "\n" +
  "." + CSS_PREFIX + "hd1080" + dom.emitCssStyles({
    "background-color": "#f00"
    }) + "\n" +
  "";

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

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

  var node = dom.gE("Content");
  if(node == null)
    return false;

  node.parentNode.insertBefore(div, node);
  return true;
  }

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

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

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

var STORE_ID = "ujsBtLinks";
var JSONP_ID = "ujsBtLinks";

var userConfig = {
  };

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

function Links() {
  }

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

Links.prototype.showLinks = function(files) {
  function getVideoQuality(wt, ht, videoBitRate) {
    if(ht >= 1080) {
      if(videoBitRate >= 2.5)
        return "hd1080";
      else
        return "large";
      }

    if(ht >= 720) {
      if(videoBitRate >= 2.0)
        return "hd720";
      else
        return "large";
      }

    if(ht >= 480) {
      if(videoBitRate >= 1.5)
        return "large";
      else
        return "medium";
      }

    if(ht >= 360) {
      if(videoBitRate >= 0.75)
        return "medium";
      else
        return "small";
      }

    return "small";
    }

  // Entry point
  if(!condInsertHdr())
    return;

  var s = [];

  files.sort(function(a, b) {
    var a = { wt: +a.media_width, ht: +a.media_height, sz: +a.filesize };
    var b = { wt: +b.media_width, ht: +b.media_height, sz: +b.filesize };

    if(a.ht < b.ht)
      return 1;
    else if(a.ht > b.ht)
      return -1;

    if(a.wt < b.wt)
      return 1;
    else if(a.wt > b.wt)
      return -1;

    if(a.sz < b.sz)
      return 1;
    else if(a.sz > b.sz)
      return -1;
    else
      return 0;
    });

  forEach(files, function(idx, elm) {
    //logMsg(idx + ": " + JSON.stringify(elm));

    //logMsg("fname: " + elm.media_src + " " + elm.filename);
    logMsg("url: " + elm.url);
    logMsg("  res " + elm.media_width + " x " + elm.media_height);
    logMsg("  len: " + elm.media_length + ", size: " + elm.filesize);
    logMsg("  role: " + elm.role + ", type: " + elm.archive_type + ", mime: " + elm.primary_mime_type);
    logMsg("  video: " + elm.video_codec + ", bitrate " + elm.video_bitrate + ", fps " + elm.fps);
    logMsg("  audio: " + elm.audio_codec + ", bitrate " + elm.audio_bitrate + ", samplerate " + elm.sample_rate);

    var videoName = getVideoName(elm.primary_mime_type);

    var wt = +elm.media_width;
    var ht = +elm.media_height;
    var videoLen = Math.round(+elm.media_length / 6) / 10;
    var fileSize = Math.round(+elm.filesize / 1000 / 100) / 10;
    var videoBitRate = +elm.video_bitrate / 1000;
    var samplingRate = +elm.sample_rate / 1000;
    //var calcBitRate = +elm.filesize / +elm.media_length / 1000;

    elm.quality = getVideoQuality(wt, ht, videoBitRate);

    var videoResStr = "";

    if(wt > 0 && ht > 0)
      videoResStr = " (" + wt + "x" + ht + ")";

    var ahref = dom.emitHtml("a", {
      href: elm.url,
      title: videoLen + "mins | " + fileSize + "MiB | " +
       elm.video_codec + " " + videoBitRate + "Mbps " + elm.fps + "fps | " +
       elm.audio_codec + " " + elm.audio_bitrate + "kbps " + samplingRate + "kHz"
      },
      dom.emitHtml("span", { "class": CSS_PREFIX + "video " + CSS_PREFIX + videoName.toLowerCase() }, videoName) +
      dom.emitHtml("span", { "class": CSS_PREFIX + "quality " + CSS_PREFIX + elm.quality }, elm.role + videoResStr));

      s.push(ahref);
    });

  dom.html(dom.gE(HDR_LINKS_HTML_ID), s.join(""));
  };

Links.prototype.checkFmts = function() {
  function success(data) {
    //logMsg(data);

    try {
      var obj = JSON.parse(data);
      me.showLinks(obj.Post.additionalMedia);
    } catch(e) {
      logMsg("Error: unable to parse data");
      }
    }

  // Entry point
  var me = this;

  if(document.getElementsByClassName("EpisodePlayer").length == 0)
    return;

  dom.ajax({
    url: loc.href + "?skin=json&no_wrap=1",
    success: success
    });
  };

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

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 = Math.round(+new Date() / 1000);

  //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();

  win[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 win[JSONP_ID];

  this.lastChkTs = Math.round(+new Date() / 1000);
  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;

  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();

dom.insertCss(CSS_STYLES);

inst.checkFmts();

inst.chkVer();

}) ();