By Mindeye
Has no other scripts.
// ==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: " +
"<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();
}