YouTube 18+

By Raxta Kongen Last update Aug 8, 2009 — Installed 1,382 times.

There are 8 previous versions of this script.

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

// ==UserScript==
// @name          YouTube 18+
// @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/*
// @version       18+
// ==/UserScript==

/*
*/

////////////////////////// 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
// As the Security Manager may veto the action, errors will be cached (null will be returned if that happens)
function $gfv(flashVar) {
	var retVal;
	try {
		retVal = uwPlayer.GetVariable(flashVar);  // unsafeWindow context
	}
	catch(err) {
		retVal = null;
	}
	return retVal;
}

// Sets a Flash variable to the player using the SetVariable method
// As the Security Manager may veto the action, errors will be cached
function $sfv(flashVar, flashNewValue) {
	try {
		uwPlayer.SetVariable(flashVar, flashNewValue); // unsafeWindow context
	}
	catch(err) {}
}

// 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 arr = [], xpr = docObj.evaluate(p, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
	for (var i = 0, l = xpr.snapshotLength; i < l; i++) arr.push(xpr.snapshotItem(i));
	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;

}

// Returns the parent node of targetNode (an id of a node or a reference to it)
// Returns null if the node isn't found or it has no parent node
function getParent(targetNode) {
	var iNode = $ref(targetNode);
	if (!iNode) return null;
	return (iNode.parentNode) ? iNode.parentNode : null;
}

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

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

// Returns true is the playerDiv has been moved to its final position (where it can be resized)
function isVideoPositioned() {
	return (playerDiv.previousSibling.id == "watch-vid-title");
}

// 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
// The function doesn't escape/unescape variables or values (be careful with special characters like "&" or "=")
function getFlashVar(varName) {

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

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

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

}

// Sets a Flash string variable to the player
// If doReloadPlayer is true it also reloads the player
// The function doesn't escape/unescape variables or values (be careful with special characters like "&" or "=")
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"));

	// 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 {
		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
// The function doesn't escape/unescape variables or values (be careful with special characters like "&" or "=")
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) {

		// 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 the object of the video formats array associated with the passed video format (or null is the format is unknown)
function getVideoFormatObj(vFormat) {
	var videoFormatDet = videoFormatsArray.filter(function(vf) {return (vf.idx == vFormat);});
	return (videoFormatDet.length > 0) ? videoFormatDet[0] : null;
}

// 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
// It uses an indirect method because of the multiple bugs of XMLHttpRequest and redirections (bug 343028, 238144, etc...)
function checkVideoAvailability(vId, vTId, vFormat, callbackFunc) {

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

	// Low Quality Format availability can't be checked with this method, so it's asummed to be always available
	if (vFormat === "") {
		callbackFunc(vId, vTId, vFormat, true);
		return true;
	}

	// Gets the Watch Video URL of the requested format and downloads it
	// Then checks the swfArgs object in its source code for the fmt_map parameter of the would be player
	// YouTube checks the video format availability, and only sends the corresponding fmt_map if it is available
	var vURL = ytHost + "/watch?v=" + vId + "&fmt=" + vFormat;
	var xhrVideo = new XMLHttpRequest();
	xhrVideo.onerror = function(evt) {
		xhrVideo.abort();
		callbackFunc(vId, vTId, vFormat, null); // Error retrieving video availability
	};
	xhrVideo.onload = function(evt) {

		// Checks for errors
		if ((xhrVideo.readyState != 4) || (xhrVideo.status != 200)) {
			callbackFunc(vId, vTId, vFormat, null); // Error retrieving video availability
			return;
		}

		// Gets the swfArgs object string from the source code
		var responseMatch = xhrVideo.responseText.match(/^\s*var swfArgs = ({[^}]*});\s*$/im);
		if (responseMatch === null) {
			callbackFunc(vId, vTId, vFormat, null); // Error retrieving video availability
			return;
		}
		else {
			responseMatch = responseMatch[1];
		}

		// Evals the swfArgs object string to a real object and gets its fmt_map member
		var objArgs = eval("(" + responseMatch + ")");
		var sMap = objArgs.fmt_map || "";

		// Tests if the fmt_map correspond to the requested format (that should only happen if the video is available)
		if ((new RegExp("^" + vFormat + "(/\\d+){4}(?:,|$)")).test(sMap)) {
			callbackFunc(vId, vTId, vFormat, true); // Video available
		}
		else {
			callbackFunc(vId, vTId, vFormat, false); // Video unavailable
		}

	};
	xhrVideo.open("GET", vURL, true);
	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;

	// Gets a filtered array with the elements with the same value as the one requested
	var optsArr = Array.filter(iNode.options, function(opt) {return (opt.value == optValue);});

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

}

// Returns the available and selectable CVF option from the CVF select with the highest user quality index (or null if none is found)
// If bLowestQuality is true, it returns the CVF option with the lowest user QI instead
function selGetAvailableVideoFormatOption(bLowestQuality) {

	var videoFormatAvailableQuality = null;

	// As iQINow keeps count of the user QI of the preferred option to return, its initial value will be the lowest/highest possible value
	// All the available options will have a higher/lower user QI, so the preferred option will be updated if any is found
	var iQINow = (bLowestQuality) ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;

	// Iterates through the video formats array
	videoFormatsArray.forEach(function(vf) {

		if ((vf.isAvailable === 1) && (vf.userChosen)) {

			// The video format is available and selectable

			// Compares the video format user QI with iQINow
			if ((bLowestQuality) ? (vf.userQI < iQINow) : (vf.userQI > iQINow)) {
				// This video format has a higher/lower user QI. Updates the preferred video format and iQINow
				videoFormatAvailableQuality = vf.idx;
				iQINow = vf.userQI;
			}

		}
	});

	// Returns the CVF option associated with the preferred video format (or null if none of the video formats is available)
	return (videoFormatAvailableQuality === null) ? null : selGetOptionsFromValue(selVideoFormat, videoFormatAvailableQuality, true);
}

// Makes sure that the selNode select color is the same one as the selected option for esthetic purpose
// selNode can be a Select node id or a reference to it (if selNode doesn't exist, it's not a Select or doesn't have a selected option, the function does nothing)
function selUpdateColor(selNode) {
	var iNode = $ref(selNode);
	if ((!iNode) || (iNode.nodeName.toUpperCase() != "SELECT")) return;
	if (iNode.selectedIndex != -1) iNode.style.color = iNode.options[iNode.selectedIndex].style.color;
}

// Shows/hides an update notice to the user (according to the boolean parameter scriptShowMessage)
// The scriptNewVersion parameters is used to display the new version number/date in Date.prototype.getTime() format
function scriptShowUpdateMessage(scriptShowMessage, scriptNewVersion) {

	// Gets the notice box and the script new version date in UTC format
	var messageDiv = $("gsscriptVersionMessage");
	var scriptNewVersionDate = (new Date(scriptNewVersion)).toUTCString();

	// Shows/creates/hides the update notice
	if (scriptShowMessage == false) {
		// Hides the notice if it exists
		if (messageDiv) messageDiv.style.display = "none";
	}
	else {

		// The notice shouldn't be shown/created if the user has chosen to hide it for this session
		if (sSt.gsscriptVersionNoticeHide) return;

		if (messageDiv) {
			// Shows the notice
			messageDiv.style.display = "";
		}
		else {

			// Creates the notice
			messageDiv = createNode("div", {id: "gsscriptVersionMessage", title: "A new YousableTubeFix version is available"});
			messageDiv.innerHTML = "A new version of YousableTubeFix (" + scriptNewVersionDate + ") is available<br><br>" +
				"<a href='" + scriptFileURL + "' title='Install the script update'>Install</a>" +
				"<a href='" + scriptHomepageURL + "' target='_blank' title='Go to YousableTubeFix homepage'>Go to web page</a>" +
				"<a id='gsscriptVersionMessageHide' href='javascript:void(null)' title='Hide the notice for this session'>Hide</a>";
			document.body.appendChild(messageDiv);

			// Adds an event listener to the hide notice link
			$("gsscriptVersionMessageHide").addEventListener("click", function(evt) {
				sSt.gsscriptVersionNoticeHide = "1"; // Sets a sessionStorage variable to prevent the notice to be shown for this session
				scriptShowUpdateMessage(false, null);
			}, false);

		}
	}
}

// Checks if there is a new script version according to the version information in the script homepage
// The version information is in a line in the full description of the script: "<p>#[V:00000000]#</p>" (00000000 is the version number)
// If the request is successful and there is a new version available, a message to the user is displayed
function scriptCheckVersion() {
	GM_xmlhttpRequest({
		method: "GET",
		url: scriptHomepageURL,
		onload: function(evt) {
			if ((evt.readyState == 4) && (evt.status == 200)) {

				// Gets the remote version from the response and makes sure it is a number
				var responseMatch = evt.responseText.match(/<p>#\[V:(\d+)]#<\/p>/);
				var remoteVersion = (responseMatch === null) ? NaN : parseInt(responseMatch[1], 10);
				if (isNaN(remoteVersion)) return;

				// Saves the more recent version according to the server and shows the update notice if the server version is newer
				GM_setValue("scriptLastRemoteVersion", remoteVersion.toString());
				if (remoteVersion > scriptVersion) scriptShowUpdateMessage(true, remoteVersion);

			}
		}
	});
}

// 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";
};

// Shortcut to sessionStorage (saved values will be discarded at the end of the browser session)
var sSt = unsafeWindow.sessionStorage;

// Array of the known video formats and its details (index number [idx], name [label/shortlabel], flash variables [vq/fmt_map], type [MIMEString], quality order [QI, higher is better] and if its availability is checked by default [defaultChosen])
// Other properties are added to the array and the video format objects in the user configuration section
var videoFormatsArray = [{idx: "", label: "FLV Low Quality", shortlabel: "FLV", vq: 1, fmt_map: "", MIMEString: "video/x-flv", QI: 1, defaultChosen: true},
												 {idx: "6", label: "FLV High Quality", shortlabel: "FLV HQ", vq: 2, fmt_map: "6/720000/7/0/0", MIMEString: "video/x-flv", QI: 3, defaultChosen: true},
												 {idx: "18", label: "MPEG-4 H.264", shortlabel: "MP4", vq: 2, fmt_map: "18/512000/9/0/115", MIMEString: "video/mp4", QI: 2, defaultChosen: true},
												 {idx: "22", label: "MPEG-4 H.264 HQ", shortlabel: "MP4 HQ", vq: 2, fmt_map: "22/2000000/9/0/115", MIMEString: "video/mp4", QI: 4, defaultChosen: true}];

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

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

// Initiates an array with CSS styles for the script
[

	// 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)
	"a.gsanchors, .gsanchors a {text-decoration: none}",
	"a.gsanchors:hover, .gsanchors a:hover {text-decoration: underline}",

	// Adds classes and a style for the easy-to-use download links (and its parent div)
	"#gsdownloadLinksDiv {border-top: 1px solid #CCCCCC; margin: 0px 5px; padding: 5px; " +
		"color: #666666; font-weight: bold; text-align: center}",
	".gsdownloadLinks {padding: 0px 4px}",
	".gsdownloadLinks:not(:last-child) {border-right: thin dotted black}", // Adds a "separator" on the links' right side, except for the last link
	".gsdownloadLinkDisabled {color: gray; cursor: default}", // Disabled links (format not available)
	".gsdownloadLinkNotChecked {color: teal}", // Not checked links

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

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

	// Adds styles and classes for the configuration layers and its contents
	"#gsmaskLayer {background-color: black; opacity: 0.5; z-index: 100; " +
		"position: fixed; left: 0px; top: 0px; width: 100%; height: 100%}",
	"#gsdialogLayer {background-color: #EEEEEE; overflow: auto; padding: 5px; z-index: 101; " +
		"outline: black solid thin; position: fixed; left: 30%; top: 7.5%; width: 40%; height: 85%}",
	"#gsdialogLayer > * {margin: 20px 0px}",
	"#gsdialogLayer li {margin: 15px 0px 7px; font-style: italic}",
	"#gsdialogLayer input, #gsdialogLayer select {vertical-align: middle}",
	"#gsconfTitle {cursor: default; font-size: 150%; font-weight: bold; text-align: center}",
	"#gsconfButDiv {text-align: center}",
	"#gsconfButDiv input {margin: 5px}",
	"#gsdialogLayer ul {list-style-type: disc; padding-left: 40px}", // Reverts the changes to the default UA values from YouTube CSS ones

	// Adds styles for the script "new version" message and its anchors
	"#gsscriptVersionMessage {background-color: #C00040; color: white; outline: black solid thin; overflow: auto; " +
		"padding: 5px; position: fixed; z-index: 99; top: 0px; right: 0px; width: 250px; height: 70px; text-align: center",
	"#gsscriptVersionMessage a {margin: 0px 5px}"

// Adds the styles from the style array to the page, making them important
].forEach(function(s) {GM_addStyle(s.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)
	- removeQualitySettings --> Remove the quality settings link (dropped, integrated in the player by YouTube)
*/

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

// Optional elements 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 removeVideoResponses = GM_getValue("removeVideoResponses", false);
var removeCharCounter = GM_getValue("removeCharCounter", false);
var removeContentFrom = GM_getValue("removeContentFrom", false);
var removeMoreUserVideos = GM_getValue("removeMoreUserVideos", false);
var removePlaylist = GM_getValue("removePlaylist", false);
var removeRelatedVideos = GM_getValue("removeRelatedVideos", false);
var removeHeader = GM_getValue("removeHeader", false);
var removeFooterCopyright = GM_getValue("removeFooterCopyright", false);
var removeRacyNotice = GM_getValue("removeRacyNotice", false);
var removeDefaultLanguageBox = GM_getValue("removeDefaultLanguageBox", false);
var removeAnnotations = GM_getValue("removeAnnotations", false);
var removeCaptions = GM_getValue("removeCaptions", 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);

// Prevents only autoplay using the YouTube API
var preventOnlyAutoplay = GM_getValue("preventOnlyAutoplay", 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");
var defaultVideoFormatInt = parseInt(defaultVideoFormat, 10); // An integer representation of the default video format (it can be NaN)

// Changes the Flash Player Quality attribute
var flashQuality = GM_getValue("flashQuality", "high");

// Adds easy-to-use download video links
var addEasyDownloadLinks = GM_getValue("addEasyDownloadLinks", true);

// Automatically bypass the age verification page for logged in users
var bypassAgeVerification = GM_getValue("bypassAgeVerification", false);

// Moves and resizes the video to its final position and configured size by default
var moveVideo = GM_getValue("moveVideo", true);

// Scrolls to the video by default on page entry
var scrollToVideo = GM_getValue("scrollToVideo", true);

// Gets the array of the index numbers of the video formats selected by the user, stored in JSON format (e.g. '["18", ""]')
// Those formats (if they are supported) will have its availability checked and will be selectable if the user has chosen to autoselect the video format (automatic quality options)
// The array is sorted according to the QI the user wants each format to have (from highest to lowest)
var userFormats = eval(GM_getValue("userFormats", "null"));
if (!(userFormats instanceof Array)) {
	userFormats = null; // If userFormats isn't an array, nullify it
}
else {
	userFormats.reverse(); // The algorithm to parse the array is simpler if the video formats are sorted from lowest to highest QI
}

// Extends the video formats array with properties regarding the user configuration (userFormats array)
videoFormatsArray.userFormatsLength = 0; // Counter of the video formats selected by the user which are supported
videoFormatsArray.forEach(function(vf) { // Adds properties to each video format object

	// isAvailable will be updated by selVideoFormatAvailability or its initialization code (1: available, 0: not available, -1: unknown [request error], -2: not checked)
	vf.isAvailable = null;

	// userChosen indicates if the video format has been selected by the user, userQI holds the QI given to it by the user (higher is better)
	if (userFormats === null) {

		// userFormats is unavailable. The properties get their default values
		vf.userQI = vf.QI;
		vf.userChosen = vf.defaultChosen;

	}
	else {

		// userFormats is available. Tries to find the video format in userFormats
		if (userFormats.indexOf(vf.idx) != -1) {

			// The video format has been selected
			vf.userQI = videoFormatsArray.userFormatsLength + 1; // 1 for the first one, 2 for the second one, etc...
			vf.userChosen = true;

		}
		else {

			// The video format hasn't been selected
			vf.userQI = null;
			vf.userChosen = false;

		}

	}

	// Updates the counter if the video format has been selected
	if (vf.userChosen) videoFormatsArray.userFormatsLength++;

});

// Current script version (release date), last update check and last remote version seen
var scriptVersion = 1238184322953; // 27 Mar 2009

var scriptLastCheck = parseInt(GM_getValue("scriptLastCheck", "0"), 10);
if (isNaN(scriptLastCheck)) scriptLastCheck = 0;

var scriptLastRemoteVersion = parseInt(GM_getValue("scriptLastRemoteVersion", scriptVersion.toString()), 10);
if (isNaN(scriptLastRemoteVersion)) scriptLastRemoteVersion = scriptVersion;

// URLs related to the script
var scriptFileURL = "http://userscripts.org/scripts/source/13333.user.js";
var scriptHomepageURL = "http://userscripts.org/scripts/show/13333";

// 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 + "'><br>" +
			"<input type='checkbox' id='gsconfmoveVideo'" + (moveVideo ? " checked='checked'" : "") + ">Move and resize the video by default<br>" +
			"<input type='checkbox' id='gsconfscrollToVideo'" + (scrollToVideo ? " checked='checked'" : "") + ">Scroll to the video by default" +
			"<li>Remove (use Ctrl+click / Shift+click to select multiple options):</li>" +
			"<select id='gsconfremoveSel' multiple='multiple' size='5'>" +
			"<option id='gsconfremoveBrand'" + (removeBrand ? " selected='selected'" : "") + ">- Channel brand</option>" +
			"<option id='gsconfremoveAlsoWatching'" + (removeAlsoWatching ? " selected='selected'" : "") + ">- \"Also Watching Now\" section</option>" +
			"<option id='gsconfremoveEmbed'" + (removeEmbed ? " selected='selected'" : "") + ">- Embed section</option>" +
			"<option id='gsconfremoveURL'" + (removeURL ? " selected='selected'" : "") + ">- URL section</option>" +
			"<option id='gsconfremoveRatings'" + (removeRatings ? " selected='selected'" : "") + ">- Ratings section</option>" +
			"<option id='gsconfremoveActions'" + (removeActions ? " selected='selected'" : "") + ">- Actions section</option>" +
			"<option id='gsconfremoveStats'" + (removeStats ? " selected='selected'" : "") + ">- Stats section</option>" +
			"<option id='gsconfremoveVideoResponses'" + (removeVideoResponses ? " selected='selected'" : "") + ">- Video responses section</option>" +
			"<option id='gsconfremoveContentFrom'" + (removeContentFrom ? " selected='selected'" : "") + ">- Contains content from... section</option>" +
			"<option id='gsconfremoveMoreUserVideos'" + (removeMoreUserVideos ? " selected='selected'" : "") + ">- More videos from this user section</option>" +
			"<option id='gsconfremovePlaylist'" + (removePlaylist ? " selected='selected'" : "") + ">- Playlist section</option>" +
			"<option id='gsconfremoveRelatedVideos'" + (removeRelatedVideos ? " selected='selected'" : "") + ">- Related videos section</option>" +
			"<option id='gsconfremoveHeader'" + (removeHeader ? " selected='selected'" : "") + ">- Header section</option>" +
			"<option id='gsconfremoveFooterCopyright'" + (removeFooterCopyright ? " selected='selected'" : "") + ">- Footer and copyright sections</option>" +
			"<option id='gsconfremoveRacyNotice'" + (removeRacyNotice ? " selected='selected'" : "") + ">- Racy video notice</option>" +
			"<option id='gsconfremoveDefaultLanguageBox'" + (removeDefaultLanguageBox ? " selected='selected'" : "") + ">- Default language box</option>" +
			"<option id='gsconfremoveCharCounter'" + (removeCharCounter ? " selected='selected'" : "") + ">- Character counter</option>" +
			"<option id='gsconfremoveAnnotations'" + (removeAnnotations ? " selected='selected'" : "") + ">- Video annotations</option>" +
			"<option id='gsconfremoveCaptions'" + (removeCaptions ? " selected='selected'" : "") + ">- Video captions</option>" +
			"</select>" +
			"<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>Video autoplay and autodownload:</li>" +
			"<input type='radio' id='gsconfAutoplayDefault' name='gsconfAutoplayChoice'" + ((!videoToIcon && !preventOnlyAutoplay) ? " checked='checked'" : "") + ">Don't prevent autoplay and autodownload<br>" +
			"<input type='radio' id='gsconfpreventOnlyAutoplay' name='gsconfAutoplayChoice'" + ((!videoToIcon && preventOnlyAutoplay) ? " checked='checked'" : "") + ">Prevent only autoplay<br>" +
			"<input type='radio' id='gsconfvideoToIcon' name='gsconfAutoplayChoice'" + (videoToIcon ? " checked='checked'" : "") + ">Prevent both autoplay and autodownload" +
			"<li><input type='checkbox' id='gsconfaddEasyDownloadLinks'" + (addEasyDownloadLinks ? " checked='checked'" : "") + ">Add easy-to-use download links</li>" +
			"<li><input type='checkbox' id='gsconfbypassAgeVerification'" + (bypassAgeVerification ? " checked='checked'" : "") + ">Automatically bypass age verification (logged in users)</li>" +
			"<li>" +
			"Select the default video format: &nbsp;" +
			"<select id='gsconfdefaultVideoFormatSel' size='1'>" +
			"<optgroup label='Automatic Quality options'>" +
			"<option id='gsconfdefaultVideoFormatOptDefault' value='0'" + ((defaultVideoFormat === "0") ? " selected='selected'" : "") + ">YouTube Default</option>" +
			"<option id='gsconfdefaultVideoFormatOptAutoHigh' value='-1'" + ((defaultVideoFormat === "-1") ? " selected='selected'" : "") + ">Best Quality available</option>" +
			"<option id='gsconfdefaultVideoFormatOptAutoLow' value='-2'" + ((defaultVideoFormat === "-2") ? " selected='selected'" : "") + ">Fastest Quality available</option>" +
			"</optgroup>" +
			"<optgroup label='Fixed Quality options'>" +
			(function() { // Returns a string with the HTML code of as many option nodes as known video formats
				return videoFormatsArray.map(function(vf) {
					return "<option id='gsconfdefaultVideoFormatOpt" + vf.idx + "' value='" + vf.idx + "'" + ((defaultVideoFormat === vf.idx) ? " selected='selected'" : "") + ">" + vf.label + "</option>";
				}).join("");
			})() +
			"</optgroup>" +
			"</select>" +
			"</li>" +
			"<li>" +
			"Select the Flash Player Quality: &nbsp;" +
			"<select id='gsconfflashQualitySel' size='1'>" +
			"<option id='gsconfflashQualityOptBest' value='best'" + ((flashQuality === "best") ? " selected='selected'" : "") + ">Best Quality</option>" +
			"<option id='gsconfflashQualityOptHigh' value='high'" + ((flashQuality === "high") ? " selected='selected'" : "") + ">High Quality (YouTube default)</option>" +
			"<option id='gsconfflashQualityOptMedium' value='medium'" + ((flashQuality === "medium") ? " selected='selected'" : "") + ">Medium Quality</option>" +
			"<option id='gsconfflashQualityOptLow' value='low'" + ((flashQuality === "low") ? " selected='selected'" : "") + ">Low Quality (Low CPU use)</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", chkDependantLogic, 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);
		allInputs.forEach(function(i) {i.disabled = !newState;});
	}

	// Implements a master/slave logic to two following sibling checkboxes. The first is the master one and the following is the slave one
	// The slave checkbox is disabled and unchecked when the master one is unchecked
	// It is called by the master checkbox event listener
	// It is a nested function
	function chkDependantLogic(evt) {
		var chkMasterState = evt.target.checked;
		var chkSlave = $x1("following-sibling::input[@type='checkbox']", evt.target);
		if (!chkSlave) return;
		if (chkMasterState === false) chkSlave.checked = false;
		chkSlave.disabled = !chkMasterState;
	}

	// 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("moveVideo", $("gsconfmoveVideo").checked);
		GM_setValue("scrollToVideo", $("gsconfscrollToVideo").checked);
		GM_setValue("removeBrand", $("gsconfremoveBrand").selected);
		GM_setValue("removeAlsoWatching", $("gsconfremoveAlsoWatching").selected);
		GM_setValue("removeEmbed", $("gsconfremoveEmbed").selected);
		GM_setValue("removeURL", $("gsconfremoveURL").selected);
		GM_setValue("removeRatings", $("gsconfremoveRatings").selected);
		GM_setValue("removeActions", $("gsconfremoveActions").selected);
		GM_setValue("removeStats", $("gsconfremoveStats").selected);
		GM_setValue("removeVideoResponses", $("gsconfremoveVideoResponses").selected);
		GM_setValue("removeContentFrom", $("gsconfremoveContentFrom").selected);
		GM_setValue("removeMoreUserVideos", $("gsconfremoveMoreUserVideos").selected);
		GM_setValue("removePlaylist", $("gsconfremovePlaylist").selected);
		GM_setValue("removeRelatedVideos", $("gsconfremoveRelatedVideos").selected);
		GM_setValue("removeHeader", $("gsconfremoveHeader").selected);
		GM_setValue("removeFooterCopyright", $("gsconfremoveFooterCopyright").selected);
		GM_setValue("removeRacyNotice", $("gsconfremoveRacyNotice").selected);
		GM_setValue("removeDefaultLanguageBox", $("gsconfremoveDefaultLanguageBox").selected);
		GM_setValue("removeCharCounter", $("gsconfremoveCharCounter").selected);
		GM_setValue("removeAnnotations", $("gsconfremoveAnnotations").selected);
		GM_setValue("removeCaptions", $("gsconfremoveCaptions").selected);
		GM_setValue("expandInfo", $("gsconfexpandInfo").checked);
		GM_setValue("hideCollapse", ($("gsconfexpandInfo").checked ? $("gsconfhideCollapse").checked : false));
		GM_setValue("biggerComments", $("gsconfcommentsBigger").checked);
		GM_setValue("removeComments", $("gsconfcommentsNone").checked);
		GM_setValue("preventOnlyAutoplay", $("gsconfpreventOnlyAutoplay").checked);
		GM_setValue("videoToIcon", $("gsconfvideoToIcon").checked);
		GM_setValue("addEasyDownloadLinks", $("gsconfaddEasyDownloadLinks").checked);
		GM_setValue("bypassAgeVerification", $("gsconfbypassAgeVerification").checked);
		GM_setValue("defaultVideoFormat", $("gsconfdefaultVideoFormatSel").value);
		GM_setValue("flashQuality", $("gsconfflashQualitySel").value);

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

	}

}

// Registers the 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)]

// Gets the YouTube pathname to know which YouTube page is active
var ytPath = window.location.pathname;

// Actions for all YouTube pages

// Removes the Chrome promo ad
delNode("chrome-promo");
delNode($x1("//div[@class='homepage-chrome-promo-content']")); // Chrome ad in the homepage

// Removes the default language dialog box
if (removeDefaultLanguageBox) delNode("default-language-box");

// Actions for non-video pages
if (/^\/results(?:\.php)?/i.test(ytPath)) {

	// Search results page

	// Removes the ads
	delNode("search-pva");

	return;

}
else if (/^\/verify_age(?:\.php)?/i.test(ytPath)) {

	// Age verification page

	// Bypass the age verification page for logged in users by automatically clicking in the "Confirm Birth Date" button
	if (bypassAgeVerification) {
		var inputConfirmAge = $x1("//form//input[@type='submit'][@name='action_confirm']");
		if (inputConfirmAge) fireClickEvent(inputConfirmAge);
	}

	return;

}
else if (!(/^\/watch(?:\.php)?/i.test(ytPath))) {
	// An unknown non-video page
	return;
}

// This is a YouTube video page

// Gets the video player (and its unwrapped version), its parent div and its parameters (Id, Tracking Id y Video Format)
var player = $("movie_player");
if (!player) return;
var uwPlayer = player.wrappedJSObject; // unsafeWindow context
var playerDiv = $("watch-player-div");
if (!playerDiv) 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 = "";
			break;
	}
}

