Created
December 28, 2011 00:47
-
-
Save kmaglione/1525583 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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