Source for "YousableTubeFix"

By Mindeye
Has no other scripts.


Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name          YousableTubeFix
// @namespace     http://userscripts.org/scripts/show/13333
// @description   Removes ads and unwanted sections (configurable), allows downloading and resizing videos, displays all comments on video page, expands the description, can prevent autoplay and autodownload, adds a HD (High Definition) select, etc...
// @include       http://youtube.tld/*
// @include       http://*.youtube.tld/*
// ==/UserScript==

/*
Author: Mindeye
Script initially based on ETcelera's YousableTube userscript (http://userscripts.org/scripts/show/5906)
Version: 27 Apr 2008
*/

////////////////////////// START OF HELPER FUNCTIONS //////////////////////////

// Shortcut to document.getElementById
function $(id) {
	return document.getElementById(id);
}

// Returns a node from its id or a reference to it
function $ref(idRef) {
	return (typeof(idRef) == "string") ? $(idRef) : idRef;
}

// Gets a Flash variable from the player using the GetVariable method
function $gfv(flashVar) {
	return player.wrappedJSObject.GetVariable(flashVar);  // unsafeWindow context
}

// Sets a Flash variable to the player using the SetVariable method
function $sfv(flashVar, flashNewValue) {
	player.wrappedJSObject.SetVariable(flashVar, flashNewValue); // unsafeWindow context
}

// Deletes a node from its id or a reference to it
function delNode(targetNode) {
	var iNode = $ref(targetNode);
	if (iNode) return iNode.parentNode.removeChild(iNode);
	return null;
}