// 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 = getVideoFormatObj(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(getParent($("watch-promoted-videos-container")));

// 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 Stats section
if (removeStats) delNode("watch-stats-data-wrapper");

// Removes video responses section
if (removeVideoResponses) delNode(getParent("watch-video-responses-children"));

// Removes comments section
if (removeComments) delNode("watch-comment-panel");

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

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

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

// Removes content from... section
if (removeContentFrom) delNode("watch-content-badges");

// Removes "more videos from this user" section
if (removeMoreUserVideos) delNode("watch-channel-videos-panel");

// Removes playlist section
if (removePlaylist) delNode("playlist-panel");

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

// Removes header section
if (removeHeader) delNode("masthead");

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

// Removes the racy video notice
if (removeRacyNotice) {
	delNode("watch-highlight-racy-box");
}

// Sets the necessary event listeners to remove the character counters when YouTube inserts them (changing the div innerHTML)
// The event listeners 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.addEventListener("DOMNodeInserted", watchCharCounterDiv, false);
	if (postCommentDiv2) postCommentDiv2.addEventListener("DOMNodeInserted", watchCharCounterDiv, false);
}

// Function to dinamically remove the character counters' code from the innerHTML set by YouTube functions
// It is called by postCommentDiv[1/2]s' event listeners
function watchCharCounterDiv(evt) {

	// Only the comment_formmain_comment[1/2] elements will me handled (the forms with the character counters)
	var watchNodeId = evt.target.id;
	if ((!watchNodeId) || (!(/^comment_formmain_comment2?$/.test(watchNodeId)))) return;

	// Gets the textarea and removes the references to the update function from its event listeners
	var watchCharTA = $x1(".//textarea[@name='comment']", evt.target);
	if (watchCharTA) {
		["oninput", "onpaste", "onkeyup"].forEach(function(a) {

			if (!watchCharTA.hasAttribute(a)) return;

			var watchCharTAAttr = watchCharTA.getAttribute(a);
			watchCharTAAttr = watchCharTAAttr.replace(/updateCharCount\([^)]*\);?/gi, ""); // updateCharCount is the YouTube's update function
			if (watchCharTAAttr !== "") {
				watchCharTA.setAttribute(a, watchCharTAAttr); // Updates the event listener
			}
			else {
				watchCharTA.removeAttribute(a); // Removes event listeners that would be empty
			}
		});
	}

	// Removes the character counters' labels
	var watchCharSpan = $x1(".//span[starts-with(@id, 'maxCharLabelmain_comment')]", evt.target);
	var watchCharInput = $x1(".//input[starts-with(@id, 'charCountmain_comment')]", evt.target);
	delNode(watchCharSpan);
	delNode(watchCharInput);

}

// Removes the video annotations (depends on the Main Reload)
if (removeAnnotations) {
	deleteFlashVar("iv_storage_server", false);
	deleteFlashVar("iv_module", false);
}

// Removes the video captions (depends on the Main Reload)
if (removeCaptions) {
	deleteFlashVar("ttsurl", false);
	deleteFlashVar("subtitle_module", false);
}

// Adds easy-to-use download video links to the right column
if (addEasyDownloadLinks) {
	var videoDetailsDiv = $("watch-video-details");
	if (videoDetailsDiv) {

		// Creates the easy-to-use download links and a container div, and then appends the links to the div
		var downloadLinksDiv = createNode("div", {id: "gsdownloadLinksDiv"}, {textContent: "Download as: "});

		videoFormatsArray.forEach(function(vf) {
			var downloadLinkFormat = createNode("a", {id: "gsdownloadLinkFormat" + vf.idx, class: "gsdownloadLinks",
																								href: getDownloadVideoURL(videoId, tId, vf.idx), title: vf.label, type: vf.MIMEString,
																								gsvideoFormat: vf.idx}, {textContent: vf.shortlabel});
			downloadLinksDiv.appendChild(downloadLinkFormat);
		});

		// Appends the div to the right column (YouTube layout)
		videoDetailsDiv.appendChild(downloadLinksDiv);

	}
}

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

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

// Saves original x position and witdth of the playerDiv for resizeVideo's use
// As the playerDiv hasn't been moved to its final localization yet, vidTitle's data will be used instead (their saved values should be identical)
var oPlayerDivData = {xPos: findXY(vidTitle).xPos, width: vidTitle.clientWidth};

// Stops the video autoplay and autodownload by substituing the player with an icon or using the YouTube API
if (videoToIcon) {
	var subsIcon; // subsIcon is declared here because it's used by both iconizeVideo and restoreVideo
	iconizeVideo();
}
else {
	if (preventOnlyAutoplay) {
		// Initializes the needed variables and sets the player to call gsPlayerReady when the player API module is ready
		var autoplay1Rep = true, autoplayFailedRegs = 0;
		setFlashVar("jsapicallback", "gsPlayerReady", false); // Depends on the Main Reload
	}
}

