Skip to content

Instantly share code, notes, and snippets.

@kmaglione
Created December 28, 2011 00:47
Show Gist options
  • Save kmaglione/1525583 to your computer and use it in GitHub Desktop.
Save kmaglione/1525583 to your computer and use it in GitHub Desktop.
/*
* Copyright (c) 2010 by Kris Maglione <maglione.k at Gmail>
*
* This work is licensed for reuse under the MIT license.
* See the included LICENSE file for details.
*/
/* use strict */;
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
const VIDEO_EXTENSIONS = ["avi", "mp4", "mpg", "mpeg", "mkv"];
const CONFIG = {
VIDEOURLS: {
PLAYER: "http://videourls.com/player/",
VERSION: "ec",
REGEN: "http://regen.videourls.com/?v=",
UNEMBED: "http://videourls.com/?v="
},
MEGAUPLOAD: {
DOWNLOAD_LINK: "#dlbutton",
SITE_PATTERN: /^http:\/\/(?:www\.)?megaupload\.com\/\?(?:[^#]*&)*d=(\w{8})(?:&|$)/i,
LINK_PATTERN: RegExp("^http://www[0-9]{1,4}\\.megaupload\\.com/files/.+\\.(?:" + VIDEO_EXTENSIONS.join("|") + ")$", "i")
},
MEGAVIDEO: {
SITE_PATTERN: /^http:\/\/(?:www\.)?megavideo\.com\/\?(?:[^#]&)*([vd])=(\w{8})(?:&|$)/i,
EMBED_PATTERNS: [
RegExp('http://wwwstatic\\.megavideo\\.com/mv_player\\.swf\\?(?:[^#]*&)*v=(\\w{8})(?:&|$)'),
RegExp('^http://(?:www\\.)megavideo\\.com/v/(\\w{8})')
]
}
};
var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
var observers = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
var sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
var extensionId = "addon@videourls.com";
var overlayURI = "chrome://browser/content/browser.xul";
var optionsURI = "chrome://videourls/content/options.xul";
var stylesheetURI = ioService.newURI("chrome://videourls/skin/videourls.css", null, null);
var strings = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService)
.createBundle("chrome://videourls/locale/videourls.properties");
function _(str) {
try {
return strings.formatStringFromName(str, Array.slice(arguments, 1), arguments.length - 1);
} catch (e) {
return str;
}
}
function reportError(e) {
dump("VideoURLs: Error: " + e + "\n" + (e.stack || Error().stack).replace(/^(?!$)/mg, " "));
Cu.reportError(e);
}
// Default preference values
var prefs = {
AutoBypass: false,
AutoRegenerate: true,
AutoWatch: false,
AutoUnembed: true,
Button: true,
Status: true
}
function Prefs(branch, defaults) {
this.constructor = Prefs; // Ends up Object otherwise... Why?
this.branch = Services.prefs[defaults ? "getDefaultBranch" : "getBranch"](branch || "");
if (this.branch instanceof Ci.nsIPrefBranch2)
this.branch.QueryInterface(Ci.nsIPrefBranch2);
this.defaults = defaults ? this : new this.constructor(branch, true);
}
Prefs.prototype = {
/**
* Returns a new Prefs object for the sub-branch *branch* of this
* object.
*
* @param {string} branch The sub-branch to return.
*/
Branch: function Branch(branch) new this.constructor(this.root + branch),
/**
* Returns the full name of this object's preference branch.
*/
get root() this.branch.root,
/**
* Returns the value of the preference *name*, or *defaultValue* if
* the preference does not exist.
*
* @param {string} name The name of the preference to return.
* @param {*} defaultValue The value to return if the preference has no value.
* @optional
*/
get: function get(name, defaultValue) {
let type = this.branch.getPrefType(name);
if (type === Ci.nsIPrefBranch.PREF_STRING)
return this.branch.getComplexValue(name, Ci.nsISupportsString).data;
if (type === Ci.nsIPrefBranch.PREF_INT)
return this.branch.getIntPref(name);
if (type === Ci.nsIPrefBranch.PREF_BOOL)
return this.branch.getBoolPref(name);
return defaultValue;
},
/**
* Returns true if the given preference exists in this branch.
*
* @param {string} name The name of the preference to check.
*/
has: function has(name) this.branch.getPrefType(name) !== 0,
/**
* Returns an array of all preference names in this branch or the
* given sub-branch.
*
* @param {string} branch The sub-branch for which to return preferences.
* @optional
*/
getNames: function getNames(branch) this.branch.getChildList(branch || "", { value: 0 }),
/**
* Returns true if the given preference is set to its default value.
*
* @param {string} name The name of the preference to check.
*/
isDefault: function isDefault(name) !this.branch.prefHasUserValue(name),
/**
* Sets the preference *name* to *value*. If the preference already
* exists, it must have the same type as the given value.
*
* @param {name} name The name of the preference to change.
* @param {string|number|boolean} value The value to set.
*/
set: function set(name, value) {
let type = typeof value;
if (type === "string") {
let string = SupportsString();
string.data = value;
this.branch.setComplexValue(name, Ci.nsISupportsString, string);
}
else if (type === "number")
this.branch.setIntPref(name, value);
else if (type === "boolean")
this.branch.setBoolPref(name, value);
else
throw TypeError("Unknown preference type: " + type);
},
/**
* Sets the preference *name* to *value* only if it doesn't
* already have that value.
*/
maybeSet: function maybeSet(name, value) {
if (this.get(name) != value)
this.set(name, value);
},
/**
* Resets the preference *name* to its default value.
*
* @param {string} name The name of the preference to reset.
*/
reset: function reset(name) {
if (this.branch.prefHasUserValue(name))
this.branch.clearUserPref(name);
}
};
var prefBranch = new Prefs("extensions." + extensionId + ".");
function xpath(path, doc, context) ({
__proto__: doc.evaluate(path, context || doc, null, Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null),
__iterator__: function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
});
var HTML = Namespace("html", "http://www.w3.org/1999/xhtml");
var VU = Namespace("videourls", "http://videourls.com/namespace/addon/firefox");
var XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
default xml namespace = HTML;
function xmlToDom(node, doc, nodes) {
if (node.length() != 1) {
let domnode = doc.createDocumentFragment();
for each (let child in node)
domnode.appendChild(xmlToDom(child, doc, nodes));
return domnode;
}
switch (node.nodeKind()) {
case "text":
return doc.createTextNode(String(node));
case "element":
let domnode = doc.createElementNS(node.namespace(), node.localName());
for each (let attr in node.@*::*)
domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr));
for each (let child in node.*::*)
domnode.appendChild(xmlToDom(child, doc, nodes));
if (nodes && node.@key)
nodes[node.@key] = domnode;
return domnode;
default:
return null;
}
}
function Stream(string) {
var data = Cc['@mozilla.org/io/string-input-stream;1'].createInstance(Ci.nsIStringInputStream);
data.data = string;
var mime = Cc['@mozilla.org/network/mime-input-stream;1'].createInstance(Ci.nsIMIMEInputStream);
mime.addHeader("Content-Type", "application/x-www-form-urlencoded");
mime.addContentLength = true;
mime.setData(data);
return mime;
}
var base = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
initBase: function () {
this.listeners = [];
this.elements = [];
this.observers = [];
this.addObserver("videourls-destroy", true);
},
"$": function (elem, context) {
try {
if (typeof elem == "string")
if (/[@\/]/.test(elem))
return this.xpath(elem, context).snapshotItem(0);
else
return (context || this.document).querySelector(elem);
return elem;
}
catch (e) {
reportError(e);
throw e;
}
},
xpath: function (path, context) xpath(path, this.document, context),
append: function (elem, xml, nodes) {
let newElem = xmlToDom(xml, this.document, nodes);
this.elements.push(newElem);
return this.$(elem).appendChild(newElem);
},
prepend: function (elem, xml, nodes) {
let newElem = xmlToDom(xml, this.document, nodes);
this.elements.push(newElem);
elem = this.$(elem);
return elem.insertBefore(newElem, elem.firstChild);
},
insertBefore: function (elem, xml, nodes) {
let newElem = xmlToDom(xml, this.document, nodes);
this.elements.push(newElem);
elem = this.$(elem);
return elem.parentNode.insertBefore(newElem, elem);
},
destroyBase: function () {
for each (let [elem, event, listen, capture] in this.listeners)
if (elem.addListener)
elem.removeListener(event, listen);
else
elem.removeEventListener(event, listen, capture);
for each (let elem in this.elements)
if (elem.parentNode)
elem.parentNode.removeChild(elem);
for each (let target in this.observers)
observers.removeObserver(this, target);
},
addObserver: function (target, weak) {
observers.addObserver(this, target, weak);
this.observers.push(target);
},
observe: function (subject, target, data) {
try {
if (target == "videourls-destroy")
this.destroy();
this.observer.apply(this, arguments);
}
catch (e) {
reportError(e);
}
},
observer: function () {},
listen: function (elem, method, event, capture) {
let listen = this.wrap(this[method]);
if (elem.addListener)
elem.addListener(event, listen);
else
elem.addEventListener(event, listen, capture);
this.listeners.push([elem, event, listen, capture]);
},
wrap: function (method) {
const self = this;
function wrapper() {
try {
return method.apply(self, arguments);
} catch (e) {
reportError(e);
}
}
return method.wrapper = wrapper;
}
};
/*
* This is where the real work happens. It gets called every time a
* content page or sub-frame thereof loads.
*/
function Page(document) {
const self = this;
this.document = document;
this.window = document.defaultView;
this.initBase();
this.listen(this.window, "destroy", "unload", false);
this.listen(this.window, "onLoad", "load", false);
this.listen(this.window, "onMessage", "message", false);
this.webNav = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation);
let match = CONFIG.MEGAUPLOAD.SITE_PATTERN.exec(this.document.documentURI);
if (match) {
let elem = this.$(CONFIG.MEGAUPLOAD.DOWNLOAD_LINK);
let href = elem && elem.href;
if (CONFIG.MEGAUPLOAD.LINK_PATTERN.test(href)) {
this.action("AutoWatch", function (button) {
if (button)
button.style.display = "none";
let div = this.$("#countertime");
if (div) {
div.setAttributeNS(VU, "class", "counter");
div.parentNode.setAttributeNS(VU, "class", "counter-parent");
this.insertBefore("#countertime",
<p xmlns:vu={VU} vu:class="counter-label">{_("Please wait")}</p>);
}
let interval = this.window.setInterval(
function () {
if (self.window.wrappedJSObject.count == 0) {
self.window.clearInterval(interval);
self.play(href, "megaupload");
}
}, 1000);
},
function () this.append(".download_white_bg3",
<div class="download_but_bl3" vu:class="button-Watch-parent" xmlns:vu={VU}>{
this.Button("Watch")
}</div>));
}
return;
}
this.action("AutoUnembed", function () {
for (let src in this.xpath("//embed/@src | //object/param[@name='movie']/@value")) {
let match = CONFIG.MEGAUPLOAD.EMBED_PATTERNS.reduce(function (res, pat) res || pat.exec(src.value),
null);
if (match) {
this.redirect(CONFIG.VIDEOURLS.UNEMBED + match[1]);
break;
}
}
});
}
Page.prototype = {
__proto__: base,
Button: function (text, style) <div xmlns:vu={VU} vu:class={"button button-" + text} style={style || ""}>{_(text)}</div>,
Searchbox: function ()
<div xmlns:vu={VU} vu:class="searchbox" key="container">
<div vu:class="searchbox-content" key="content">
<span vu:class="searchbox-label">{_("Search Megavideo links")}</span>
<form vu:class="searchbox-form" key="form" action="javascript:void-0">
<input vu:class="searchbox-input" key="input" type="search" name="search"/>
<div vu:class="searchlight" key="submit"/>
</form>
</div>
</div>,
destroy: function () {
this.destroyBase();
},
action: function (pref, action, ask) {
if (prefs[pref])
action.call(this);
else if (prefs.Button) {
let elem = ask && ask.call(this);
if (elem)
elem.addEventListener("click", this.wrap(function () {
action.call(this, elem);
}), false);
}
},
redirect: function redirect(url, post, headers) {
this.webNav.loadURI(url,
this.webNav.LOAD_FLAGS_STOP_CONTENT,
this.webNav.currentURI,
post, headers);
},
play: function play(data, type) {
let params = { vutype: type, vudata: data, euver: CONFIG.VIDEOURLS.VERSION };
let post = [k + "=" + encodeURIComponent(v) for ([k, v] in Iterator(params))].join("&");
this.redirect(CONFIG.VIDEOURLS.PLAYER, Stream(post));
},
onLoad: function (event) {
let match = CONFIG.MEGAVIDEO.SITE_PATTERN.exec(this.document.documentURI);
if (match) {
let player = this.$("#mvplayer");
if (match[1] == "v" && !player) {
this.action("AutoRegenerate", function () {
this.redirect(CONFIG.VIDEOURLS.REGEN + match[2]);
},
function () this.prepend(".st_note_bg1", this.Button("Regenerate")));
}
else {
let div = this.$("#playerdiv");
if (div) {
this.action("AutoBypass", function () {
// Unfortunately, we can't just access
// window.wrappedJSObject.flashvars because, while
// it is global, it's later reset.
let obj = {}, m, re = /\bflashvars\.(\w+)\s*=\s*((?:"(?:[^"]|\\.)*")|\d+)/g;
while (m = re.exec(div.parentNode.innerHTML))
try {
obj[m[1]] = JSON.parse(m[2])
} catch (e) {}
this.play([match[1] + match[2]].concat(["un", "k1", "k2", "s"].map(function (k) obj[k])).join("|"),
"megavideo");
},
function () this.prepend("//tbody/tr[2]/td[3]", this.Button("Bypass")));
}
}
return;
}
},
onMessage: function (event) {
if (event.data === "videourls-search") {
if (this.message)
return;
let elements = {};
this.message = this.append(":root", this.Searchbox(), elements);
let done = this.wrap(function done(event) {
delete this.message;
elements.input.blur();
if (elements.container.parentNode)
elements.container.parentNode.removeChild(elements.container);
this.document.documentElement.removeEventListener("click", done, false);
});
let submit = function submit() {
done();
let input = elements.input.value.toLowerCase()
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-|-$/g, "");
this.redirect("http://videourls.com/search/" + input);
}
this.document.documentElement.addEventListener("click", done, false);
elements.content.addEventListener("click", function (event) { event.stopPropagation() }, false);
elements.form.addEventListener("submit", this.wrap(submit), false);
elements.submit.addEventListener("click", this.wrap(submit), false);
elements.input.focus();
}
}
}
function Overlay(window) {
this.window = window;
this.document = window.document;
this.initBase();
this.panel = this.append("#status-bar",
<statusbarpanel id="videourls-statusbar" class="statusbarpanel-menu-iconic"
context="videourls-status-context" xmlns={XUL}/>);
this.listen(this.panel, "onStatusClick", "click", false);
this.append(":root",
<popupset xmlns={XUL}>
<menupopup id="videourls-status-context">
<menuitem key="showSearchItem" label={_("Search Megavideo links")} default="true"/>
<menuitem label={_("Preferences")} oncommand={"openDialog(" + optionsURI.quote() + ",\
'VideoURL-Options',\
'chrome,dialog,centerscreen,alwaysRaised')"}/>
</menupopup>
</popupset>, this);
this.listen(this.showSearchItem, "showSearch", "command", false);
prefBranch.branch.addObserver("", this, false);
this.observer(null, "nsPref:changed", "Status");
this.listen(this.window, "destroy", "unload", false);
this.listen(this.window.gBrowser, "onDOMContentLoaded", "DOMContentLoaded", true);
// Follow Jetpack's behavior.
let addonBar = this.$("#addon-bar");
if (addonBar)
addonBar.collapsed = false;
}
Overlay.prototype = {
__proto__: base,
destroy: function () {
this.destroyBase();
prefBranch.branch.removeObserver("", this);
},
showSearch: function (event) {
// Don't esk me why this doesn't work without the eval,
// but it doesn't. The message doesn't propagate.
this.window.eval('content.postMessage("videourls-search", "*")');
},
observer: function (subject, topic, data) {
if (data === "Status")
this.panel.hidden = !prefs.Status;
},
onStatusClick: function (event) {
if (event.button === 0)
this.showSearch(event);
},
onDOMContentLoaded: function (event) {
new Page(event.originalTarget);
},
}
function VideoURLs() {
this.initBase();
this.addObserver("toplevel-window-ready");
}
VideoURLs.prototype = {
__proto__: base,
contractID: "@videourls.com/firefox/overlay",
classID: Components.ID("{47071b78-ffcf-4b09-be63-2890da870886}"),
classDescription: "VideoURLs overlay",
_xpcom_categories: [{
category: "profile-after-change",
entry: "m-videourls"
}],
init: function () {
sss.loadAndRegisterSheet(stylesheetURI, sss.USER_SHEET);
for (let item in Iterator(prefs)) {
let [pref, defaultValue] = item;
prefBranch.defaults.set(pref, defaultValue);
prefs.__defineSetter__(pref, function (val) { prefBranch.set(pref, val); return val });
prefs.__defineGetter__(pref, function () prefBranch.get(pref, defaultValue));
}
let enumer = windowMediator.getEnumerator("navigator:browser");
while (enumer.hasMoreElements())
this.initWindow(enumer.getNext());
},
destroy: function () {
this.destroyBase();
sss.unregisterSheet(stylesheetURI, sss.USER_SHEET);
},
initWindow: function (window) {
function checkWindow() {
window.removeEventListener("DOMContentLoaded", checkWindow.wrapper, false);
if (window.document.documentURI == overlayURI)
new Overlay(window);
}
if (["interactive", "complete"].indexOf(window.document.readyState) >= 0)
checkWindow();
else
window.addEventListener("DOMContentLoaded", this.wrap(checkWindow), false);
},
observer: function (subject, target, data) {
if (target == "profile-after-change")
this.init();
else if (target == "toplevel-window-ready")
this.initWindow(subject);
}
};
if (XPCOMUtils.generateNSGetFactory)
var NSGetFactory = XPCOMUtils.generateNSGetFactory([VideoURLs]);
else
var NSGetModule = XPCOMUtils.generateNSGetModule([VideoURLs]);
// vim: set fdm=marker sw=4 ts=4 et:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment