Greasy Fork is available in English.

YouTube Downloader (update)

Download videos and audio from YouTube with support DASH formats

// ==UserScript==
// @name         YouTube Downloader (update)
// @namespace    https://greasyfork.org/es/users/77045-foxrom
// @version      0.10.1
// @description  Download videos and audio from YouTube with support DASH formats
// @author       foxrom
// @icon         http://youtube.com/favicon.ico
// @include      http://www.youtube.com*
// @include      https://www.youtube.com*
// @license      Creative Commons; http://creativecommons.org/licenses/by/4.0/
// @require      http://code.jquery.com/jquery-1.11.0.min.js
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// ==/UserScript==

// Down arrow selector icon is made by Freepik at http://www.freepik.com under Creative Commons 3.0

// src/prototypes.js
// =================================================
// These are functions that are references throughout
// the script that perform useful tasks

var downloadCancelBtn_width = 30;

function GM_download2_Init(){
	var $container = $("#downloadBtnCont");

	var $downloadCancelCont = $("#downloadCancelCont");
	if ($downloadCancelCont.length === 0) {
		$downloadCancelCont = $("<span>", {
			id:"downloadCancelCont",
		});
		$container.append($downloadCancelCont);
	}else{
		$downloadCancelCont.find("button").click();
		$downloadCancelCont.html("");
	}
}

function GM_download2 (url, name) {
	if (url == null) return;

	var $downloadCancelBtnLabel_img = $("<img>", {
	class:"midalign downloadCancelBtnLabel_img",
	src:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACJklEQVQ4T2NkoBAwUqifAcWA5TJSyWwcrAbBdx7mYjN4PgMDh5iW2s7v334cCHnwqB6kBm7AclnxJCN9nbkc7OwMp09f6gh59KQS2ZBJQkJ8GtJix3V11LUuXbp50/3qDQ0UA1bLSrcZ6apXcnNyMHz99p3hzKUbReFPn/eDFE3n4RFTVZA+raOuLHf91sPndx8+1kn59OkdigEgzkoZiXJ9VaUOfm4uho9fvjGcv3kn7sf3X4eVZCRPqylIi9x58vTDrVsPtZO/fXsGcx1GIC4XF03SUZSbI8zLzfj685f/n798+6YkKcb98OXb7zcePNZP/PTpNrLXsMbCQmFhfz1pibVi/DzMIMVP3334ffXVS7PE1x8uoAcuVgMWifG5awuJbRHn42UBG/Dp489rj1+aJn79epmgAUtFBKO1hIQWiXCwM737+ev/59+/f8jzcHM++Pzl6413H3VTP368j9MLy4SECnQFePv5WVkZPv/5w3Dh/af073/+7FPh5jytxMcrcO/L57e3P3zRTv369SVGIK4UEqrX5eZo4GZmZvj+7x/Dxa/fqsPfvm8DKZzLxSUlx8t9Ro2TQ/LGt+9PHvz+q5P+/v1HlGhcxseXpcvBPpWNkZHhws8fk8I/fMpHduocPj4hCWamk9rs7CpXf32/6vPukw5GOljKx5fDzvTXMPjD1xRGBob/6AG2SFycW/Dnj/3fGBn2h7//WI5hADkZi+LcCAA1QdYRLzAxRQAAAABJRU5ErkJggg=="
});

	var $downloadCancelBtnLabel_img2 = $("<img>", {
	class:"midalign downloadCancelBtnLabel_img2",
	src:"data:image/gif;base64,R0lGODlhEAAQAPYAAF6IsP///2CJsaK60ai+1GiPtIChwHWZu5q0zf///6S80oGiwd7m7srX5GmQtZWwy6C50GWNs6zB1uzw9Y2rx5axy5y2zmSMs2GKsZSvynyevtPe6fb4+sHR4HKWuWuStoemxOPq8dvk7dHd6HibvIKiwdji68zZ5tbh6tDc53qdvn6gv8XU4oyqxmqRtrfJ25GuyYqoxYinxMLS4cTT4cDQ36m/1HCVua7D11+IsJeyzO7y9uLp8MfW4/v8/MvY5X2fv9rj7MbV4u3x9maOtHeavJu1zm+VuG2Tt9Xg6pKvyeft8unv9JCtyLbJ2s/b593l7rvM3XmcvYWkw+bs8q/E19Tf6cnW5Pf5+vj6+4+sx26Ut87a5mOLsoupxr3O3rzN3nOXupmzzPL1+Oju8wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAHaIAAgoMgIiYlg4kACxIaACEJCSiKggYMCRselwkpghGJBJEcFgsjJyoAGBmfggcNEx0flBiKDhQFlIoCCA+5lAORFb4AJIihCRbDxQAFChAXw9HSqb60iREZ1omqrIPdJCTe0SWI09GBACH5BAkKAAAALAAAAAAQABAAAAdrgACCgwc0NTeDiYozCQkvOTo9GTmDKy8aFy+NOBA7CTswgywJDTIuEjYFIY0JNYMtKTEFiRU8Pjwygy4ws4owPyCKwsMAJSTEgiQlgsbIAMrO0dKDGMTViREZ14kYGRGK38nHguHEJcvTyIEAIfkECQoAAAAsAAAAABAAEAAAB2iAAIKDAggPg4iJAAMJCRUAJRIqiRGCBI0WQEEJJkWDERkYAAUKEBc4Po1GiKKJHkJDNEeKig4URLS0ICImJZAkuQAhjSi/wQyNKcGDCyMnk8u5rYrTgqDVghgZlYjcACTA1sslvtHRgQAh+QQJCgAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCQARAtOUoQRGRiFD0kJUYWZhUhKT1OLhR8wBaaFBzQ1NwAlkIszCQkvsbOHL7Y4q4IuEjaqq0ZQD5+GEEsJTDCMmIUhtgk1lo6QFUwJVDKLiYJNUd6/hoEAIfkECQoAAAAsAAAAABAAEAAAB2iAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4uen4ICCA+IkIsDCQkVACWmhwSpFqAABQoQF6ALTkWFnYMrVlhWvIKTlSAiJiVVPqlGhJkhqShHV1lCW4cMqSkAR1ofiwsjJyqGgQAh+QQJCgAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCSMhREZGIYYGY2ElYebi56fhyWQniSKAKKfpaCLFlAPhl0gXYNGEwkhGYREUywag1wJwSkHNDU3D0kJYIMZQwk8MjPBLx9eXwuETVEyAC/BOKsuEjYFhoEAIfkECQoAAAAsAAAAABAAEAAAB2eAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4ueICImip6CIQkJKJ4kigynKaqKCyMnKqSEK05StgAGQRxPYZaENqccFgIID4KXmQBhXFkzDgOnFYLNgltaSAAEpxa7BQoQF4aBACH5BAkKAAAALAAAAAAQABAAAAdogACCg4SFggJiPUqCJSWGgkZjCUwZACQkgxGEXAmdT4UYGZqCGWQ+IjKGGIUwPzGPhAc0NTewhDOdL7Ykji+dOLuOLhI2BbaFETICx4MlQitdqoUsCQ2vhKGjglNfU0SWmILaj43M5oEAOwAAAAAAAAAAAA==",
	style:"width:16px;",
});

	var $downloadCancelBtn = $("<button>", {
		class:"downloadCancelBtn"
	});
	$downloadCancelBtn.attr({
		"label": "Cancelar descarga: " + name,
		"title": "Cancelar descarga: " + name,
	});

	var $downloadCancelBtnProgress = $("<div>", {
		class:"downloadCancelBtnProgress"
	});
	$downloadCancelBtn.append($downloadCancelBtnProgress);

	var $downloadCancelBtnLabel = $("<span>", {
		class:"downloadCancelBtnLabel",
		html:$downloadCancelBtnLabel_img,
	});
	$downloadCancelBtn.append($downloadCancelBtnLabel);

	var $downloadCancelCont = $("#downloadCancelCont");
	$downloadCancelCont.append($downloadCancelBtn);

	var data = {
		method: 'GET',
		responseType: 'arraybuffer',

		onload: function (res) {
			console.log(name + " > GM_download2 > onload:", "descarga completa ini");
			$downloadCancelBtnLabel.append($downloadCancelBtnLabel_img2);

			var blob = new Blob([res.response], {type: 'application/octet-stream'});
			var url = URL.createObjectURL(blob); // blob url

			var a = document.createElement('a');
			a.setAttribute('href', url);
			a.setAttribute('download', data.name != null ? data.name : 'filename');
			document.documentElement.appendChild(a);

			// call download
			// a.click() or CLICK the download link can't modify filename in Firefox (why?)
			// Solution from FileSaver.js, https://github.com/eligrey/FileSaver.js/
			var e = new MouseEvent('click');
			a.dispatchEvent(e);

			document.documentElement.removeChild(a);

			setTimeout(function(){
				// reduce memory usage
				URL.revokeObjectURL(url);
				if ('close' in blob) blob.close(); // File Blob.close() API, not supported by all the browser right now
				blob = undefined;
			}, 1000);

			if (typeof data.onafterload === 'function') data.onafterload(); // call onload function

			$downloadCancelBtn.remove();
			console.log(name + " > GM_download2 > onload:", "descarga completa fin");
		},

		onprogress: function(res) {
			if (res.lengthComputable) {
				var percentComplete = res.loaded / res.total;
				percentComplete = percentComplete * downloadCancelBtn_width;
				$downloadCancelBtnProgress.css("width", percentComplete.toFixed(3)+"px");
				console.log(name + " > GM_download2 > onprogress:", percentComplete.toFixed(3) + " de " + downloadCancelBtn_width);
				if(parseFloat(percentComplete) >= parseFloat(downloadCancelBtn_width)){
					$downloadCancelBtnLabel.html($downloadCancelBtnLabel_img2);
					//$downloadCancelBtn.off('click');
					console.log(name + " > GM_download2 > onprogress:", "100% = " + parseFloat(percentComplete) + " - " + parseFloat(downloadCancelBtn_width));
				}
			} else {
				GM_xhr.abort();
				$downloadCancelBtn.remove();
				console.log(name + " > GM_download2 > onprogress:", "El tamaño total es desconocida");
			}
		},
	};

	if (typeof url === 'string') {
		data.url = url;
		data.name = name;
	}
	else {
		if (url instanceof Object === false) return;

		// as documentation, you can only use [url, name, headers, saveAs, onload, onerror] function, but we won't check them
		// Notice: saveAs is not supported
		if (url.url == null) return;

		for (var i in url) {
			if (i === 'onload') data.onafterload = url.onload; // onload function support
			else data[i] = url[i];
		}
	}

	// it returns this GM_xhr, thought not mentioned in documentation
	var GM_xhr = GM_xmlhttpRequest(data);

	$downloadCancelBtn.on({
		click: function() {
			GM_xhr.abort();
			this.remove();
		},
	});

	return GM_xhr;
}