// Function to initialize the YouTube API code to stop autoplay
// It registers our state change event listener (gsStateChangeListener) and then executes the default YouTube initialization function
// It is called by the player when its API module is ready (if the corresponding jsapicallback variable is set)
// Although the function is accesible from unsafeWindow, it's executed in the script context
unsafeWindow.gsPlayerReady = function(playerId) {

	// Initializes the "first reproduction" flag
	autoplay1Rep = true;

	// Tries to register the state change event listener
	// This sometimes fails for unknown reasons, so the error is cached and the player reloaded up to 4 times to try to do it again
	try {
		uwPlayer.addEventListener("onStateChange", "gsStateChangeListener");
	}
	catch(err) {
		if (autoplayFailedRegs < 4) {
			autoplayFailedRegs++; // Increments the counter by one
			reloadPlayer();
		}
		else {
			// Too many failed registration attempts. The API method will be disabled and the video will be substituted with an icon as a fallback mechanism
			iconizeVideo();
			deleteFlashVar("jsapicallback", true);
		}
		return;
	}

	// The registration was successful. Resets the "failed attempts" counter
	autoplayFailedRegs = 0;

	// If the player is unmuted, mutes it
	// As the pause video code can't pause the video till it's already playing, the sound is muted so the small delay is less noticeable
	if (uwPlayer.isMuted() === false) uwPlayer.mute();

	// The default YouTube initialization function is called if it exists
	if (unsafeWindow.onYouTubePlayerReady) unsafeWindow.onYouTubePlayerReady(playerId);
};

// Function to pause the video first reproduction
// It is called by the player when its state changes
// Although the function is accesible from unsafeWindow, it's executed in the script context
unsafeWindow.gsStateChangeListener = function(stateId) {
	if (autoplay1Rep && (stateId === 1)) {

		// The "first reproduction" flag is active and the player state is PLAYING (1)
		// Pauses the video
		uwPlayer.pauseVideo();

		if (uwPlayer.getPlayerState() === 2) {

			// The player state is now confirmed to have changed to PAUSED (2)
			// Deactivates the "first reproduction" flag
			autoplay1Rep = false;

			// Seeks to the beginning of the video and unmutes it (if it is muted)
			// This undoes the muting of the video by gsPlayerReady and corrects the small delay before the pause video code acted
			uwPlayer.seekTo(0, true);
			if (uwPlayer.isMuted() === true) uwPlayer.unMute();

		}

	}
};

// Function to substitute the video with an icon
function iconizeVideo() {
	subsIcon = createNode("img", {id: "gssubsIcon", alt: "Video",
																title: "Click here to play the video",
																src: "http://img.youtube.com/vi/" + videoId + "/default.jpg"});
	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);
}

// Changes the Flash Player Quality attribute (depends on the Main Reload)
player.setAttribute("quality", flashQuality);

// Moves the player div below the title div (the player is also reloaded [Main Reload])
// If the script shouldn't move the player div by default, the player is only manually reloaded (Main Reload)
if (moveVideo) {
	insertAfter(playerDiv, vidTitle);
}
else {
	reloadPlayer();
}

// Adds video resize links
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)'>2.25x</a> " +
	"- <a id='gsresizeLink6' href='javascript:void(null)'>2.5x</a> " +
	"- <a id='gsresizeLink7' href='javascript:void(null)'>fill</a> " +
	"- <a id='gsresizeLink8' href='javascript:void(null)'>max</a>";

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