// Runs a particular XPath expression p against the context node context (or the document, if not provided)
// If a document (docObj) is provided, its evaluate method is used instead of document.evaluate (and it's also the default context)
// Returns the results as an array
function $x(p, context, docObj) {
	if (!docObj) docObj = document;
	if (!context) context = docObj;
	var item, arr = [], xpr = docObj.evaluate(p, context, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
	for (var i = 0; item = xpr.snapshotItem(i); i++) arr.push(item);
	return arr;
}

// Returns only the first element of the array returned by $x (or null if the array was empty)
function $x1(p, context, docObj) {
	var nodeArray = $x(p, context, docObj);
	return (nodeArray.length > 0) ? nodeArray[0] : null;
}

// Creates a new node with the given attributes and properties (be careful with XPCNativeWrapper limitations)
function createNode(type, attributes, props) {
	var node = document.createElement(type);
	if (attributes) {
		for (var attr in attributes) {
			node.setAttribute(attr, attributes[attr]);
		}
	}
	if (props) {
		for (var prop in props) {
			if (prop in node) node[prop] = props[prop];
		}
	}
	return node;
}

// Inserts the specified node as a sibling AFTER the reference element
function insertAfter(newNode, node) {
	return node.parentNode.insertBefore(newNode, node.nextSibling);
}

// Removes all the contents of a node and returns a document fragment with them (or null if the node isn't found)
function removeAllChildren(targetNode) {

	// Checks targetNode
	var iNode = $ref(targetNode);
	if (!iNode) return null;

	// Selects the contents of the node with a range and extracts them from the DOM tree into a document fragment
	var contentsRange = document.createRange();
	contentsRange.selectNodeContents(iNode);
	var contentsFrag = contentsRange.extractContents();
	contentsRange.detach();

	// Returns the document fragment
	return contentsFrag;

}

// Find the absolute location in pixels for a provided element or id
// Returns an object with .x and .y properties or null id the element isn't found
function findXY(eltRef) {
	var x = 0, y = 0, elt = $ref(eltRef);
	if (!elt) return null;
	while (elt !== null) {
		x += elt.offsetLeft - elt.scrollLeft;
		y += elt.offsetTop - elt.scrollTop;
		elt = elt.offsetParent;
	}
	return {x: x, y: y};
}

// Returns true if the video is substituted by an icon
function isVideoIcon() {
	return ($("gssubsIcon") === null) ? false: true;
}

// Reloads the player by removing it from the DOM tree and inserting it again in the same position
// If the video is substituted by an icon, it won't do anything (a reload isn't necessary)
function reloadPlayer() {
	if (isVideoIcon()) return;
	var playerParent = player.parentNode;
	var playerNextSibling = player.nextSibling;
	playerParent.removeChild(player);
	playerParent.insertBefore(player, playerNextSibling);
}

// Gets a Flash string variable from the player
// Returns null if the variable isn't found
function getFlashVar(varName) {

	// Gets the flashvars from the player
	var flashVars = String(player.getAttribute("flashvars"));

	// Searchs for the varName (encoded) in the flashvars
	varName = escape(varName);
	var queryRE = new RegExp("(?:^|&)" + varName + "=([^&]*)");
	var queryRet = queryRE.exec(flashVars);

	// Returns the corresponding value (decoded) or null (if not found)
	return (queryRet === null) ? null : unescape(queryRet[1]);

}

// Sets a Flash string variable to the player
// If doReloadPlayer is true it also reloads the player
function setFlashVar(varName, varNewValue, doReloadPlayer) {

	// Gets varName value now and the flashvars from the player
	var varValue = getFlashVar(varName);
	var flashVars = String(player.getAttribute("flashvars"));

	// Encodes varName y varNewValue
	varName = escape(varName);
	varNewValue = escape(varNewValue);

	// If varName isn't set, just adds it
	// If varName is set, replaces its value with varNewValue
	if (varValue === null) {
		player.setAttribute("flashvars", flashVars + "&" + varName + "=" + varNewValue);
	}
	else {
		varValue = escape(varValue); // Encodes varValue
		var replaceRE = new RegExp("(^|&)" + varName + "=" + varValue);
		flashVars = flashVars.replace(replaceRE, "$1" + varName + "=" + varNewValue);
		player.setAttribute("flashvars", flashVars);
	}

	// Reloads the player
	if (doReloadPlayer) reloadPlayer();

}

// Deletes a Flash string variable from the player
// If doReloadPlayer is true it also reloads the player
function deleteFlashVar(varName, doReloadPlayer) {

	// Gets varName value now and the flashvars from the player
	var varValue = getFlashVar(varName);
	var flashVars = String(player.getAttribute("flashvars"));

	// Deletes varName if it's set
	if (varValue !== null) {

		// Encodes varName y varValue
		varName = escape(varName);
		varValue = escape(varValue);

		// Searchs for varName and deletes it
		var replaceRE = new RegExp("(^|&)" + varName + "=" + varValue + "(&?)");
		flashVars = flashVars.replace(replaceRE, lambdaReplacer);
		player.setAttribute("flashvars", flashVars);
	}

	// Reloads the player
	if (doReloadPlayer) reloadPlayer();

	// Lambda function to remove varName in all scenarios
	// It is a nested function
	function lambdaReplacer(str, p1, p2, soffset, s) {
		return (p1 == "") ? p1 : p2; // p1 ==  "" if (^|&) matches ^ (start of string)
	}

}

// Fires an event on targetNode (an id of a node or a reference to it)
// Returns null if the node isn't found, otherwise it returns the return value of dispatchEvent
function fireEvent(targetNode, evtType, evtName, evtBubble, evtCancelable) {

	var iNode = $ref(targetNode);
	if (iNode) {
		var evtObj = document.createEvent(evtType);
		evtObj.initEvent(evtName, evtBubble, evtCancelable);
		return iNode.dispatchEvent(evtObj);
	}
	else {
		return null;
	}

}

// Calls fireEvent to fire a click event on targetNode
function fireClickEvent(targetNode) {
	return fireEvent(targetNode, "MouseEvent", "click",	true, true);
}

// Calls fireEvent to fire a change event on targetNode
function fireChangeEvent(targetNode) {
	return fireEvent(targetNode, "Event", "change",	true, false);
}

// Builds the download video URL from its parameters (videoId, tId y videoFormat)
function getDownloadVideoURL(vId, vTId, vFormat) {
	return ytHost + "/get_video?video_id=" + vId + "&t=" + vTId + ((vFormat == "") ? "" : "&fmt=" + vFormat);
}

// Updates the URL Input field according to the videoId and the current video format
function updateURLInputField() {
	var URLInput = $("watch-url-field");
	if (URLInput) URLInput.value = ytHost + "/watch?v=" + videoId + "&fmt=" + videoFormat; // &fmt is always included to force LQ if videoFormat is ""
}

// Returns an object with the details of the passed video format (or null is the format is unknown)
function getVideoFormatDetails(vFormat) {

	var sFmt, sVq, sMIME;
	var unknownFormat = false;

	switch(vFormat) {
		case "":
			// Low Quality format
			sVq = "1";
			sFmt = "";
			sMIME = "video/x-flv";
			break;
		case "6":
			// FLV High Quality format
			sVq = "2";
			sFmt = "6/720000/7/0/0";
			sMIME = "video/x-flv";
			break;
		case "18":
			// MPEG-4 H.264 format
			sVq = "2";
			sFmt = "18/512000/9/0/115";
			sMIME = "video/mp4";
			break;
		default:
			// Unknown format
			unknownFormat = true;
	}

	return (unknownFormat) ? null : {vq: sVq, fmt_map: sFmt, MIMEString: sMIME};

}

// Asynchronously checks if a video with the provided parameters is available
// Calls callbackFunc with the video paramaters and an additional one (true, false or null if error) indicating the result
// Returns true if the request was sent, false otherwise
function checkVideoAvailability(vId, vTId, vFormat, callbackFunc) {

	// Checks callbackFunc parameter
	if ((!callbackFunc) || (typeof(callbackFunc) != "function")) return false;

	// Gets the download video URL and sends a HEAD request to check the video avaliability
	// This code may cause a security exception in the error console for violation of JavaScript same origin policy
	var vURL = getDownloadVideoURL(vId, vTId, vFormat);
	var xhrVideo = new XMLHttpRequest();
	xhrVideo.onerror = function(evt) {
		xhrVideo.abort();
		callbackFunc(vId, vTId, vFormat, null); // Error retrieving video availability
	};
	xhrVideo.open("HEAD", vURL, true);
	xhrVideo.onreadystatechange = function(evt) {
		if (xhrVideo.readyState == 2) { // Headers received
			xhrVideo.abort(); // Workaround for bug 343028 ("XMLHttpRequest does a GET after having been redirected from a HEAD")
			if ((xhrVideo.status == 200) || (xhrVideo.status.toString().charAt(0) == "3")) {
				// OK or redirection HTTP status codes
				callbackFunc(vId, vTId, vFormat, true); // Video available
			}
			else {
				callbackFunc(vId, vTId, vFormat, false); // Video unavailable
			}
		}
	};
	xhrVideo.send(null);
	return true;

}

// Returns an array with the Options in the Select element (selNode) with the specified value (or null if none is found)
// selNode can be a Select node id or a reference to it (if selNode doesn't exist or it's not a Select, the function returns null)
// If singleNode is true, it only returns the first Option node found (or null if none)
function selGetOptionsFromValue(selNode, optValue, singleNode) {

	var iNode = $ref(selNode);
	if ((!iNode) || (iNode.nodeName.toUpperCase() != "SELECT")) return null;

	var optsArr = [], selOpts = iNode.options;

	for (var i = 0; i < selOpts.length; i++) {
		// Adds the option to the array if its value is the one requested
		if (selOpts.item(i).value == optValue) optsArr.push(selOpts.item(i));
	}

	if (optsArr.length > 0) {
		return (singleNode) ? optsArr[0] : optsArr;
	}
	else {
		return null; // The array is empty
	}

}

// Extends the String object with a trim funcion
String.prototype.trim = function() {
	return this.replace(/^\s+|\s+$/g, "");
}

// Adds !important to CSS rules of any type
String.prototype.makeImportant = function() {

	var Selector, DeclarationBlock, CssArray = this.match(/([^{]+)({[^{}]+})/);
	if (CssArray === null) {
		// Inline CSS rule (e.g. "display: none") or scripting rule (e.g. element.style.display = "none")
		Selector = "";
		DeclarationBlock = this;
	}
	else {
		// Complete CSS rule (e.g. ".nd {display: none}")
		Selector = CssArray[1];
		DeclarationBlock = CssArray[2];
	}

	// Adds !important to each rule
	if (DeclarationBlock.indexOf(":") != -1) {
		DeclarationBlock = DeclarationBlock.replace(/[^:;{}]+:[^:;{}]+/g, "$& !important");
	}
	else {
		// No estructure could be recognized, so we'll just add !important
		DeclarationBlock += " !important";
	}
	// Remove any !important duplicates
	DeclarationBlock = DeclarationBlock.replace(/(?:\s*!important\s*){2,}/gi, " !important");

	return Selector + DeclarationBlock;

}

// Transforms a number into a valid CSS dimension (in pixels)
Number.prototype.toCSS = function() {
	return String(Math.round(this)) + "px";
}

/////////////////////////// END OF HELPER FUNCTIONS ///////////////////////////

///////////////////////////// START OF CSS STYLES /////////////////////////////

// Adds a class to set anchors' text decoration
// Anchors with the class and descendant anchors of elements with the class are affected (text-decoration isn't automatically inherited)
GM_addStyle("a.gsAnchors, .gsAnchors a {text-decoration: none}".makeImportant());
GM_addStyle("a.gsAnchors:hover, .gsAnchors a:hover {text-decoration: underline}".makeImportant());

// Adds a style for the icon which substitutes the video (if appropriate)
GM_addStyle("#gssubsIcon {margin: 150px auto; display: block; cursor: pointer}".makeImportant());

// Adds styles for the ajax wrapper div and its contents
GM_addStyle("#gsajaxWrapper {margin: 100px auto; cursor: default; text-align: center}".makeImportant());
GM_addStyle("#gsajaxWrapper * {vertical-align: middle}".makeImportant()); // vertical-align isn't automatically inherited
GM_addStyle("#gsloadingIcon {margin: 5px}".makeImportant());
GM_addStyle("#gsprogressMeter {font-variant: small-caps}".makeImportant());
GM_addStyle("#gsabortButton {margin: 5px auto; display: block}".makeImportant());

// Adds styles and classes for the configuration layers and its contents
GM_addStyle(("#gsmaskLayer {background-color: black; opacity: 0.5; z-index: 100; " +
						 "position: fixed; left: 0px; top: 0px; width: 100%; height: 100%}").makeImportant());
GM_addStyle(("#gsdialogLayer {background-color: #EEEEEE; overflow: auto; padding: 5px; z-index: 101; " +
						 "outline: black solid thin; position: fixed; left: 30%; top: 10%; width: 40%; height: 80%}").makeImportant());
GM_addStyle("#gsdialogLayer > * {margin: 20px 0px}".makeImportant());
GM_addStyle("#gsdialogLayer li {margin: 15px 0px 7px; font-style: italic}".makeImportant());
GM_addStyle("#gsdialogLayer input, #gsdialogLayer select {vertical-align: middle}".makeImportant());
GM_addStyle("#gsconfTitle {cursor: default; font-size: 150%; font-weight: bold; text-align: center}".makeImportant());
GM_addStyle("#gsconfButDiv {text-align: center}".makeImportant());
GM_addStyle("#gsconfButDiv input {margin: 5px}".makeImportant());

////////////////////////////// END OF CSS STYLES //////////////////////////////

///////////////////////// START OF USER CONFIGURATION /////////////////////////

/*
	Use "YousableTubeFix configuration" menu command to configure the script
	The menu is under "Tools" / "Greasemonkey" / "User Script Commands"
*/

/*
	Obsolete configuration variables names, do not use:
	- addCharCounter --> Add character counter feature (dropped, added by YouTube)
	- forceHDMode --> Force format 18 video mode (dropped in favor of the more general defaultVideoFormat)
*/

// Default video player size (a floating point number or either "fill" or "max")
var videoSize = GM_getValue("videoSize", "fill");

// Optional junk removal (either "true" or "false", without quotes)
var removeBrand = GM_getValue("removeBrand", true);
var removeAlsoWatching = GM_getValue("removeAlsoWatching", true);
var removeEmbed = GM_getValue("removeEmbed", false);
var removeURL = GM_getValue("removeURL", false);
var removeRatings = GM_getValue("removeRatings", false);
var removeActions = GM_getValue("removeActions", false);
var removeComments = GM_getValue("removeComments", false);
var removeStats = GM_getValue("removeStats", false);
var removeCharCounter = GM_getValue("removeCharCounter", false);
var removeMoreUserVideos = GM_getValue("removeMoreUserVideos", false);
var removeRelatedVideos = GM_getValue("removeRelatedVideos", false);
var removeFooterCopyright = GM_getValue("removeFooterCopyright", false);

// Automatically expand the video info, hide collapse link
var expandInfo = GM_getValue("expandInfo", true);
var hideCollapse = GM_getValue("hideCollapse", true);

// Substitutes the video with an icon, preventing autoplay and autodownload
var videoToIcon = GM_getValue("videoToIcon", false);

// Makes the page size of the comments in the video page bigger (500 comments per page)
var biggerComments = GM_getValue("biggerComments", false);

// Forces the change of the video format
var defaultVideoFormat = GM_getValue("defaultVideoFormat", "0");

// Function to validate video size input. Returns a valid videoSize string or null (if input isn't valid)
function valVideoSize(vs) {

	var cs = String(vs).toLowerCase().trim();

	if ((cs === "fill") || (cs === "max")) return cs;
	if (!isNaN(parseFloat(cs))) return Math.abs(parseFloat(cs)).toString();
	return null;

}

// Configuration function
function configureScript(e) {

	// Gets the layers
	var maskLayer = $("gsmaskLayer");
	var dialogLayer = $("gsdialogLayer");

	// Checks the layers state
	// Creates the layers if they don't exist or displays them if they are hidden
	if ((maskLayer) && (dialogLayer)) {
		if ((maskLayer.style.display == "none") && (dialogLayer.style.display == "none")) {
			setDialogInputState(true); // Makes sure the input/select fields are enabled
			maskLayer.style.display = "";
			dialogLayer.style.display = "";
		}
		dialogLayer.focus();
	}
	else {
		createLayers();
	}

	return; // Exit function


	// Creates the configuration layers
	// It is a nested function
	function createLayers() {

		// Creates a layer to mask the page during configuration
		maskLayer = createNode("div", {id: "gsmaskLayer", title: "Click here to return to the page"});

		// Creates a layer for the configuration dialog
		dialogLayer = createNode("div", {id: "gsdialogLayer"});

		// Creates the configuration dialog HTML
		dialogLayer.innerHTML = "<div id='gsconfTitle'>YousableTubeFix Configuration</div>" +
		"<ul>" +
		"<li>Select the default video size. Enter a floating point number or either \"fill\" or \"max\":</li>" +
		"<input type='text' id='gsconfvideoSize' value='" + videoSize + "'>" +
		"<li>Remove:</li>" +
		"<input type='checkbox' id='gsconfremoveBrand'" + (removeBrand ? " checked='checked'" : "") + ">Channel brand<br>" +
		"<input type='checkbox' id='gsconfremoveAlsoWatching'" + (removeAlsoWatching ? " checked='checked'" : "") + ">\"Also Watching Now\" section<br>" +
		"<input type='checkbox' id='gsconfremoveEmbed'" + (removeEmbed ? " checked='checked'" : "") + ">Embed section<br>" +
		"<input type='checkbox' id='gsconfremoveURL'" + (removeURL ? " checked='checked'" : "") + ">URL section<br>" +
		"<input type='checkbox' id='gsconfremoveRatings'" + (removeRatings ? " checked='checked'" : "") + ">Ratings section<br>" +
		"<input type='checkbox' id='gsconfremoveActions'" + (removeActions ? " checked='checked'" : "") + ">Actions section<br>" +
		"<input type='checkbox' id='gsconfremoveStats'" + (removeStats ? " checked='checked'" : "") + ">Stats section<br>" +
		"<input type='checkbox' id='gsconfremoveMoreUserVideos'" + (removeMoreUserVideos ? " checked='checked'" : "") + ">More videos from this user section<br>" +
		"<input type='checkbox' id='gsconfremoveRelatedVideos'" + (removeRelatedVideos ? " checked='checked'" : "") + ">Related videos section<br>" +
		"<input type='checkbox' id='gsconfremoveFooterCopyright'" + (removeFooterCopyright ? " checked='checked'" : "") + ">Footer and copyright sections<br>" +
		"<input type='checkbox' id='gsconfremoveCharCounter'" + (removeCharCounter ? " checked='checked'" : "") + ">Character counter" +
		"<li>Video information:</li>" +
		"<input type='checkbox' id='gsconfexpandInfo'" + (expandInfo ? " checked='checked'" : "") + ">Automatically expanded<br>" +
		"<input type='checkbox' id='gsconfhideCollapse'" + ((hideCollapse && expandInfo) ? " checked='checked'" : "") + ">Hide collapse link" +
		"<li>Comments on the video page:</li>" +
		"<input type='radio' id='gsconfcommentsNormal' name='gsconfcommentsChoice'" + ((!removeComments && !biggerComments) ? " checked='checked'" : "") + ">Normal<br>" +
		"<input type='radio' id='gsconfcommentsBigger' name='gsconfcommentsChoice'" + ((!removeComments && biggerComments) ? " checked='checked'" : "") + ">More comments<br>" +
		"<input type='radio' id='gsconfcommentsNone' name='gsconfcommentsChoice'" + (removeComments ? " checked='checked'" : "") + ">None" +
		"<li><input type='checkbox' id='gsconfvideoToIcon'" + (videoToIcon ? " checked='checked'" : "") + ">Prevent autoplay and autodownload</li>" +
		"<li>" +
		"Select the default video format: &nbsp;" +
		"<select id='gsconfdefaultVideoFormatSel' size='1'>" +
		"<option id='gsconfdefaultVideoFormatOptAuto' value='0'" + ((defaultVideoFormat === "0") ? " selected='selected'" : "") + ">Automatic</option>" +
		"<option id='gsconfdefaultVideoFormatOptLQ' value=''" + ((defaultVideoFormat === "") ? " selected='selected'" : "") + ">Low Quality</option>" +
		"<option id='gsconfdefaultVideoFormatOpt6' value='6'" + ((defaultVideoFormat === "6") ? " selected='selected'" : "") + ">FLV High Quality</option>" +
		"<option id='gsconfdefaultVideoFormatOpt18' value='18'" + ((defaultVideoFormat === "18") ? " selected='selected'" : "") + ">MPEG-4 H.264</option>" +
		"</select>" +
		"</li>" +
		"</ul>" +
		"<div id='gsconfButDiv'>" +
		"<input type='button' id='gsconfOKBut' value='OK' title='Save the current configuration'>" +
		"<input type='button' id='gsconfCancelBut' value='Cancel' title='Return to the page without saving'>" +
		"</div>";

		// Appends the layers to the document
		document.body.appendChild(maskLayer);
		document.body.appendChild(dialogLayer);

		// Adds the necessary event listeners
		maskLayer.addEventListener("click", hideLayers, false);
		$("gsconfexpandInfo").addEventListener("click", videoInfoLogic, false);
		$("gsconfOKBut").addEventListener("click", saveConfiguration, false);
		$("gsconfCancelBut").addEventListener("click", hideLayers, false);

	}

	// Changes the enabled state of all input/select fields of the dialog layer. If newState is undefined or not boolean, it does nothing
	// It is a nested function
	function setDialogInputState(newState) {
		if (typeof(newState) != "boolean") return;
		var allInputs = $x(".//input|.//select", dialogLayer);
		for (var i = 0; i < allInputs.length; i++) allInputs[i].disabled = !newState;
	}

	// Checks the logic of the video information choices
	// It is called by the expand info checkbox event listener
	// It is a nested function
	function videoInfoLogic(evt) {
		var infoState = $("gsconfexpandInfo").checked;
		var confhideCollapse = $("gsconfhideCollapse");
		if (infoState === false) confhideCollapse.checked = false;
		confhideCollapse.disabled = !infoState;
	}

	// Exits the configuration by hiding the layers
	// It is called by the Cancel button and the maskLayer event listeners
	// It is a nested function
	function hideLayers(evt) {
		dialogLayer.style.display = "none";
		maskLayer.style.display = "none";
	}

	// Checks and saves the configuration to the configuration variables
	// It is called by the Ok button event listener
	// It is a nested function
	function saveConfiguration(evt) {

		// Disables the input/select fields
		setDialogInputState(false);

		// Checks default video size
		var valvs = valVideoSize($("gsconfvideoSize").value);
		if (valvs === null) {
			window.alert("Invalid default video size");
			setDialogInputState(true); // Re-enables the input/select fields
			$("gsconfvideoSize").focus();
			return;
		}
		GM_setValue("videoSize", valvs);

		// Sets other configuration variables
		GM_setValue("removeBrand", $("gsconfremoveBrand").checked);
		GM_setValue("removeAlsoWatching", $("gsconfremoveAlsoWatching").checked);
		GM_setValue("removeEmbed", $("gsconfremoveEmbed").checked);
		GM_setValue("removeURL", $("gsconfremoveURL").checked);
		GM_setValue("removeRatings", $("gsconfremoveRatings").checked);
		GM_setValue("removeActions", $("gsconfremoveActions").checked);
		GM_setValue("removeStats", $("gsconfremoveStats").checked);
		GM_setValue("removeMoreUserVideos", $("gsconfremoveMoreUserVideos").checked);
		GM_setValue("removeRelatedVideos", $("gsconfremoveRelatedVideos").checked);
		GM_setValue("removeFooterCopyright", $("gsconfremoveFooterCopyright").checked);
		GM_setValue("removeCharCounter", $("gsconfremoveCharCounter").checked);
		GM_setValue("expandInfo", $("gsconfexpandInfo").checked);
		GM_setValue("hideCollapse", ($("gsconfexpandInfo").checked ? $("gsconfhideCollapse").checked : false));
		GM_setValue("biggerComments", $("gsconfcommentsBigger").checked ? true : false);
		GM_setValue("removeComments", $("gsconfcommentsNone").checked ? true : false);
		GM_setValue("videoToIcon", $("gsconfvideoToIcon").checked);
		GM_setValue("defaultVideoFormat", $("gsconfdefaultVideoFormatSel").value);

		// Reloads page and script
		window.location.reload();

	}

}

// Register configuration menu command
GM_registerMenuCommand("YousableTubeFix Configuration", configureScript, null, null, "Y");

////////////////////////// END OF USER CONFIGURATION //////////////////////////

// YouTube Video URL:    http://(Youtube hostname)/watch?v=(videoId)[&fmt=(videoFormat)]
// YouTube Download URL: http://(Youtube hostname)/get_video?video_id=(videoId)&t=(tId)[&fmt=(videoFormat)]

// Removes "New on Youtube" ads (search results page)
delNode("sideContentWithPVA");

// Gets the video and its parameters (Id, Tracking Id y Video Format)
var player = $("movie_player");
if (!player) return;

var videoId = getFlashVar("video_id");
if (videoId === null) return;
var tId = getFlashVar("t");
if (tId === null) return;

var videoFormatMatch = null, videoFormat = getFlashVar("fmt_map");
if (videoFormat !== null) videoFormatMatch = videoFormat.match(/^(\d+)(?:\/\d+){4}$/);
if ((videoFormat === null) || (videoFormatMatch === null)) {
	// Video is LQ (its fmt_map isn't set or doesn't match the regex)
	videoFormat = "";
}
else {
	// fmt_map is ok. videoFormat will be set according to the vq parameter	(user video quality account preference)
	switch(getFlashVar("vq")) {
		case "":
		case null:
		case "1":
			// vq is empty, doesn't exist or the user has chosen to load always the LQ format. Video is LQ
			videoFormat = "";
			break;
		case "2":
			// The user has chosen to load always the HQ version (or the page was called with an fmt parameter). Video is HQ
			videoFormat = videoFormatMatch[1];
			break;
		case "0":
		case "null":
			// The user is unregistered or has chosen to let the player decides the format based on the bandwidth
			// We'll try to get the chosen vq from the player, but it's unreliable due to a race condition (LQ will be chosen if that fails)
			videoFormat = ($gfv("vq") == "2") ? videoFormatMatch[1] : "";
			break;
		default:
			// vq isn't recognized. Video is LQ
			videoFormat = "";
	}
}

// Gets the YouTube base URL, the video download URL and the MIMEString
var ytHost = window.location.protocol + "//" + window.location.host;
var videoURL = getDownloadVideoURL(videoId, tId, videoFormat);

var MIMEString = getVideoFormatDetails(videoFormat);
MIMEString = (MIMEString === null) ? "" : MIMEString.MIMEString;

// Adds the download video link
var vidTitle = $("watch-vid-title");
if (!vidTitle) return;
var vidTitleLink = createNode("a", {id: "gsvidTitleLink", class: "gsAnchors", href: videoURL,
																		title: "Click here to download the video"}, {textContent: vidTitle.textContent});
if (MIMEString != "") vidTitleLink.type = MIMEString;
removeAllChildren(vidTitle);
vidTitle.appendChild(vidTitleLink);

// Updates the URL input field according to the video parameters
updateURLInputField();

// Removes "Promoted videos" ads (and its parent wrapper)
delNode($("watch-promoted-container").parentNode);

// Removes Channel Brand
if (removeBrand) {
	delNode("watch-channel-brand-cap");
	delNode("watch-channel-brand-div");
}

// Removes "Also Watching Now" section
if (removeAlsoWatching) delNode("watch-active-sharing");

// Removes Ratings section
if (removeRatings) delNode("watch-ratings-views");

// Removes Actions section ("Share", "Favorite", "Playlists" and "Flag")
if (removeActions) delNode("watch-actions-area");

// Removes comments section and Stats section
if ((removeComments) || (removeStats)) {

	var commentsStatsTable = $("watch-comments-stats");

	// If both options are activated, removes the entire table
	if ((removeComments) && (removeStats)) {
		delNode(commentsStatsTable);
	}

	// Only one option is activated
	else {

		if (removeComments) {
			delNode("watch-tab-commentary-body");
			delNode("watch-tab-commentary");
		}

		if (removeStats) {
			delNode("watch-tab-stats-body");
			delNode("watch-tab-stats");
		}

		// Activates the remaining tab and removes the tab selector (it isn't necessary with only one tab left)
		var tabSelector = $x1(".//div[@class='watch-tabs']", commentsStatsTable);
		var tabSelectorLinkLeft = $x1(".//a", tabSelector);
		if (tabSelectorLinkLeft) fireClickEvent(tabSelectorLinkLeft);
		delNode(tabSelector);

	}

}

// Removes "Embed" section
if (removeEmbed) delNode("watch-embed-div");

// Removes URL section
if (removeURL) delNode("watch-url-div");

// Removes "more videos from this user" section (and its parent wrapper)
if (removeMoreUserVideos) delNode($("more-from-panel").parentNode);

// Removes related videos section
if (removeRelatedVideos) delNode("watch-related-videos-panel");

// Removes footer and copyright sections
if (removeFooterCopyright) {
	delNode("footer");
	delNode("copyright");
}

// Sets the necessary watchpoints to remove the character counters when YouTube inserts them (changing the div innerHTML)
// DOM mutation events are unreliable (at present) when using innerHTML, so watchpoints are better here
// The watchpoints aren't removed later because YouTube sometimes changes its innerHTML more than once
if (removeCharCounter) {
	var postCommentDiv1 = $("div_main_comment");
	var postCommentDiv2 = $("div_main_comment2");

	if (postCommentDiv1) postCommentDiv1.wrappedJSObject.watch("innerHTML", watchCharCounterDiv); // unsafeWindow context
	if (postCommentDiv2) postCommentDiv2.wrappedJSObject.watch("innerHTML", watchCharCounterDiv); // unsafeWindow context
}

// Watchpoint to dinamically remove the character counters' code from the innerHTML set by YouTube functions
// It is called by changes in the postCommentDiv[1/2]s' innerHTML
function watchCharCounterDiv(prop, oldVal, newVal) {

	// Gets the textarea HTML
	var taRE = /<textarea [^>]*\bname="comment"[^>]*>/i;
	var taString = newVal.match(taRE);

	if (taString !== null) {
		taString = taString[0].replace(/updateCharCount\([^)]*\);?/gi, ""); // Removes references to the update function
		taString = taString.replace(/\bon\w+=""/gi, ""); // Removes empty event listeners
		newVal = newVal.replace(taRE, taString); // Updates the textarea HTML
	}

	// Removes the character counters' labels
	newVal = newVal.replace(/<span [^>]*\bid="maxCharLabelmain_comment2?"[^>]*>[^<]*<\/span>/i, "");
	newVal = newVal.replace(/<input [^>]*\bid="charCountmain_comment2?"[^>]*>/i, "");

	// Returns the new innerHTML
	return newVal;

}

// Expands the video information
if (expandInfo) {
	var expandLink = $x1("//div[@id='watch-video-details']//div[@class='collapse-content']//a[@class='eLink']");
	if (expandLink) {
		fireClickEvent(expandLink); // Fires a click event on expandLink
		if (hideCollapse) {
			var collapseLink = $x1("//div[@id='watch-video-details']//div[@class='expand-content']//a[@class='eLink']");
			delNode(collapseLink.parentNode); // Deletes the "(less information)" link and its parent wrapper
		}
	}
}

// Saves original video dimensions and aspect ratio for resizeVideo's use
var oSize = {width: player.clientWidth, height: player.clientHeight,
						 AR: player.clientWidth / player.clientHeight};

// Substitutes the video with an icon
var playerDiv = $("watch-player-div");
if (!playerDiv) return;
if (videoToIcon) {
	var subsIcon = createNode("img", {id: "gssubsIcon", alt: "Video",
																		title: "Click here to play the video",
																		src: ytHost + "/img/c_logo_no_text.gif"});
	playerDiv.replaceChild(subsIcon, player); //Replaces the player with the icon
	subsIcon.addEventListener("click", restoreVideo, false); //Adds an event listener to the icon
}

// Function to remove the icon and restore the video
// It is called by subsIcon event listener
function restoreVideo(evt) {
	playerDiv.replaceChild(player, subsIcon);
	resizeVideo(videoSize);
}

// Moves player below title
insertAfter(playerDiv, vidTitle);

// Adds video resize links
var watchFull = unsafeWindow["fullscreenUrl"].toString();
var linkDiv = createNode("div", {id: "gsresizeLinks", class: "gsAnchors"});
linkDiv.innerHTML = "<a id='gsresizeLink0' href='javascript:void(null)'>1x</a> " +
	"- <a id='gsresizeLink1' href='javascript:void(null)'>1.25x</a> " +
	"- <a id='gsresizeLink2' href='javascript:void(null)'>1.5x</a> " +
	"- <a id='gsresizeLink3' href='javascript:void(null)'>1.75x</a> " +
	"- <a id='gsresizeLink4' href='javascript:void(null)'>2x</a> " +
	"- <a id='gsresizeLink5' href='javascript:void(null)'>fill</a> " +
	"- <a id='gsresizeLink6' href='javascript:void(null)'>max</a> " +
	"- <a id='gsresizeLink7' href='" + watchFull + "'>full</a> " +
	"- <a id='gsresizeLink8' href='" + watchFull + "' target='_blank'>fullscreen</a>";

// Adds the CVF ("change video format") select
linkDiv.innerHTML += " - Format: ";
var selVideoFormat = createNode("select", {id: "gsselVideoFormat", size: "1", title: "Choose video format"});

// Creates and adds the CVF options to the CVF select
var optVideoFormatLQ = createNode("option", {id: "gsoptVideoFormatLQ", value: ""}, {textContent: "Low Quality"});
var optVideoFormat6 = createNode("option", {id: "gsoptVideoFormat6", value: "6"}, {textContent: "FLV High Quality"});
var optVideoFormat18 = createNode("option", {id: "gsoptVideoFormat18", value: "18"}, {textContent: "MPEG-4 H.264"});

selVideoFormat.add(optVideoFormatLQ, null);
selVideoFormat.add(optVideoFormat6, null);
selVideoFormat.add(optVideoFormat18, null);

// Chooses the CVF select selected option based on the current video format
var optVideoFormatCurrent = selGetOptionsFromValue(selVideoFormat, videoFormat, true);
if (optVideoFormatCurrent === null) {
	// There isn't any option with a value equal to the current video format. No option is selected
	selVideoFormat.selectedIndex = -1;
}
else {
	// Selects the option with a value equal to the current video format
	optVideoFormatCurrent.selected = true;
}

// Inserts the CVF select into the video resize links div
linkDiv.appendChild(selVideoFormat);

// Inserts the video resize links div after the player
insertAfter(linkDiv, playerDiv);

linkDiv.addEventListener("click", resizeClick, false); // Adds an event listener to the div for the resize links
selVideoFormat.addEventListener("change", selChangeVideoFormat, false); // Adds an event listener to the CVF select

// Forces a video mode by selecting the corresponding CVF option
if (defaultVideoFormat !== "0") {
	var optVideoFormatDefault = selGetOptionsFromValue(selVideoFormat, defaultVideoFormat, true);
	if ((optVideoFormatDefault !== null) && (selVideoFormat.selectedIndex != optVideoFormatDefault.index)) {
		// The video format is known and isn't selected. Selects it
		optVideoFormatDefault.selected = true;
		fireChangeEvent(selVideoFormat); // Manual fire of the change event (script changes don't fire it)
	}
}

// Resize the video to default size
resizeVideo(videoSize);

// Function to change the player and the video download link to the selected video format in the CVF select
// It is called by the CVF select event listener
function selChangeVideoFormat(evt) {

	var selNewValue = evt.target.value;

	// Gets the video format details and updates the player, videoFormat and MIMEString
	var videoFormatDetails = getVideoFormatDetails(selNewValue);
	if (videoFormatDetails !== null) {
		setFlashVar("vq", videoFormatDetails.vq, false);
		setFlashVar("fmt_map", videoFormatDetails.fmt_map, true);
		videoFormat = selNewValue;
		MIMEString = videoFormatDetails.MIMEString;
	}

	// Makes sure that the CVF select color is the same one of the selected option for esthetic purpose
	selVideoFormat.style.color = selVideoFormat.options.item(selVideoFormat.selectedIndex).style.color;

	// Updates the download video link
	videoURL = getDownloadVideoURL(videoId, tId, videoFormat);
	vidTitleLink.href = videoURL;
	vidTitleLink.type = MIMEString;

	// Updates the URL input field
	updateURLInputField();

}

// Function used to resize video
function resizeVideo(aSize) {

	var newH, newW;
	// Gets viewport dimensions without scrollbars (in Strict mode)
	var vh = document.documentElement.clientHeight, vw = document.documentElement.clientWidth;

	// If the video is substituted by an icon, only scroll up/down to get the video title on top
	if (isVideoIcon()) {
		window.scrollTo(0, findXY(vidTitle).y);
		return; // Exit function
	}

	switch(aSize) {
		case "fill":
			// Fill the space available in the parent div, preserving the aspect ratio
			newW = playerDiv.clientWidth;
			newH = newW / oSize.AR;
			break;
		case "max":
			// Fill the viewport dimensions completely, preserving the aspect ratio
			if (vw > vh) {
				newH = vh;
				newW = newH * oSize.AR;
			}
			else {
				newW = vw;
				newH = newW / oSize.AR;
			}
			break;
		default:
			// Multiply the original dimensions by the aSize factor
			newW = oSize.width * aSize;
			newH = oSize.height * aSize;
	}

	// Resizes the player
	player.style.width = newW.toCSS();
	player.style.height = newH.toCSS();

	// Centers the player, video title and resize links horizontally
	player.style.position = "relative"; // Enables CSS relative positioning
	player.style.left = ((playerDiv.clientWidth - newW) / 2).toCSS(); // Calculates the offset
	vidTitle.style.position = "relative";
	vidTitle.style.left = player.style.left;
	linkDiv.style.position = "relative";
	linkDiv.style.left = player.style.left;

	// Scrolls up/down to get the video title (or the player if aSize is "max") on top
	var scrollPos = findXY((aSize == "max") ? player : vidTitle);
	window.scrollTo(0, scrollPos.y);

}

// Funtion used to call resizeVideo with the appropriate aSize parameter
// It is called by the resize links event listener when a click on a child element (or on the div itself) bubbles up to the resize links div
function resizeClick(evt) {

	var linkId = evt.target.id; // Gets the id of the element clicked
	if (!linkId || (evt.target.nodeName.toUpperCase() != "A")) return;  // Only A links with id will be handled
	if (linkId.search(/^gsresizeLink[0-6]$/) == -1) return; // Only resize links from 0 to 6 will be handled

	var linkSize = valVideoSize(evt.target.textContent); // Gets a valid video size
	if (linkSize !== null) resizeVideo(linkSize);

}

// Sends requests to show information in the CVF options about the video format availability
for (var i = 0; i < selVideoFormat.options.length; i++) {
	checkVideoAvailability(videoId, tId, selVideoFormat.options.item(i).value, selVideoFormatAvailability);
}

// Function to change the CVF options' appearance to indicate the availability of their formats
// It is called by the checkVideoAvailability function when the request returns
function selVideoFormatAvailability(vId, vTId, vFormat, vAvailabilityRet) {
	var optAvailability = selGetOptionsFromValue(selVideoFormat, vFormat, true);
	if (optAvailability !== null) {
		switch(vAvailabilityRet) {
			case true:
				optAvailability.style.color = "green";
				optAvailability.textContent += " (available)";
				break;
			case false:
				optAvailability.style.color = "red";
				optAvailability.textContent += " (unavailable)";
				break;
			case null:
				optAvailability.style.color = "purple";
				optAvailability.textContent += " (request error)";
				break;
		}
		// Makes sure that the CVF select color is the same one of the selected option for esthetic purpose
		selVideoFormat.style.color = selVideoFormat.options.item(selVideoFormat.selectedIndex).style.color;
	}
}

// Substitutes the original page size of comments in video page (10) for the bigger one used in the view all comments page (500)
// Other page sizes can't be easily used because Youtube ajax page only accepts those values in the pagesize parameter (others seem to default to 10)
if (biggerComments) {

	// Gets necessary data from the page
	var recentComments = $("recent_comments");
	var commentsThreshold = $x1("//div[@id='watch-tab-commentary-body']//form//select[@name='commentthreshold']");

	if (recentComments) {

		// Creates the elements that will indicate that the comments are being loaded
		var loadingIcon = createNode("img", {id: "gsloadingIcon", alt: "Loading...",
																				 src: ytHost + "/img/icn_loading_animated.gif"});
		var progressMeter = createNode("span", {id: "gsprogressMeter"});
		var abortButton = createNode("input", {id: "gsabortButton", title: "Abort the transaction", type: "button", value: "Abort"});
		var ajaxWrapper = createNode("div", {id: "gsajaxWrapper", title: "The comments are being loaded..."});

		// Inserts the contents within the wrapper
		ajaxWrapper.appendChild(loadingIcon);
		ajaxWrapper.appendChild(progressMeter);
		ajaxWrapper.appendChild(abortButton);

		// Removes the original comments and save them into a document fragment
		var recentCommentsFrag = removeAllChildren(recentComments);

		// Inserts the wrapper within the now empty div
		recentComments.appendChild(ajaxWrapper);

		// Adds an event listener to the abort button
		abortButton.addEventListener("click", abortAjax, false);

		// Gets the XML data from Youtube
		// GM_xmlhttpRequest's privileged features aren't necessary and it doesn't support responseXML without using DOMParser
		var xhrComments = new XMLHttpRequest();
		xhrComments.onload = function(evt) {

			// Checks for errors
			if ((xhrComments.readyState != 4) || (xhrComments.status != 200) || (!xhrComments.responseXML)) {
				restoreComments();
				return;
			}

			// The data was received. It is now used to fill the recent comments div
			var xmlData = $x1("//html_content", null, xhrComments.responseXML);
			var xmlReturnCodeNode = $x1("//return_code", null, xhrComments.responseXML);
			var xmlReturnCode = ((xmlReturnCodeNode) && (xmlReturnCodeNode.textContent)) ? xmlReturnCodeNode.textContent : null;
			if ((xmlData) && (xmlData.textContent) && (xmlReturnCode === "0")) {
				recentComments.innerHTML = xmlData.textContent;
			}
			else {
				restoreComments();
				return;
			}

			// Change the commentsThreshold combobox (if it exists) so it won't restore the old pagesize
			if ((commentsThreshold) && (commentsThreshold.hasAttribute("onchange"))) {
				commentsThreshold.setAttribute("onchange", commentsThreshold.getAttribute("onchange").replace("&page_size=10", "&page_size=500"));
			}

		}
		xhrComments.onprogress = function(evt) {
			progressMeter.textContent = Math.round(((evt.position / evt.totalSize) * 100)) + "% completed";
		}
		xhrComments.onerror = function(evt) {
			restoreComments();
		}
		var commentsURL = ytHost + "/watch_ajax?v=" + videoId + "&action_get_comments=1&p=1&commentthreshold=" + ((commentsThreshold) ? commentsThreshold.value : -5) + "&page_size=500";
		xhrComments.open("GET", commentsURL, true);
		xhrComments.send(null);

	}

}

// Function to remove the ajax wrapper (with the loading icon, etc...) and restore the original comments
// It is called if the ajax transaction fails or is aborted
function restoreComments() {
	recentComments.replaceChild(recentCommentsFrag, ajaxWrapper);
}

// Function to abort the ajax transaction
// It is called by the ajax abort button event listener
function abortAjax(evt) {
	abortButton.disabled = true;
	if (xhrComments) xhrComments.abort();
	restoreComments();
}