// Set JSON localstorage
Storage.prototype.setObject = function(key, value) {
	this.setItem(key, JSON.stringify(value));
};

// Retrieve JSON localstorage
Storage.prototype.getObject = function(key) {
	var value = this.getItem(key);
	return value && JSON.parse(value);
};

// Get the setting from an encoded URL string
String.prototype.getSetting = function(setting, index) {
	index = index*2-1 || 1;
	var val = false;
	var regex = new RegExp("(?:\\?|&|^|,)"+setting+"=([^&|,]*)", "g");
	var split = this.split(regex);
	if (split.length > index) {
		val = split[index].split(",")[0];
	}

	return val;
};

String.prototype.setSetting = function(setting, value) {
	var newString = this;
	var hasQuestionMark = (newString.indexOf("?") !== -1);
	if (!hasQuestionMark) {
		newString += "?";

	// Search for setting, delete it if it exists
	} else {
		var search = newString.split(setting+"=");
		if (search.length > 1) {
			search[1] = search[1].replace(/[^\&]*/, "");
			newString = search.join("");
		}
	}

	// Append the setting on the end
	var ampersand = (hasQuestionMark) ? "&" : "";
	newString += ampersand + setting + "=" + value;

	// Remove multiple ampersand
	newString = newString.replace(/&{2,}/g, "&");

	return newString;
};

// Return the indexes of records with specified value
Array.prototype.listIndexOf = function(property, value) {
	var indexes = [];

	// If the value exists
	if (typeof(value) !== "undefined") {
		value = value.toString();
		for (var i = 0; i<this.length; i++) {
			var str = (this[i][property]) ? this[i][property].toString() : "";
			if (str === value) {
				indexes.push(i);
			}
		}
	}

	return indexes;
};

// Return the records with specified value
Array.prototype.listMatches = function(property, value){
	var indexes = this.listIndexOf(property, value);
	var values = [];
	for (var i = 0; i<indexes.length; i++){
		values.push(this[indexes[i]]);
	}

	return values;
};

// Assert function
function assert(condition, message) {
	var context = "Youtube Downloader - ";
	if (!condition) {
		message = message || "Assertion failed";
		if (typeof Error !== "undefined") {
			throw new Error(context + message);
		}
		throw message; // Fallback
	}
}

// Adds useful prototyping functions for jQuery objects
$.fn.extend({
	toggleState: function(){
		if ($(this).hasClass("disabled")){
			$(this).removeClass("disabled");
		} else {
			$(this).addClass("disabled");
		}
	},
	onState: function(){
		if ($(this).hasClass("disabled")){
			$(this).html("");
			$(this).removeClass("disabled");
			$(this).append($downloadIcon).append($("<span>", {
				html:"",//Download
				class:"midalign"
			}));
		}
	},
});