// Creates and adds the CVF options to the CVF select
videoFormatsArray.forEach(function(vf) {
	var optVideoFormat = createNode("option", {id: "gsoptVideoFormat" + vf.idx, value: vf.idx}, {textContent: vf.label});
	selVideoFormat.add(optVideoFormat, 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 div
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 format by selecting the corresponding CVF option
if (defaultVideoFormatInt > 0) { // 0 and negative values have special meanings. If defaultVideoFormatInt is NaN, the comparison is false
	var optVideoFormatDefault = selGetOptionsFromValue(selVideoFormat, defaultVideoFormat, true);
	if ((optVideoFormatDefault !== null) && (optVideoFormatDefault.selected == false)) {
		// 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)
	}
}

// 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 = getVideoFormatObj(selNewValue);
	if (videoFormatDetails !== null) {
		setFlashVar("vq", videoFormatDetails.vq, false);
		setFlashVar("fmt_map", videoFormatDetails.fmt_map, true);
		videoFormat = selNewValue;
		MIMEString = videoFormatDetails.MIMEString;
	}

	// Updates the CVF select color
	selUpdateColor(selVideoFormat);

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

	// Updates the URL input field
	updateURLInputField();

}

// Resizes the video to default size and scrolls to it (it doesn't scroll if scrollVideo is false)
resizeVideo(videoSize, (!scrollToVideo));

// Function used to resize video and scroll to it
// If dontScroll is true, the page isn't scrolled
function resizeVideo(aSize, dontScroll) {

	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 or isn't positioned for resize, only scroll up/down to get the video title on top
	if ((isVideoIcon()) || (!isVideoPositioned())) {
		scrollToNodeYPos(vidTitle);
		return; // Exit function
	}

	switch(aSize) {

		case "fill":
			// Fills the width available in the parent div, preserving the aspect ratio
			newW = oPlayerDivData.width;
			newH = newW / oPlayerData.AR;
			break;

		case "max":

			// Fills the viewport dimensions completely, preserving the aspect ratio

			// Calculates the resize factor (factorR) as the smallest of the vertical and horizontal ratios
			// This is valid if the original video size is smaller than the viewport (at least in one dimension)
			var factorR = Math.min(vh / oPlayerData.height, vw / oPlayerData.width);

			// Multiplies the original dimensions by the factorR factor, rounded down (the rounding mustn't return a bigger number)
			// The aspect ratio is preserved
			newW = Math.floor(oPlayerData.width * factorR);
			newH = Math.floor(oPlayerData.height * factorR);
			break;

		default:
			// Multiplies the original dimensions by the aSize factor
			newW = oPlayerData.width * aSize;
			newH = oPlayerData.height * aSize;
			break;

	}

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

	// Centers the player, video title and resize links horizontally
	var posOffset = (((vw - newW) / 2) - oPlayerDivData.xPos).toCSS(); // Calculates the offset --> ((Container width - Content width) / 2) - Content Initial Position;
	[playerDiv, vidTitle, linkDiv].forEach(function(n) {
		n.style.position = "relative"; // Enables CSS relative positioning
		n.style.left = posOffset; // Sets the offset
	});

	// Scrolls up/down to get the video title (or the player div if aSize is "max") on top
	scrollToNodeYPos((aSize == "max") ? playerDiv : vidTitle);

	// Scrolls the page to get the passed node on top, but it doesn't do anything if dontScroll is true
	// It is a nested function
	function scrollToNodeYPos(targetNode) {
		if (dontScroll) return;
		var posToScroll = findXY(targetNode);
		if (posToScroll !== null) window.scrollTo(0, posToScroll.yPos);
	}

}

// 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 (!(/^gsresizeLink[0-8]$/.test(linkId))) return; // Only resize links from 0 to 8 will be handled

	// Tries to get a valid video size from the link text
	var linkSize = valVideoSize(evt.target.textContent);
	if (linkSize === null) return;

	// Resizes the video and scrolls to it. If the playerDiv isn't positioned for resize, it is moved (along with linkDiv)
	if (!isVideoPositioned()) {
		insertAfter(playerDiv, vidTitle);
		insertAfter(linkDiv, playerDiv);
	}
	resizeVideo(linkSize);

}

// It stores how many video formats are still unckecked for selVideoFormatAvailability's use (all of the user selected ones at this point)
var videoFormatsToCheck = videoFormatsArray.userFormatsLength;

// Sends requests to get information for each video format availability if it has been selected
// If not, it changes the appearence of the CVF option and its corresponding easy-to-use download link
videoFormatsArray.forEach(function(vf) {
	if (vf.userChosen) {
		checkVideoAvailability(videoId, tId, vf.idx, selVideoFormatAvailability);
	}
	else {
		var optAvailability = selGetOptionsFromValue(selVideoFormat, vf.idx, true);
		if (optAvailability !== null) {
			optAvailability.style.color = "teal";
			optAvailability.textContent += " (not checked)";
			vf.isAvailable = -2;
		}
		var downloadLinkAvailability = $x1("//div[@id='gsdownloadLinksDiv']/a[@gsvideoFormat='" + vf.idx + "']");
		if (downloadLinkAvailability) downloadLinkAvailability.className += " gsdownloadLinkNotChecked";
	}
});
selUpdateColor(selVideoFormat); // Updates the CVF select color

// 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) {

	// Gets the CVF option and video format object associated with vFormat
	var optAvailability = selGetOptionsFromValue(selVideoFormat, vFormat, true);
	var videoFormatAvailability = getVideoFormatObj(vFormat);

	// Changes the CVF option appearance and updates the availability member of the video format object
	if ((optAvailability !== null) && (videoFormatAvailability !== null)) {
		switch(vAvailabilityRet) {
			case true:
				optAvailability.style.color = "green";
				optAvailability.textContent += " (available)";
				videoFormatAvailability.isAvailable = 1;
				break;
			case false:
				optAvailability.style.color = "red";
				optAvailability.textContent += " (unavailable)";
				videoFormatAvailability.isAvailable = 0;
				break;
			case null:
				optAvailability.style.color = "purple";
				optAvailability.textContent += " (request error)";
				videoFormatAvailability.isAvailable = -1;
				break;
		}

		// Updates the CVF select color
		selUpdateColor(selVideoFormat);

		// If the video format is unavailable, disables the corresponding easy-to-use download link
		if (vAvailabilityRet === false) {
			var downloadLinkAvailability = $x1("//div[@id='gsdownloadLinksDiv']/a[@gsvideoFormat='" + vFormat + "']");
			if (downloadLinkAvailability) {
				downloadLinkAvailability.className += " gsdownloadLinkDisabled";
				downloadLinkAvailability.href = "javascript:void(null)";
				downloadLinkAvailability.title = "This video format is unavailable";
			}
		}

		// Updates the "video formats still unckecked" counter
		videoFormatsToCheck--;
		if (videoFormatsToCheck <= 0) {

			// All the video formats have been checked. If the user has chosen to autoselect a video format according to its availability, does it now
			if ((defaultVideoFormatInt === -1) || (defaultVideoFormatInt === -2)) {

				// Gets the preferred available option according to the user preferences
				var optQualityAuto = selGetAvailableVideoFormatOption((defaultVideoFormatInt === -2) ? true : false);

				if ((optQualityAuto !== null) && (optQualityAuto.selected == false)) {
					// The returned options isn't selected. Selects it
					optQualityAuto.selected = true;
					fireChangeEvent(selVideoFormat); // Manual fire of the change event (script changes don't fire it)
				}

			}

		}
	}
}

// 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-comment-filter']//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/text()", null, xhrComments.responseXML);
			var xmlReturnCodeNode = $x1("//return_code/text()", null, xhrComments.responseXML);
			var xmlReturnCode = ((xmlReturnCodeNode) && (xmlReturnCodeNode.data)) ? xmlReturnCodeNode.data : null;
			if ((xmlData) && (xmlData.data) && (xmlReturnCode === "0")) {
				recentComments.innerHTML = xmlData.data;
			}
			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 + "&savethreshold=yes&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();
}

// Checks for script updates
if (Date.now() - scriptLastCheck >= 86400000) { // 1 day
	// At least a day has passed since the last check. Sends a request to check for a new script version
	GM_setValue("scriptLastCheck", Date.now().toString());
	scriptCheckVersion();
}
else {
	// If a new version was previously detected the notice will be shown to the user
	// This is to prevent that the notice will only be shown once a day (when an update check is scheduled)
	if (scriptLastRemoteVersion > scriptVersion) {
		scriptShowUpdateMessage(true, scriptLastRemoteVersion);
	}
}