// src/classes/display.js
// =================================================
// Generates the display, updates the display, all
// things related to the interface can be found here
function Display() {
	// The text colour of the size once loaded
	this.SIZE_LOADED = "red";

	// The text colour of the size when waiting on audio size
	this.SIZE_WAITING = "green";

	// Sprites
	// Download icon (with cloud)
	this.$downloadIcon = $("<img>", {
		class:"midalign downloadIcon",
		src:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAMAAACsjQ8GAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAA81BMVEX///8AAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AgAAY1NEfAAAAT3RSTlMAACKO3/z96KM0ADHF/tpMGb3ZNHL6/fb29vb6orzQRj8/PI7n2MAJaf3a8qGXcQU+l5TT8lIbwuNNHLLjTRyy400csuNNHLLjSxuy4sj+3zONWQAAAAFiS0dEUONuTLwAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfgBwMLDTTiSqfFAAAAuUlEQVQoz92S1Q7DMBRD3TEzddytg46ZmTn//zfrqklLpML7/GbnSLF0DcBktliJimx2h9MFwO3xEi35/EAgSHQUCiMS/dlYPMHzfDJFEWlkKJfN5QVBKBSpSESJcuUKOI5DlYok1PQB8m9AvdFstRWg0+31VYDBEJ/njzAaq30xmX4JzObqHRZLhcBqrVVys5UJ7PZMSfpY5HAETmc6kZhzE3K53u5MIDKDkfV4vhifNp6c4WiNZv8GO3e7aQ4nYEYAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE2LTA3LTAzVDExOjEzOjUyLTA0OjAwhxoNwQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNi0wNy0wM1QxMToxMzo1Mi0wNDowMPZHtX0AAAAASUVORK5CYII="
	});
	// Down select arrow (for dropdown)
	this.$downArrow = $("<img>", {
		class:"midalign downArrow",
		src:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABmJLR0QAtQC1ALVZ+kfjAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4AcFEAAUWZLqDQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAA2ElEQVRo3u2UQQ6EIAxFP16r5++tJjKbiYkRtUCrkPlvzeK9QgEIIYQQQv6XJCIrgDRrwKKqC4A8oXtW1bRNfrKbyL/B74UnidjkUZIdPGInjzPRUSNU9eC0nBwcbbFzSR53Ux7kJg7PxhwwQMSlPKxiL0XcyqNG6uEIkzxqhR6KMMujRSY4okoerSJBEdXy6JFwjmiSR6+AU0SzPDwmKCL5LXl4PYHGiG55eC5iZYSLPLx/EmOEm7x7gCHCVT4k4CLCXT4soBARIh+OiKwi8gEhhBBCCAnhCzz2dWSNsOWhAAAAAElFTkSuQmCC"
	});
}

Display.prototype = {
	update: function() {
		var _this = this;
		var sizes = qualities.sizes;

		// Main window
		var $downloadBtnInfo = $("#downloadBtnInfo");
		sizes.getSize($downloadBtnInfo.find("span:eq(0)"), function($span, size) {
			_this.updateDisplay($span, size, true);
		});

		// Drop down list
		$lis = $("#options").find("li");
		for (var i = 0; i<$lis.length; i++) {
			sizes.getSize($lis.eq(i), function($li, size) {
				_this.updateDisplay($li, size);
			});
		}
	},
	// Initialises the display
	initOptions: function(qualities) {
		// Fallback for setting to top value
		var $topEl = false;

		// Reset
		this.updateInfo(false);
		$options = $("<ul>", {
			id:"options",
			class:"unselectable",
		});

		// Initialise items in the drop-down list
		for (i = 0; i<qualities.items.length; i++) {
			var quality = qualities.items[i];
			var display = (quality.hidden) ? "none" : "inherit";

			$li = $("<li>", {
				html  : quality.label,
				itag  : quality.itag,
				style : "display:"+display,
			});

			// Tags - get them and then append them to the $li
			$tags = this.getTags(quality);
			for (var j = 0; j<$tags.length; j++) {
				$li.append($tags[j]);
			}

			// Add the $li to the $options
			$options.append($li);

			// Add the first as a fallback
			if (!$topEl) $topEl = $li;

			// If it matches the set quality, assign it to the info box
			var sameQuality = (quality.itag === Number(localStorage.selQuality));
			var visible     = !quality.hidden;
			if (sameQuality && visible) {
				$topEl = $li;
			}
		}

		// Update the top panel with the top element
		this.updateInfo($topEl);

		// Prepend options if necessary
		if ($("#options").length === 0 && $options) {
			$("#downloadBtnCont").append($options);
		}
	},
	// Updates the display LIST-ITEM
	updateDisplay: function($li, size, forceNeutralFloat) {
		var item = qualities.getFromItag($li.attr("itag"));
		var sizes = qualities.sizes;

		var _this = this;
		var color = (item.dash) ? this.SIZE_WAITING : this.SIZE_LOADED;

		// If the SIZE tag doesn't already exist, add it
		var extraClass = (forceNeutralFloat) ? " floatNormal" : "";
		$spanSize = $li.find("span.size");

		// Add it if it doesn't exist
		if ($spanSize.length === 0) {
			$spanSize = $("<span>", {
				style:"color:"+color,
				class:"size ignoreMouse"+extraClass
			});
			$li.append($spanSize);
		}

		$spanSize.html(sizes.formatSize(size));

		// If it is of the DASH format
		if (item.dash) {
			if (globalProperties.audioSize) {
				// Let the size be the sum of the size and the audio size
				size = parseInt(size) + parseInt(globalProperties.audioSize);

				$li.find("span.size").html(sizes.formatSize(size));
				$li.find("span.size").css("color", this.SIZE_LOADED);

			} else {
				// Try again in 2 seconds
				setTimeout(function() {
					_this.updateDisplay($li, size);
				}, 2000);
			}
		}
	},

	// Returns a jquery element of the download button with a certain text
	updateDownloadButton: function (text, disabled) {
		// Create the download button container
		var $container = this.checkContainer();

		// Determine if it is of the disabled class
		var disabledText = (disabled) ? " disabled" : "";

		// Create the button if it doesn't exist
		var $button = $container.find("#downloadBtn");
		if ($button.length === 0) {
			$button = $("<button>", {
				id:"downloadBtn",
				title:"Download",
			});
			$button.append(this.$downloadIcon);
			$button.append($("<span>", {
				class:"midalign"
			}));

			// Append it to the container
			$container.append($button);
		}

		// Update the properties
		$button.attr("class", disabledText);
		$button.find("span").html(text);
	},

	// Update the downloadBtnInfo (top, non drop-down)
	updateInfo: function ($li) {
		var $downloadBtnInfo = $("#downloadBtnInfo");

		// Add it if it doesn't exist
		if ($downloadBtnInfo.length === 0) {
			$downloadBtnInfo = $("<span>", {
				id:"downloadBtnInfo"
			}).append(this.$downArrow);

			// Find the container
			var $container = this.checkContainer();

			// Append it to the container
			$container.append($downloadBtnInfo);
		}

		// If an element was passed, prepend it
		if ($li) {
			var item = qualities.getFromItag($li.attr("itag"));
			$span = $downloadBtnInfo.find("span:eq(0)");
			if ($span.length === 0) {
				$span = $("<span>");

				// Prepend the new element
				$downloadBtnInfo.prepend($span);
			}

			// Set the span ATTRIBUTES
			$span.attr({
				"itag": item.itag
			});

			var $child = $span.find("span.text");
			if ($child.length === 0) {
				$child = $("<span>", {
					class:"text"
				});
				$span.append($child);
			}

			// Set the span HTML
			$child.html(item.label);
		}
	},

	// Fetch the container if it exists, otherwise make it
	checkContainer: function() {
		var $container = $("#downloadBtnCont");
		if ($container.length === 0) {
			$container = $("<span>", {
				id:"downloadBtnCont",
				class:"unselectable"
			});

			$("#watch7-subscription-container").append($container);
		}

		return $container;
	},
	getTags: function(quality) {
		$tags = [];
		$tags.push($("<span>", {
			class:"tag ignoreMouse",
			html:quality.type
		}));

		var dash = quality.dash;
		if (dash && dash !== "false") {
			$tags.push($("<span>", {
				class:"tag ignoreMouse",
				html:"DASH"
			}));
		}

		var muted = quality.muted;
		if (muted && muted !== "false") {
			$tags.push($("<span>", {
				class:"tag ignoreMouse",
				html:"MUTED"
			}));
		}

		return $tags;
	}
};

// src/classes/qualities.js
// =================================================
// This class handles the qualities that can be downloaded
// This class also manages the the display of qualities (both
// the top quality and the list of qualities)

function Qualities() {
	this.items = [];
	this.sizes = new GetSizes();

	this.itags = {
		5: {
			type:"flv"
		},
		17: {
			resolution:144,
			type:"3gpp"
		},
		18: {
			resolution:360,
			type:"mp4"
		},
		22: {
			resolution:720,
			type:"mp4"
		},
		36: {
			resolution:180,
			type:"3gpp"
		},
		43: {
			resolution:360,
			type:"webm"
		},
		133: {
			resolution:240,
			type:"mp4",
			dash:true
		},
		134: {
			resolution:360,
			type:"mp4",
			muted:true
		},
		135: {
			resolution:480,
			type:"mp4",
			dash:true
		},
		136: {
			resolution:720,
			type:"mp4",
			muted:true
		},
		137: {
			resolution:1080,
			type:"mp4",
			dash:true
		},
		140: {
			audio:true,
			type:"mp4"
		},
		160: {
			resolution:144,
			type:"mp4",
			muted:true
		},
		171: {
			audio:true,
			type:"webm",
		},
		242: {
			resolution:240,
			type:"webm",
			muted:true
		},
		243: {
			resolution:360,
			type:"webm",
			muted:true
		},
		244: {
			resolution:480,
			type:"webm",
			dash:true
		},
		247: {
			resolution:720,
			type:"webm",
			muted:true
		},
		248: {
			resolution:1080,
			type:"webm",
			muted:true
		},
		249: {
			audio:true,
			type:"webm",
		},
		250: {
			audio:true,
			type:"webm",
		},
		251: {
			audio:true,
			type:"webm",
		},
		264: {
			resolution:1440,
			type:"mp4",
			dash:true
		},
		266: {
			resolution:2160,
			type:"mp4",
			dash:true
		},
		271: {
			resolution:1440,
			type:"webm",
			dash:true
		},
		278: {
			resolution:140,
			type:"webm",
			muted:true
		},
		298: {
			resolution:720,
			fps:60,
			type:"mp4",
			dash:true
		},
		299: {
			resolution:1080,
			fps:60,
			type:"mp4",
			dash:true
		},
		302: {
			resolution:720,
			fps:60,
			type:"webm",
			muted:true
		},
		303: {
			resolution:1080,
			fps:60,
			type:"webm",
			muted:true
		},
		313: {
			resolution:2160,
			type:"webm",
			dash:true
		},
	};
}

Qualities.prototype = {
	reset: function() {
		this.items = [];
	},
	initialise: function(callback) {
		this.reset();
		var _this = this;
		this.getPotentialDash(function(potential) {
			var split = potential.split(",");

			// Iterate through each option
			for (var i = 0; i<split.length; i++) {
				// Get relevant properties
				var sect = split[i];
				var url  = decodeURIComponent(sect.getSetting("url"));
				var s    = sect.getSetting("s");
				var type = decodeURIComponent(url.getSetting("mime"));
				var clen = url.getSetting("clen") || sect.getSetting("clen");
				var itag = parseInt(url.getSetting("itag"), 10);
				var size = false;

				// Decode the url
				url = signature.decryptSignature(url, s);

				// Get data from the ITAG identifier
				var tag = _this.itags[itag] || {};

				// Get the value from the tag
				var value = _this.getValue(tag);

				// Get the label from the tag
				var label = sect.getSetting("quality_label") || _this.getLabel(tag);

				// If we have content-length, we can find size IMMEDIATELY
				if (clen) {
					size = parseInt(clen, 10);
				}

				// Get the type from the tag
				assert(type.split("/").length > 1, "Incorrect type: "+type);
				var newType = type.split("/")[1].split(",")[0];
				if (newType !== tag.type) {
					console.log("Error with "+itag+", "+newType+"!="+tag.type);
					console.log(decodeURIComponent(url));
				}

				// Fix the types
				if (newType === "mp4" && tag.audio) {
					newType = "m4a";
				}
				if (newType === "mp4" && tag.dash) {
					newType = "m4v";
				}

				// Append to qualities (if it shouldn't be ignored)
				var item = {
					itag : itag,
					url  : url,
					size : size,
					type : newType,
					dash : tag.dash || false,
					muted: tag.muted || false,
					label: label,
					audio: tag.url || false,
					value: value
				};
				if (_this.checkValid(item)) {
					_this.items.push(item);

				// Check if it should be added but HIDDEN
				} else {
					if (newType === "m4a") {
						item.hidden = true;
						_this.items.push(item);
					}
				}

				_this.checkMP3(item);

				// If it is the audio url - find the size and update
				if (newType === "m4a" && tag.audio) {
					var $li = $("<li>", {
						url  : url,
						itag : itag,
					});

					_this.sizes.getSize($li, function($li, size) {
						globalProperties.audioSize = size;
					});
				}
			}
			callback();
		});
	},
	getLabel: function(tag) {
		var label = false;
		tag = tag || {};
		if (tag.resolution) {
			label = tag.resolution.toString()+"p";
			if (tag.fps) {
				label += tag.fps.toString();
			}
		} else if (tag.audio) {
			label = "Audio";
		}

		return label;
	},
	getValue: function(tag) {
		// Base value is the resolution OR 0
		var value = tag.resolution || 0;

		// Multiply if it has an fps tag (high frame rate)
		if (tag.fps >= 30) {
			value += 10;
		}

		// Multiply if it is mp4
		if (tag.type === "mp4" || tag.type === "m4v") {
			value *= 100;
		}

		// Make it negative if it's audio
		if (tag.audio) {
			value -= 5;
			value *= -1;
		}

		if (tag.type === "mp3") {
			value -= 1;
		}

		return value;
	},

	sortItems: function() {
		var _this = this;
		this.items.sort(_this.sortDescending);
	},
	sortDescending: function(a, b) {
		if (isNaN(a.value)) a.value = 0;
		if (isNaN(b.value)) b.value = 0;
		return Number(b.value) - Number(a.value);
	},

	// Check if the item should be ignored or not
	checkValid: function(item) {
		var valid = true;

		// If it is muted and we are ignoring muted
		if (settings.get("ignoreMuted") && item.muted) {
			valid = false;
		}

		// If it matches a blacklisted type
		if (settings.get("ignoreTypes").indexOf(item.type) !== -1) {
			valid = false;
		}

		// If it matches a blacklisted value
		if (settings.get("ignoreVals").indexOf(item.value) !== -1) {
			valid = false;
		}

		return valid;
	},

	// Get potential inclusive of dash formats from
	// 2010-2011
	getPotentialDash: function(callback) {
		// Get native potential, within ytplayer
		var potential = this.getPotential();

		var dashmpd = ytplayer.config.args.dashmpd;

		// We must make a get request, and find the
		// additional qualities within the file
		if (dashmpd !== undefined) {
			console.log("Making dashmpd request...");
			var _this = this;
			Ajax.request({
				method:"GET",
				url:dashmpd,
				success: function(xhr, text, jqXHR) {
					var text = (typeof(xhr) === "string") ? jqXHR.responseText : xhr.responseText;

					// Add the potential from BaseURL tags
					var add = [];
					var addPotential = text.split(/\<BaseURL\>([^\<]*)\<\/BaseURL\>/);
					for (var i = 0; i<Math.floor(addPotential.length/2); i++) {
						var url = addPotential[i*2 + 1];
						add.push(_this.decodeURL(url));
					}

					assert(add.length > 0, "No videos found in dashmpd!");
					potential = potential+",url="+add.join(",url=");
					callback(potential);
				}
			});
		} else {
			callback(potential);
		}
	},

	// Process the forward slashes / and make them into
	// ampersands (&) and equals (=)
	decodeURL: function(url) {
		var split = url.split("videoplayback/");
		var host = split[0];
		var str = split[1];
		str = str.replace(/\/([^\/]*(?:\/|$))/g, "=$1");
		str = str.replace(/\//g, "&");
		url = host+"videoplayback?"+str;

		// Encode it
		return encodeURIComponent(url);
	},
	// Get potential list
	getPotential: function() {
		assert(ytplayer !== undefined, "Ytplayer is undefined!");

		// Find the valid links and place them in an array
		var args = ytplayer.config.args;
		var arr = [];
		if (args.adaptive_fmts !== undefined && args.adaptive_fmts !== "") {
			arr.push(args.adaptive_fmts);
		}
		if (args.url_encoded_fmt_stream_map !== undefined && args.url_encoded_fmt_stream_map !== "") {
			arr.push(args.url_encoded_fmt_stream_map);
		}


		// Assert that there is a potential
		var potential = arr.join(",");
		potential = potential.replace(/([0-9])s=/g, ",s=");

		// Make potential false if neither found
		if (arr.length === 0) potential = false;

		return potential;
	},
	checkPotential: function(potential) {
		var valid = false;
		if (potential) {
			var lengths = this.getPotentialLengths(potential);
			valid = (lengths.url >= lengths.url && lengths.sig > 1);

			// Trace out why it isn't valid
			if (!valid) {
				var split = potential.split(",") || "";
				for (var i = 0; i<split.length; i++) {
					var splitLengths = this.getPotentialLengths(split[i]);
					if (splitLengths.url !== 1 || splitLengths.sig !== 1) {
						console.log("checkPotential");
						console.log(split[i]);
						console.log(splitLengths.url, splitLengths.sig);
					}
				}
			}
		}

		// Return if it is valid
	    return valid;
	},
	// Get url and sig lengths from potential list
	getPotentialLengths: function(potential) {
		return {
			url: potential.split("url=").length - 1,
			sig: decodeURIComponent(potential).split(/(?:(?:&|,|\?|^)s|signature|sig)=/).length - 1
		};
	},

	// Check if MP3 should be added
	checkMP3: function(item) {
		if (item.type === "m4a") {
			// Copy over the properties into
			// a new object
			var newItem = {};
			for (var key in item) {
				newItem[key] = item[key];
			}

			newItem.type = "mp3";
			newItem.itag += 1;
			this.items.push(newItem);
		}
	},

	// Get from ITAG
	getFromItag: function(itag) {
		var matches = qualities.items.listMatches("itag", Number(itag));

		if (matches.length !== 1) {
			console.log("ERROR: Found "+matches.length+" with itag: "+itag);
		}
		var item = matches[0] || {};

		// Return the item obtained from the itag
		return item;
	}
};

// src/classes/qualities/getSizes.js
// =================================================
// Obtains the sizes of each of the urls, adding
// the "size" attribute to each li element, and setting
// the size in kb/mb/gb etc on each element

function GetSizes() {
	// Number of decimal places to represent the
	// size as
	this.SIZE_DP = 1;
}

GetSizes.prototype = {
	getSize: function($li, callback) {
		var item = qualities.getFromItag($li.attr("itag"));
		var url = item.url;

		// Attempt to obtain the size from the qualities values
		var size = item.size;

		if (size) {
			callback($li, size);
		} else {
			// We must make a cross-domain request to determine the size from the return headers...
			Ajax.request({
				method:"HEAD",
				url:url,
				success:function(xhr, text, jqXHR) {
					var size = Number(Ajax.getResponseHeader(xhr, text, jqXHR, "Content-Length"));

					// Revert to old header name
					if (size === 0) {
						size = Number(Ajax.getResponseHeader(xhr, text, jqXHR, "Content-length"));
					}
					item.size = size;
					callback($li, size);
				}
			});
		}
	},

	// Takes the input in bytes, and returns a formatted string
	formatSize: function(size) {
		size = parseInt(size, 10);
		var sizes = {
			GB:Math.pow(1024,3),
			MB:Math.pow(1024,2),
			KB:Math.pow(1024,1),
		};

		// Default of 0MB
		var returnSize = "0MB";

		for (var sizeFormat in sizes){
			if (sizes.hasOwnProperty(sizeFormat)) {
				var minSize = sizes[sizeFormat];
				if (size > minSize) {
					returnSize = (size/minSize).toFixed(this.SIZE_DP) + sizeFormat;
					break;
				}
			}
		}

		// Return the string of return size
		return returnSize;
	}
};

// src/classes/signature.js
// =================================================
// Gets the signature code from YouTube in order
// to be able to correctly decrypt direct urls
// USES: ytplayer.config.assets.js

function Signature() {
	// constructor
}

Signature.prototype = {
	fetchSignatureScript: function(callback) {
		var scriptURL = this.getScriptURL(ytplayer.config.assets.js);

		// If it's only positive, it's wrong
		if (!/,0,|^0,|,0$|\-/.test(settings.get("signatureCode"))) {
			settings.set("signatureCode", null);
		}

		var _this = this;
		Ajax.request({
			method:"GET",
			url:scriptURL,
			success:function(xhr, text, jqXHR) {
				var text = (typeof(xhr) === "string") ? jqXHR.responseText : xhr.responseText;
				_this.findSignatureCode(text);
				callback();
			},
			error:function(xhr, text, jqXHR) {
				console.log("Error getting signature script");
			}
		});
	},
	getScriptURL: function(scriptURL) {
		var split = scriptURL.split("//");
		if (split[0] === "") {
			split.shift();
			scriptURL = window.location.href.split(":")[0] + "://" + split.join("//");
		}

		return scriptURL;
	},
	isInteger: function(n) {
		return (typeof n === 'number' && n%1 === 0);
	},
	findSignatureCode: function(sourceCode) {
		// Signature function name
		var sigCodes = [
			this.regMatch(sourceCode, /\.set\s*\("signature"\s*,\s*([a-zA-Z0-9_$][\w$]*)\(/),
			this.regMatch(sourceCode, /\.sig\s*\|\|\s*([a-zA-Z0-9_$][\w$]*)\(/),
			this.regMatch(sourceCode, /\.signature\s*=\s*([a-zA-Z_$][\w$]*)\([a-zA-Z_$][\w$]*\)/)
		];

		var sigFuncName = this.getFirstValid(sigCodes);
		var binary = [];
		binary.push(sourceCode);
		//SaveToDisk(URL.createObjectURL(new Blob(binary, {type: "application/js"})), {title:"hi", type:".js"});
		assert(sigFuncName !== null, "Signature function name not found!");


		// Regcode (1,2) - used for functionCode
		var regCodes = [
			this.regMatch(sourceCode, sigFuncName + '\\s*=\\s*function' +
			'\\s*\\([\\w$]*\\)\\s*{[\\w$]*=[\\w$]*\\.split\\(""\\);\n*(.+);return [\\w$]*\\.join'),
			this.regMatch(sourceCode, 'function \\s*' + sigFuncName +
			'\\s*\\([\\w$]*\\)\\s*{[\\w$]*=[\\w$]*\\.split\\(""\\);\n*(.+);return [\\w$]*\\.join')
		];

		var funcCode = this.getFirstValid(regCodes);

		// Slice function name
		var sliceFuncName = this.regMatch(sourceCode, /([\w$]*)\s*:\s*function\s*\(\s*[\w$]*\s*,\s*[\w$]*\s*\)\s*{\s*(?:return\s*)?[\w$]*\.(?:slice|splice)\(.+\)\s*}/);

		// Reverse function name
		var reverseFuncName = this.regMatch(sourceCode, /([\w$]*)\s*:\s*function\s*\(\s*[\w$]*\s*\)\s*{\s*(?:return\s*)?[\w$]*\.reverse\s*\(\s*\)\s*}/);

		// Possible methods
		var methods = {
			slice:   '\\.(?:'+'slice'+(sliceFuncName?'|'+sliceFuncName:'')+
					 ')\\s*\\(\\s*(?:[a-zA-Z_$][\\w$]*\\s*,)?\\s*([0-9]+)\\s*\\)',
			reverse: '\\.(?:'+'reverse'+(reverseFuncName?'|'+reverseFuncName:'')+
					 ')\\s*\\([^\\)]*\\)',
			swap:    '[\\w$]+\\s*\\(\\s*[\\w$]+\\s*,\\s*([0-9]+)\\s*\\)',
			inline:  '[\\w$]+\\[0\\]\\s*=\\s*[\\w$]+\\[([0-9]+)\\s*%\\s*[\\w$]+\\.length\\]'
		};

		var decodeArray = [];
		var codeLines = funcCode.split(';');
		for (var i = 0; i<codeLines.length; i++) {
			var codeLine = codeLines[i].trim();

			if (codeLine.length > 0) {
				var arrSlice   = codeLine.match(methods.slice);
				var arrReverse = codeLine.match(methods.reverse);

				// Use slice method
				if (arrSlice && arrSlice.length >= 2) {
					var slice = parseInt(arrSlice[1], 10);
					assert(this.isInteger(slice), "Not integer");
					decodeArray.push(-slice);

				// Reverse
				} else if (arrReverse && arrReverse.length >= 1) {
					decodeArray.push(0);

				// Inline swap
				} else if (codeLine.indexOf('[0]') >= 0) { // inline swap
					var nextLine = codeLines[i+1].trim();
					var hasLength = (nextLine.indexOf(".length") >= 0);
					var hasZero =   (nextLine.indexOf("[0]") >= 0);

					if (nextLine && hasLength && hasZero) {
						var inline = this.regMatch(nextLine, methods.inline);
						inline = parseInt(inline, 10);
						decodeArray.push(inline);
						i += 2;
					}

				// Swap
				} else if (codeLine.indexOf(',') >= 0) {
					var swap = this.regMatch(codeLine, methods.swap);
					swap = parseInt(swap, 10);
					assert(this.isInteger(swap) && swap > 0);
					decodeArray.push(swap);
				}
			}
		}

		// Make sure it is a valid signature
		assert(this.isValidSignatureCode(decodeArray));
		globalProperties.signatureCode = decodeArray;
	},
	isValidSignatureCode: function(arr) {
		var valid = false;
		var length = arr.length;
		if (length > 1) {
			valid = true;

			// Ensure that every value is an INTEGER
			for (var i = 0; i<length; i++) {
				if (!this.isInteger(parseInt(arr[i],10))) {
					valid = false;
				}
			}
		}

		return valid;
	},
	regMatch: function(string, regex) {
		if (typeof(regex) === "string") {
			regex = new RegExp(regex);
		}

		var result = regex.exec(string);
		if (result) {
			result = result[1];
		}

		return result;
	},
	getFirstValid: function(arr) {
		var val = null;
		for (var i = 0; i<arr.length; i++) {
			if (arr[i]) {
				val = arr[i];
				break;
			}
		}

		return val;
	},
	decryptSignature: function(url, s) {
		url = decodeURIComponent(url);
		var sig = url.getSetting("signature") || url.getSetting("sig");

		// Decryption is only required if signature is non-existant AND
		// there is an encrypted property (s)
		if (!sig) {
			assert(s !== "false" && s, "S attribute not found!");
			sig = this.decodeSignature(s, globalProperties.signatureCode);
			url = url.setSetting("signature", sig);
		}

		url = url.setSetting("ratebypass", "yes");
		assert(url.getSetting("signature"), "URL does not have signature!");

		return url;
	},
	decodeSignature: function(s) {
		var arr = globalProperties.signatureCode;
		var sigA = s.split("");
		for (var i = 0; i<arr.length; i++) {
			var act = arr[i];

			// Determine what sigA should be, based
			// on polarity of act
			if (act > 0) {
				sigA = this.swap(sigA, act);
			} else if (act === 0) {
				sigA = sigA.reverse();
			} else {
				sigA = sigA.slice(-act);
			}
		}

		var result = sigA.join("");
		return result;
	},
	swap: function(a, b) {
		var c = a[0];
		a[0] = a[b%a.length];
		a[b] = c;
		return a;
	}
};

// src/classes/unique/ajaxclass.js
// =================================================
// Isolates the async functions from the modules

function AjaxClass() {
}

AjaxClass.prototype = {
	request: function(params) {
		// Setup the request
		var success = params.success;
		var error   = params.error;

		params.onerror = function(xhr) {
			error(xhr);
		};

		params.onload = function(xhr) {
			if (xhr.readyState === 4 && xhr.status === 200) {
				success(xhr);
			} else {
				console.log(xhr);
				if (typeof error === "function") error(xhr);
			}
		};

		// Call the request
		GM_xmlhttpRequest(params);
	},
	getResponseHeader: function(xhr, text, jqXHR, type) {
		var value = false;
		if (typeof xhr.getResponseHeader === "function") {
			value = xhr.getResponseHeader(type);
		} else if (xhr.responseHeaders) {
			var regex = new RegExp(type.split("-")[1]+": (.*)");
			var match = regex.exec(xhr.responseHeaders);
			if (match){
				value = match[1];
			}
		}

		// Return the value
		return value;
	}
};

// src/classes/unique/css.js
// =================================================
// This function adds styling to the page by
// injecting CSS into the document

(function() {
	var css = {
		".disabled": {
			"cursor":"default!important",
		},
		".midalign": {
			"vertical-align":"middle!important",
		},
		".unselectable": {
			"-webkit-user-select":"none",
			"-moz-user-select":"none",
			"-ms-user-select":"none",
		},
		"#downloadBtnCont": {
			"margin-left":"1em",
			"position":"relative",
			"display":"inline-block,",
		},
		"#downloadBtn": {
			"padding":"0 8px 0 5.5px",
			"height":"24px",
			"background-color":"green",
			"color":"white",
			"font-weight":"normal",
			"box-shadow":"0 1px 0 rgba(0,0,0,0.05)",
			"vertical-align":"middle",
			"font-size":"11px",
			"border":"solid 1px transparent",
			"border-radius":"2px 0 0 2px",
			"cursor":"pointer",
			"font":"11px Roboto,arial,sans-serif",
			"-webkit-user-select":"none",
			"-moz-user-select":"none",
			"-ms-user-select":"none",
			"user-select":"none",
		},
		"#downloadBtn.disabled": {
			"background-color":"gray!important"
		},
		"#downloadBtn:hover": {
			"background-color":"darkgreen"
		},
		"#downloadBtn span": {
			"font-size":"12px"
		},
		"#downloadBtn img": {
			"height":"12px"
		},
		"#downloadBtnInfo": {
			"cursor":"default",
			"height":"22px",
			"line-height":"24px",
			"padding":"0 6px",
			"color":"#737373",
			"font-size":"11px",
			"text-align":"center",
			"display":"inline-block",
			"margin-left":"-2px",
			"border":"1px solid #ccc",
			"background-color":"#fafafa",
			"vertical-align":"middle",
			"border-radius":"0 2px 2px 0",
		},
		"span.text": {
			"margin-right":"0.2em",
		},
		"ul#options": {
			"position":"absolute!important",
			"background-color":"white",
			"z-index":"500",
			"width":"200px",
			"padding":"0 5px",
			"cursor":"default",
			"box-shadow":"0 1px 2px rgba(0,0,0,0.5)",
			"left":"0",
			"display":"none",
		},
		"ul#options li": {
			"line-height":"2em",
			"padding":" 0 5px",
			"margin":"0 -5px",
		},
		"ul#options li:hover": {
			"background-color":"orange",
		},
		"span.size": {
			"float":"right",
		},
		"span.tag": {
			"margin":"0.2em",
			"padding":"0.2em",
			"background-color":"lightblue",
			"color":"grey",
		},
		".floatNormal": {
			"float":"inherit!important",
		},
		".ignoreMouse": {
			"pointer-events":"none",
		},
		"#watch7-user-header": {
			"overflow":"visible!important",
		},
		"#watch7-content": {
			"overflow":"visible!important",
			"z-index":"500!important",
		},

		// Fix the drag-drop events causing ghost image
		"img": {
			"pointer-events": "none"
		},
		/* Download sprites */
		".downloadIcon": {
		    "margin-right":"0px"
		},
		".downArrow": {
		    "margin-bottom":"-13px",
		    "margin-left":"6px",
		    "width":"13px",
		    "transform":"translateY(-50%)"
		},
		"#downloadCancelCont": {
			"cursor":"default",
			//"width":"40px",
			"height":"24px",
			"overflow":"hidden",
			"line-height":"24px",
			"padding":"0 0px",
			"color":"#737373",
			"font-size":"11px",
			"text-align":"center",
			"display":"inline-block",
			"margin":"0px",
			"border":"0px solid #ccc",
			"background-color":"#fafafa",
			"vertical-align":"middle",
			"border-radius":"0 0px 0px 0",
			"position":"relative",
		},
		".downloadCancelBtn": {
			"cursor":"pointer",
			"width":downloadCancelBtn_width+"px",
			"height":"24px",
			"overflow":"hidden",
			"line-height":"24px",
			"padding":"0 0px",
			"color":"#e62117",
			"font-size":"11px",
			"text-align":"center",
			"display":"inline-block",
			"margin":"0px",
			"border":"1px solid #ccc",
			"background-color":"transparent",
			"vertical-align":"top",
			"border-radius":"0 2px 2px 0",
			"position":"relative",
		},
		".downloadCancelBtnProgress": {
			"cursor":"default",
			"width":"0px",
			"height":"24px",
			"overflow":"hidden",
			"line-height":"24px",
			"padding":"0 0px",
			"color":"#737373",
			"font-size":"11px",
			"text-align":"center",
			"display":"inline-block",
			"margin":"0px",
			"border":"0px solid #ccc",
			"background-color":"#008000",
			"vertical-align":"top",
			"border-radius":"0 0px 0px 0",
			"position":"absolute",
			"z-index":"101",
		},
		".downloadCancelBtnLabel": {
			"cursor":"pointer",
			"width":downloadCancelBtn_width+"px",
			"height":"24px",
			"overflow":"hidden",
			"line-height":"24px",
			"padding":"0 0px",
			"color":"#e62117",
			"font-size":"11px",
			"text-align":"center",
			"display":"inline-block",
			"margin":"0px",
			"border":"0px solid #ccc",
			"background-color":"transparent",
			"vertical-align":"top",
			"border-radius":"0 0px 0px 0",
			"position":"relative",
			"z-index":"102",
		},
	};

	// Append the CSS to the document
	var node = document.createElement("style");
	var html = "";
	for (var key in css) {
		var props = css[key];

		html += key + " {\n";
		for (var prop in props) {
			html += "\t" + prop + ":" + props[prop] + ";\n";
		}

		html += "}\n";
	}

	node.innerHTML = html;
	document.body.appendChild(node);
})();

// src/classes/unique/download.js
// =================================================
// Functions that are used to download the video and audio
// files

function Download() {
	// Construct
}

Download.prototype = {
	// Download the file
	getVid: function($span, title) {
		var item = qualities.getFromItag($span.attr("itag"));
		var type = item.type;
		var dash = item.dash;

		title = title || this.getTitle(item.label);
		var name = title;
		var url = item.url.setSetting("title", encodeURIComponent(title));

		// MP3 change
		if (type === "mp3") {
			//name = "MP3 - "+name;
			type = "m4a";
		}

		// Save to disk
		this.saveToDisk(url, name+"."+type);

		// If it requires audio, download it
		if (dash) {
			this.handleAudio(name);
		}

		// Re-enable the button after 0.5 seconds
		setTimeout(function() {
			display.updateDownloadButton("");//Download
		}, 500);
	},
	getTitle: function(label) {
		label = (label) ? label : "";
		var str = $("title").html().split(" - YouTube")[0];

		// Add the label if required
		if (settings.get("label") && label.toString() !== "Audio") {
			str += " " + label.toString();
		}

		str = str.replace(/!|\+|\.|\:|\?|\||\\|\//g, "").replace(/\"/g, "'");
		return str;
	},
	// Download audio if required
	handleAudio: function(name) {
		// Download the audio file
		this.getVid($("#options").find("li[itag=140]:eq(0)"), "" + name);//AUDIO - 

		// Download the script

		/*
		var os = GetOs();
		var text = MakeScript(settings.title, type, "m4a", "mp4", os);
		settings.type = os.scriptType;
		if (os.os === 'win'){
			SaveToDisk(URL.createObjectURL(text), settings);
		} else {
			SaveToDisk("https://github.com/Domination9987/YouTube-Downloader/raw/master/muxer/Muxer.zip", settings);
		}*/
	},
	getOs: function() {
		var os = (navigator.appVersion.indexOf("Win") !== -1) ? "win" : "mac";
		var scriptType = (os === "win") ? "bat" : "command";
		return {os:os, scriptType:scriptType};
	},
	saveToDisk: function(url, name) {
		console.log("Trying to download:", name + " > " + url);
		if (typeof(GM_download) === "undefined") {
			//this.fallbackSave(url, name);
			GM_download2(url, name);
			//alert("Please enable GM_Download if you have videoplayback issues");
		} else {
			//GM_download(url, name);
			var GM_download_details = {
				url: url,
				name: name,
				//saveAs: true,//no funciona
				onload: function(res) {
					console.log(name + " > GM_download > onload:", "descarga completa");
				},
				onerror: function(download) {
					if(download.error == "not_whitelisted"){
						alert('Modificar la configuracion de Tampermonkey:'+'\n'+
							 'En: "Downloads BETA > Download Mode" escoger "Browser API"'+'\n'+
							 'En: "Downloads BETA > Whitelisted File Extensions" agregar /\.(3gpp|m4a|m4v)$/');
					};
					console.log(name + " > GM_download > onerror.error:", download.error);
					console.log(name + " > GM_download > onerror.details:", download.details);
					console.log(name + " > GM_download > onerror.details.current:", download.details.current);
				},
			};
			GM_download(GM_download_details);
		}
	},

	// Saves using the old method
	// NOTE: Does not work for audio or DASH formats
	//       will download as "videoplayback"
	fallbackSave: function(url, name) {
		var save = document.createElement('a');
		save.target = "_blank";
		save.download = name;//true
		console.log(decodeURIComponent(url));
		save.href = url;
		(document.body || document.documentElement).appendChild(save);
		save.onclick = function() {
			(document.body || document.documentElement).removeChild(save);
		};
		save.click();
	}
};

// src/classes/unique/settings.js
// =================================================
// This class handles the settings
// Uses localStorage to remember the settings

function Settings(defaultSettings) {
	// Fetch the settings from localStorage
	this.settings = {};

	// Set the default settings
	for (var key in defaultSettings) {
		if (defaultSettings.hasOwnProperty(key)) {
			this.settings[key] = defaultSettings[key];
		}
	}
}

Settings.prototype = {
	// Get the value of a property
	get: function(key) {
		var value = this.settings[key];
		if (Number(value) === value) {
			value = Number(value);
		}

		return value;
	},
	// Set a new property
	set: function(key, value) {
		this.settings[key] = value;
	}
};

// src/classes/unique/unsafe.js
// =================================================
function Unsafe() {
	this.id = 0;
}

Unsafe.prototype = {
	getVariable: function(name, callback) {
		var script = "(function() {"+
			"setTimeout(function(){"+
				"var event = document.createEvent(\"CustomEvent\");"+
				"var val = (typeof "+name+" !== 'undefined') ? "+name+" : false;"+
				"event.initCustomEvent(\""+name+"\", true, true, {\"passback\":JSON.stringify(val)});"+
				"window.dispatchEvent(event);"+
			"},100);"+
		"})()";

		// Inject the script
		this.injectScript(script, name, function(obj) {
			var passback = obj.detail.passback || {};
			callback(JSON.parse(passback));
		});
	},

	injectScript: function(script, name, callback) {
		//Listen for the script return
		var myFunc = function(e) {
			window.removeEventListener(name, myFunc);
			callback(e);
		};
		window.addEventListener(name, myFunc);
		this.id++;

		//Inject the script
		var s = document.createElement("script");
		s.innerText = script;
		(document.head||document.documentElement).appendChild(s);
		s.parentNode.removeChild(s);
	}
};

// src/main.js
// =================================================
// Variables
// Selected quality
localStorage.selQuality = localStorage.selQuality || 298;

// Default settings
var defaultSettings = {
	// Ignore muted
	ignoreMuted:true,

	// Types that are ignored
	ignoreTypes:["webm"],

	// Values that are ignored
	ignoreVals:[],

	// Have quality label on download
	label:true,
};

// Volatile properties
var globalProperties = {
	// Size of audio
	audioSize:false,

	// Obtained signature pattern
	signatureCode:false
};

// Objects
var Ajax      = new AjaxClass();
var settings  = new Settings(defaultSettings);
var signature = new Signature();
var display   = new Display();
var qualities = new Qualities();
var download  = new Download();
var unsafe    = new Unsafe();
var ytplayer  = {};

// Run the script ONLY if it's on the top
if (window.top === window) {
	AddEvents();
	Program();
}

// This function is run on every new page load....
function Program() {
	// Make sure it is of the correct URL
	var url = window.location.href;
	if (!url.match(/watch|embed/)) return;

	unsafe.getVariable("ytplayer", function(ytp) {
		// If the old thing is still there, wait a while
		ytplayer = ytp || {};
		if ($("#downloadBtn").length > 0 || !ytplayer.config) {
			setTimeout(Program, 2000);
			return;
		}

		// Verify that the potential is LOADED, by comparing the
		// number of SIGNATURES to the number of URLs
		var potential = qualities.getPotential();
		if (!qualities.checkPotential(potential)) {
			setTimeout(Program, 2000);
			return;
		}

		// Get the signature (required for decrypting)
		signature.fetchSignatureScript(function() {
			// Reset the audio size
			globalProperties.audioSize = false;

			// Initialise the available qualities
			qualities.initialise(function() {
				qualities.sortItems();
				// Update the download button, set it to be ENABLED
				// with text "Download"
				display.updateDownloadButton("");//Download

				// Initialise the options & add it to the frame
				display.initOptions(qualities, $("#downloadBtnInfo"));

				// Update the display (fetch sizes as well)
				display.update();
			});
		});
	});
}

// Adds events to the window
function AddEvents() {
	// Call the function on page change
	window.lastURL = window.location.href;
	setInterval(function() {
		var newURL = window.location.href;
		if (newURL !== window.lastURL) {
			window.lastURL = newURL;
			$(window).ready(function() {
				Program();
			});
		}
	}, 200);

	// On download button click
	$(document).on("click", "#downloadBtn", function() {
		// Ensure that the button is ENABLED
		if (!$(this).hasClass("disabled")) {
			GM_download2_Init();
			var $span = $("#downloadBtnInfo span:eq(0)");
			$(this).toggleState();
			download.getVid($span);
		}
	});

	// Toggle options on info click
	$(document).on("click", "#downloadBtnInfo", function() {
		$("#options").toggle();
	});

	// On individual option click
	$(document).on("click", "#options li", function() {
		// Close the options
		$("#options").hide();

		// Update the relevant settings
		localStorage.selQuality = Number($(this).attr("itag"));

		// Update the info
		display.updateInfo($(this));

		// Update the display
		display.update();
	});

	// Hide options on document click
	$(document).click(function(e) {
		// If it matches the info or is a child of the top info, ignore
		$el = $(e.target);
		$parent = $(e.target).parent();

		var str = $el.attr("id") + $parent.attr("id") + $parent.parent().attr("id");
		str = str || "";
		if (str.split("downloadBtnInfo").length > 1) {
			return;
		}

		// Hide the options
		$("#options").hide();
	});
};