Large

Yays! (Yet Another Youtube Script)

By eugenox Last update Mar 29, 2013 — Installed 40,386 times.

There are 28 previous versions of this script.

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

// ==UserScript==
// @name        Yays! (Yet Another Youtube Script)
// @namespace   youtube
// @description A lightweight and non-intrusive userscript that control autoplaying and set the preferred player size and playback quality on YouTube.
// @version     1.6.2
// @author      Eugene Nouvellieu <eugenox_gmail_com>
// @license     MIT License
// @include     http*://*.youtube.com/*
// @include     http*://youtube.com/*
// @run-at      document-end
// @noframes
// @grant       GM_deleteValue
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_xmlhttpRequest
// @homepageURL http://eugenox.appspot.com/script/yays
// @updateURL   https://eugenox.appspot.com/blob/yays/yays.meta.js
// @downloadURL https://eugenox.appspot.com/blob/yays/yays.user.js
// ==/UserScript==

// Copyright (c) 2012-2013 Eugene Nouvellieu <eugenox_gmail_com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

function YAYS(unsafeWindow) {

/*
 * Meta.
 */

var Meta = {
	title:       'Yays! (Yet Another Youtube Script)',
	version:     '1.6.2',
	releasedate: 'Mar 29, 2013',
	site:        'http://eugenox.appspot.com/script/yays',
	ns:          'yays'
};

/*
 * Script context.
 */

unsafeWindow[Meta.ns] = {};

/*
 * Utility functions.
 */

function each(iterable, callback, scope) {
	if (iterable.length) {
		for (var i = 0, len = iterable.length; i < len; ++i)
			callback.call(scope, i, iterable[i]);
	}
	else {
		for (var key in iterable)
			if (iterable.hasOwnProperty(key))
				callback.call(scope, key, iterable[key]);
	}
}

function map() {
	var
		args = Array.prototype.constructor.apply([], arguments),
		callback = args.shift() || bind(Array.prototype.constructor, []),
		buffer = [];

	if (args.length > 1) {
		var i = 0, len = Math.max.apply(Math, map(function(arg) { return arg.length; }, args));
		function getter(arg) { return arg[i]; }

		for (; i < len; ++i)
			buffer.push(callback.apply(null, map(getter, args)));
	}
	else {
		for (var i = 0, arg = args[0], len = arg.length; i < len; ++i)
			buffer.push(callback(arg[i]));
	}

	return buffer;
}

function combine(keys, values) {
	var object = {};
	map(function(key, value) { object[key] = value; }, keys, values);

	return object;
}

function bind(func, scope, args) {
	return function() {
		return func.apply(scope, typeof args == 'undefined' ? arguments : args);
	};
}

function merge(target, source, override) {
	override = override === undefined || override;

	for (var key in source) {
		if (override || ! target.hasOwnProperty(key))
			target[key] = source[key];
	}

	return target;
}

function extend(base, proto) {
	function T() {}
	T.prototype = base.prototype;

	return merge(new T(), proto);
}

function asyncCall(func, scope, args) {
	setTimeout(bind(func, scope, args), 0);
}

function asyncProxy(func) {
	return function() {
		asyncCall(func, this, arguments);
	};
}

function extendFn(func, extension) {
	if (! func)
		return extension;

	return function() {
		asyncCall(func, this, arguments);

		extension.apply(this, arguments);
	};
}

function emptyFn() { return; };

function buildURL(path, parameters) {
	var query = [];
	each(parameters, function(key, value) { query.push(key.concat('=', encodeURIComponent(value))); });

	return path.concat('?', query.join('&'));
}

function parseJSON(data) {
	if (typeof JSON != 'undefined')
		return JSON.parse(data);

	return eval('('.concat(data, ')'));
}

function debug() {
	unsafeWindow.console.debug.apply(unsafeWindow.console, Array.prototype.concat.apply(['['.concat(Meta.ns, ']')], arguments));
}

/*
 * i18n
 */

var _ = (function() {
	var
	vocabulary = ["Auto play", "ON", "OFF", "AUTO", "Toggle video autoplay", "Quality", "AUTO", "ORIGINAL", "Set default video quality", "Size", "AUTO", "WIDE", "FIT", "Set default player size", "Player settings", "Help"],
	dictionary = combine(vocabulary, (function() {
		switch ((document.documentElement.lang || 'en').substr(0, 2)) {
			// hungarian - eugenox
			case 'hu':
				return ["Automatikus lej\u00e1tsz\u00e1s", "BE", "KI", "AUTO", "Automatikus lej\u00e1tsz\u00e1s ki-be kapcsol\u00e1sa", "Min\u0151s\u00e9g", "AUTO", "EREDETI", "Vide\u00f3k alap\u00e9rtelmezett felbont\u00e1sa", "M\u00e9ret", "AUTO", "SZ\u00c9LES", "ILLESZTETT", "Lej\u00e1tsz\u00f3 alap\u00e9rtelmezett m\u00e9rete", "Lej\u00e1tsz\u00f3 be\u00e1ll\u00edt\u00e1sai", "S\u00fag\u00f3"];

			// dutch - Mike-RaWare
			case 'nl':
				return ["Auto spelen", "AAN", "UIT", "AUTOMATISCH", "Stel automatisch afspelen in", "Kwaliteit", "AUTOMATISCH", null, "Stel standaard videokwaliteit in", null, null, null, null, null, null, null];

			// spanish - yonane
			case 'es':
				return ["Reproducci\u00f3n Autom\u00e1tica", null, null, "AUTO", "Modificar Reproducci\u00f3n Autom\u00e1tica", "Calidad", "AUTO", null, "Usar calidad por defecto", null, null, null, null, null, null, null];

			// german - xemino
			case 'de':
				return ["Automatische Wiedergabe", "AN", "AUS", "AUTO", "Automatische Wiedergabe umschalten", "Qualit\u00e4t", "AUTO", null, "Standard Video Qualit\u00e4t setzen", null, null, null, null, null, null, null];

			// portuguese - Pitukinha
			case 'pt':
				return ["Reprodu\u00e7\u00e3o Autom\u00e1tica", "LIGADO", "DESLIGADO", "AUTOM\u00c1TICO", "Modificar Reprodu\u00e7\u00e3o Autom\u00e1tica", "Qualidade", "AUTOM\u00c1TICO", null, "Defini\u00e7\u00e3o padr\u00e3o de v\u00eddeo", null, null, null, null, null, "Configura\u00e7\u00e3o do usu\u00e1rio", null];

			// greek - TastyTeo
			case 'el':
				return ["\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae", "\u0395\u039d\u0395\u03a1\u0393\u039f", "\u0391\u039d\u0395\u039d\u0395\u03a1\u0393\u039f", "\u0391\u03a5\u03a4\u039f\u039c\u0391\u03a4\u039f", "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2", "\u03a0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1", "\u0391\u03a5\u03a4\u039f\u039c\u0391\u03a4\u039f", "\u03a0\u03a1\u039f\u0395\u03a0\u0399\u039b\u039f\u0393\u0397", "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b2\u03af\u03bd\u03c4\u03b5\u03bf", "\u039c\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2", "\u0391\u03a5\u03a4\u039f\u039c\u0391\u03a4\u039f", "\u03a0\u039b\u0391\u03a4\u03a5", "\u03a0\u03a1\u039f\u03a3\u0391\u03a1\u039c\u039f\u0393\u0397", "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ad\u03b1", "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ad\u03b1", "\u0392\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1"];

			// french - eXa
			case 'fr':
				return ["Lecture Auto", "ON", "OFF", "AUTO", "Lecture auto ON/OFF", "Qualit\u00e9", "AUTO", "ORIGINAL", "Qualit\u00e9 par d\u00e9faut", "Taille", "AUTO", "LARGE", "ADAPT\u00c9", "Taille par d\u00e9faut du lecteur", "Options du lecteur", "Aide"];

			// slovenian - Paranoia.Com
			case 'sl':
				return ["Samodejno predvajanje", "Vklju\u010di", "Izklju\u010di", "Samodejno", "Vklop/izklop samodejnega predvajanja", "Kakovost", "Samodejno", null, "Nastavi privzeto kakovost videa", null, null, null, null, null, "Nastavitve predvajalnika", "Pomo\u010d"];

			// russian - an1k3y
			case 'ru':
				return ["\u0410\u0432\u0442\u043e \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u0435", "\u0412\u041a\u041b", "\u0412\u042b\u041a\u041b", "\u0410\u0412\u0422\u041e", "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a \u0432\u0438\u0434\u0435\u043e", "\u041a\u0430\u0447\u0435\u0441\u0442\u0432\u043e", "\u0410\u0412\u0422\u041e", null, "\u041a\u0430\u0447\u0435\u0441\u0442\u0432\u043e \u0432\u0438\u0434\u0435\u043e \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", "\u0420\u0410\u0417\u041c\u0415\u0420", null, "\u0420\u0410\u0417\u0412\u0415\u0420\u041d\u0423\u0422\u042c", "\u0420\u0410\u0421\u0422\u042f\u041d\u0423\u0422\u042c", "\u0420\u0430\u0437\u043c\u0435\u0440 \u0432\u0438\u0434\u0435\u043e \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043b\u0435\u0435\u0440\u0430", "\u041f\u043e\u043c\u043e\u0449\u044c"];

			// hebrew - baryoni
			case 'iw':
				return ["\u05d4\u05e4\u05e2\u05dc\u05d4 \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9\u05ea", "\u05e4\u05e2\u05d9\u05dc", "\u05db\u05d1\u05d5\u05d9", "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9", "\u05e9\u05e0\u05d4 \u05de\u05e6\u05d1 \u05d4\u05e4\u05e2\u05dc\u05d4 \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9\u05ea \u05e9\u05dc \u05d4\u05d5\u05d9\u05d3\u05d0\u05d5", "\u05d0\u05d9\u05db\u05d5\u05ea", "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9\u05ea", null, "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05ea \u05d0\u05d9\u05db\u05d5\u05ea \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc \u05e9\u05dc \u05d4\u05d5\u05d9\u05d3\u05d0\u05d5", "\u05d2\u05d5\u05d3\u05dc", null, "\u05e8\u05d7\u05d1", "\u05de\u05dc\u05d0", "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05ea \u05d2\u05d5\u05d3\u05dc \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc \u05e9\u05dc \u05d4\u05e0\u05d2\u05df", "\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05e0\u05d2\u05df", "\u05e2\u05d6\u05e8\u05d4"];

			// chinese - blankhang
			case 'zh':
				return ["\u81ea\u52a8\u64ad\u653e", "\u5f00", "\u5173", "\u81ea\u52a8", "\u5207\u6362\u89c6\u9891\u81ea\u52a8\u64ad\u653e", "\u89c6\u9891\u8d28\u91cf", "\u81ea\u52a8", "\u539f\u753b", "\u8bbe\u7f6e\u9ed8\u8ba4\u89c6\u9891\u8d28\u91cf", "\u64ad\u653e\u5668\u5927\u5c0f", "\u81ea\u52a8", "\u5bbd\u5c4f", "\u81ea\u9002\u5e94", "\u8bbe\u7f6e\u64ad\u653e\u5668\u9ed8\u8ba4\u5927\u5c0f", "\u64ad\u653e\u5668\u8bbe\u7f6e", "\u5e2e\u52a9"];

			// polish - mkvs
			case 'pl':
				return ["Automatyczne odtwarzanie", "W\u0141\u0104CZONE", "WY\u0141ACZNONE", "AUTOMATYCZNE", "Ustaw automatyczne odtwarzanie film\u00f3w", "Jako\u015b\u0107", "AUTOMATYCZNA", "ORYGINALNA", "Ustaw domy\u015bln\u0105 jako\u015b\u0107 film\u00f3w", "Rozmiar", "AUTOMATYCZNY", "SZEROKI", "DOPASOWANY", "Ustaw domy\u015blny rozmiar odtwarzacza", "Ustawienia odtwarzacza", "Pomoc"];

			// swedish - eson
			case 'sv':
				return ["Automatisk uppspelning", "P\u00c5", "AV", "AUTO", "V\u00e4xla uppspelningsl\u00e4ge", "Kvalitet", "AUTO", "ORIGINAL", "Ange standardkvalitet", "Storlek", "AUTO", "BRED", "ANPASSAD", "Ange standardstorlek", "Inst\u00e4llningar", "Hj\u00e4lp"];
		}

		return [];
	})());

	return function(text) { return dictionary[text] || text; };
})();

/*
 * DOM Helper singleton.
 */

var DH = {
	ELEMENT_NODE: 1,

	build: function(def) {
		switch (Object.prototype.toString.call(def)) {
			case '[object Object]':
				def = merge({tag: 'div', style: null, attributes: null, listeners: null, children: null}, def);

				var node = this.createElement(def.tag);

				if (def.style !== null)
					this.style(node, def.style);

				if (def.attributes !== null)
					this.attributes(node, def.attributes);

				if (def.listeners !== null)
					this.listeners(node, def.listeners);

				if (def.children !== null)
					this.append(node, def.children);

				return node;

			case '[object String]':
				return this.createTextNode(def);

			default:
				return def;
		}
	},

	id: bind(unsafeWindow.document.getElementById, unsafeWindow.document),
	createElement: bind(unsafeWindow.document.createElement, unsafeWindow.document),
	createTextNode: bind(unsafeWindow.document.createTextNode, unsafeWindow.document),

	style: function(node, style) {
		each(style, node.style.setProperty, node.style);
	},

	append: function(node, children) {
		each([].concat(children), function(i, child) { node.appendChild(this.build(child)); }, this);
		node.normalize();
	},

	insertAfter: function(node, children) {
		var parent = node.parentNode, sibling = node.nextSibling;
		if (sibling) {
			each([].concat(children), function(i, child) { parent.insertBefore(this.build(child), sibling); }, this);
			parent.normalize();
		}
		else
			this.append(parent, children);
	},

	prepend: function(node, children) {
		if (node.hasChildNodes())
			each([].concat(children), function(i, child) { node.insertBefore(this.build(child), node.firstChild); }, this);
		else
			this.append(node, children);
	},

	attributes: function(node, attributes) {
		each(attributes, node.setAttribute, node);
	},

	hasClass: function(node, clss) {
		return 			node.hasAttribute('class') &&
			(clss instanceof RegExp ? clss.test(node.getAttribute('class')) : node.getAttribute('class').indexOf(clss) != -1);
	},

	addClass: function(node, clss) {
		if (clss.indexOf(' ') == -1) {
			if (! this.hasClass(node, clss))
				node.setAttribute('class', (node.getAttribute('class') || '').concat(' ', clss).trim());
		}
		else
			each(clss.split(/ +/), function(i, clss) { this.addClass(node, clss); }, this);
	},

	delClass: function(node, clss) {
		clss = clss.trim().replace(/ +/g, '|');

		if (this.hasClass(node, new RegExp(clss)))
			node.setAttribute('class', node.getAttribute('class').replace(new RegExp('\\s*'.concat(clss, '\\s*'), 'g'), ' ').trim());
	},

	listeners: function(node, listeners) {
		each(listeners, function(type, listener) { this.on(node, type, listener); }, this);
	},

	on: function(node, type, listener) {
		node.addEventListener(type, listener, false);
	},

	un: function(node, type, listener) {
		node.removeEventListener(type, listener, false);
	},

	unwrap: function(element) {
		if (typeof XPCNativeWrapper != 'undefined' && typeof XPCNativeWrapper.unwrap == 'function')
			return XPCNativeWrapper.unwrap(element);

		return element;
	},

	walk: function(node, path) {
		var steps = path.split('/'), step = null;

		while (node && (step = steps.shift())) {
			if (step == '..') {
				node = node.parentNode;
				continue;
			}

			var
				selector = /^(\w*)(?:\[(\d+)\])?$/.exec(step),
				name = selector[1],
				index = Number(selector[2]) || 0;

			for (var i = 0, j = 0, nodes = node.childNodes; node = nodes.item(i); ++i)
				if (node.nodeType == this.ELEMENT_NODE && (! name || node.tagName.toLowerCase() == name) && j++ == index)
					break;
		}

		return node;
	}
};

/*
 * Configuration handler singleton.
 */

var Config = (function(namespace) {
	// Greasemonkey compatible
	if (typeof GM_getValue == 'function') {
		return {
			get: GM_getValue,
			set: GM_setValue,
			del: GM_deleteValue
		};
	}

	var prefix = namespace + '.';

	// HTML5
	if (typeof unsafeWindow.localStorage != 'undefined') {
		return {
			get: function(key) {
				return unsafeWindow.localStorage.getItem(prefix + key);
			},

			set: function(key, value) {
				unsafeWindow.localStorage.setItem(prefix + key, value);
			},

			del: function(key) {
				unsafeWindow.localStorage.removeItem(prefix + key);
			}
		};
	}

	// Cookie
	return {
		get: function(key) {
			return (document.cookie.match(new RegExp('(?:^|; *)'.concat(prefix, key, '=(\\w+)(?:$|;)'))) || [, null])[1];
		},

		set: function(key, value) {
			document.cookie = prefix.concat(key, '=', value, '; path=/; expires=', new Date(new Date().valueOf() + 365 * 24 * 36e5).toUTCString());
		},

		del: function(key) {
			document.cookie = prefix.concat(key, '=deleted; path=/; max-age=0');
		}
	};
})(Meta.ns);

/*
 * Create XHR or JSONP requests.
 */

var JSONRequest = (function(namespace) {
	var Request = null;

	// XHR
	if (typeof GM_xmlhttpRequest == 'function') {
		Request = function(url, parameters, callback) {
			this._callback = callback;

			GM_xmlhttpRequest({
				method: 'GET',
				url: buildURL(url, parameters),
				onload: bind(this._onLoad, this)
			});
		};

		Request.prototype = {
			_onLoad: function(response) {
				this._callback(parseJSON(response.responseText));
			}
		};
	}
	// JSONP
	else {
		Request = (function() {
			var requests = [], requestsNs = 'jsonp';

			function Request(url, parameters, callback) {
				this._callback = callback;
				this._id = requests.push(bind(this._onLoad, this)) - 1;

				parameters.callback = namespace.concat('.', requestsNs, '[', this._id, ']');

				this._scriptNode = document.body.appendChild(DH.build({
					tag: 'script',
					attributes: {
						'type': 'text/javascript',
						'src': buildURL(url, parameters)
					}
				}));
			}

			Request.prototype = {
				_callback: null,
				_id: null,
				_scriptNode: null,

				_onLoad: function(response) {
					this._callback(response);

					document.body.removeChild(this._scriptNode);
					delete requests[this._id];
				}
			};

			unsafeWindow[namespace][requestsNs] = requests;

			return Request;
		})();
	}

	return Request;
})(Meta.ns);

/*
 * Update checker.
 */

(function() {
	if (new Date().valueOf() - Number(Config.get('update_checked_at')) < 24 * 36e5) // 1 day
		return;

	var popup = null;

	new JSONRequest(Meta.site + '/changelog', {version: Meta.version}, function(changelog) {
		Config.set('update_checked_at', new Date().valueOf().toFixed());

		if (changelog && changelog.length)
			popup = renderPopup(changelog);
	});

	function renderPopup(changelog) {
		return document.body.appendChild(DH.build({
			style: {
				'position': 'fixed',
				'top': '15px',
				'right': '15px',
				'z-index': '1000',
				'padding': '10px 15px 10px',
				'background-color': '#f8f8f8',
				'border': '1px solid #cccccc'
			},
			children: [{
				tag: 'h3',
				style: {
					'text-align': 'center',
				},
				children: Meta.title
			}, {
				style: {
					'font-size': '11px',
					'color': '#808080',
					'text-align': 'center'
				},
				children: 'User Script update notification.'
			}, {
				tag: 'p',
				style: {
					'margin': '10px 0'
				},
				children: [
					'You are using version ',
					{
						tag: 'strong',
						children: Meta.version
					},
					', released on ',
					{
						tag: 'em',
						children: Meta.releasedate
					},
					'.',
					{
						tag: 'br'
					},
					'Please consider updating to the latest release.'
				]
			}, {
				children: map(function(entry) {
					return {
						style: {
							'margin-bottom': '5px'
						},
						children: [{
							tag: 'strong',
							style: {
								'font-size': '11px'
							},
							children: entry.version
						}, {
							tag: 'em',
							style: {
								'margin-left': '5px'
							},
							children: entry.date
						}, {
							style: {
								'padding': '0 0 2px 10px',
								'white-space': 'pre'
							},
							children: [].concat(entry.note).join('\n')
						}]
					};
				}, [].concat(changelog))
			}, {
				style: {
					'text-align': 'center',
					'padding': '10px 0'
				},
				children: map(function(text, handler) {
					return DH.build({
						tag: 'span',
						attributes: {
							'class': 'yt-uix-button yt-uix-button-default'
						},
						style: {
							'margin': '0 5px',
							'padding': '5px 10px'
						},
						children: text,
						listeners: {
							'click': handler
						}
					});
				}, ['Update', 'Dismiss'], [openDownloadSite, removePopup])
			}]
		}));
	}

	function removePopup() {
		document.body.removeChild(popup);
	}

	function openDownloadSite() {
		removePopup();
		unsafeWindow.open(buildURL(Meta.site + '/download', {version: Meta.version}));
	}
})();

/*
 * Player singleton.
 */

var Player = (function() {
	function Player(element) {
		this._element = element;
		this._boot();
	}

	Player.prototype = {
		_element: null,
		_muted: 0,

		_onReady: emptyFn,

		_boot: function() {
			if (typeof this._element.getApiInterface == 'function') {
				this._exportApiInterface();
				this._onApiReady();
			}
			else
				setTimeout(bind(this._boot, this), 10);
		},

		_exportApiInterface: function() {
			each(this._element.getApiInterface(), function(i, method) {
				if (! Player.prototype.hasOwnProperty(method))
					this[method] = bind(this._element[method], this._element);
			}, this);
		},

		_onApiReady: function() {
			this._muted = Number(this.isMuted());

			// The player sometimes reports inconsistent state.
			if (this.isAutoPlaying())
				this.resetState();

			this._onReady(this);
			this._onReady = null;
		},

		onReady: function(callback) {
			if (this._onReady)
				this._onReady = callback;
			else
				callback(this);
		},

		getArgument: function(name) {
			// Flash
			if (this._element.hasAttribute('flashvars')) {
				var match = this._element.getAttribute('flashvars').match(new RegExp('(?:^|&)'.concat(name, '=(.+?)(?:&|$)')));
				if (match)
					return decodeURIComponent(match[1]);
			}
			// HTML5
			else {
				try {
					return unsafeWindow.ytplayer.config.args[name];
				}
				catch (e) {}
			}

			return;
		},

		isAutoPlaying: function() {
			return (this.getArgument('autoplay') || '1') == 1;
		},

		// Suppressing random exception.
		seekTo: function() {
			try {
				this._element.seekTo.apply(this._element, arguments);
			}
			catch (e) {}
		},

		// Seek to the beginning of the video considering deep-links.
		seekToStart: function(ahead) {
			var
				code = (location.hash + location.search).match(/\bt=(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/) || new Array(4),
				seconds = (Number(code[1]) || 0) * 3600 + (Number(code[2]) || 0) * 60 + (Number(code[3]) || 0);

			this.seekTo(seconds, ahead);
		},

		// This hack resets some aspects of the player.
		resetState: function() {
			this.seekTo(this.getCurrentTime(), true);
		},

		mute: function() {
			if (! this._muted++)
				this._element.mute();
		},

		unMute: function() {
			if (! --this._muted)
				this._element.unMute();
		}
	};

	var instance = {
		_element: null
	};

	return {
		UNSTARTED: -1,
		ENDED: 0,
		PLAYING: 1,
		PAUSED: 2,
		BUFFERING: 3,

		instance: function() {
			return instance;
		},

		initialize: function(element) {
			if (instance._element === element)
				throw 'Player already initialized';

			return instance = new Player(element);
		}
	};
})();

/*
 * Button class.
 */

function Button(labelText, tooltipText, callbacks) {
	var
		node = DH.build(this._def.node),
		label = DH.build(this._def.label),
		indicator = DH.build(this._def.indicator);

	DH.attributes(node, {title: tooltipText});
	DH.append(label, labelText);
	DH.append(node, [label, indicator]);

	DH.on(node, 'click', bind(this._onClick, this));

	this._node = node;
	this._indicator = indicator.firstChild;

	merge(this, callbacks);
}

Button.prototype = {
	_indicator: null,
	_node: null,

	_def: {
		node: {
			tag: 'button',
			style: {
				'margin': '2px'
			},
			attributes: {
				'type': 'button',
				'class': 'yt-uix-button yt-uix-button-default yt-uix-tooltip'
			}
		},

		label: {
			tag: 'span',
			attributes: {
				'class': 'yt-uix-button-content'
			}
		},

		indicator: {
			tag: 'span',
			style: {
				'font-size': '14px',
				'margin-left': '5px'
			},
			attributes: {
				'class': 'yt-uix-button-content'
			},
			children: '-'
		}
	},

	_onClick: function() {
		this.handler();
		this.refresh();
	},

	refresh: function() {
		this._indicator.data = this.display();
	},

	render: function() {
		this.refresh();
		return this._node;
	},

	handler: emptyFn,
	display: emptyFn
};

/*
 * PlayerOption class.
 */

function PlayerOption(key, overrides) {
	merge(this, overrides);

	this._key = key;
}

PlayerOption.prototype = {
	_player: null,
	_key: null,

	label: null,
	tooltip: null,
	states: [],

	_step: function() {
		this.set((this.get() + 1) % this.states.length);
	},

	_indicator: function() {
		return _(this.states[this.get()]);
	},

	get: function() {
		return Number(Config.get(this._key) || 0);
	},

	set: function(value) {
		Config.set(this._key, Number(value));
	},

	button: function(type) {
		return new type(_(this.label), _(this.tooltip), {
			handler: bind(this._step, this),
			display: bind(this._indicator, this)
		});
	},

	init: function(player) {
		this._player = player;

		this.configure();
	},

	configure: emptyFn,
	apply: emptyFn
};

/*
 * Prevent autoplaying.
 */

var AutoPlay = new PlayerOption('auto_play', {
	_applied: false,
	_focused: false,
	_muted: false,
	_player: null,
	_timer: null,

	label: 'Auto play',
	tooltip: 'Toggle video autoplay',
	states: ['ON', 'OFF', 'AUTO'],

	_onFocus: function() {
		if (this._applied && ! this._focused) {
			this._timer = setTimeout(bind(function() {
				this._player.resetState();
				this._player.playVideo();

				debug('Playback autostarted');

				this._focused = true;
				this._timer = null;
			}, this), 500);
		}
	},

	_onBlur: function() {
		if (this._timer !== null) {
			clearTimeout(this._timer);

			this._timer = null;
		}
	},

	// @see http://www.w3.org/TR/page-visibility/
	_isVisible: function() {
		var doc = unsafeWindow.document;
		return doc.hidden === false || doc.mozHidden === false || doc.webkitHidden === false;
	},

	configure: function() {
		if (this._player.isAutoPlaying()) {
			switch (this.get()) {
				case 0: // ON
					this._applied = true;
					break;

				case 1: // OFF
					this._applied = false;
					break;

				case 2: // AUTO
					// Video is visible or opened in the same window.
					if (this._focused || this._isVisible() || unsafeWindow.history.length > 1) {
						this._applied = true;
					}
					// Video is opened in a background tab.
					else {
						DH.on(unsafeWindow, 'focus', bind(this._onFocus, this));
						DH.on(unsafeWindow, 'blur', bind(this._onBlur, this));

						this._applied = false;
					}
					break;
			}
		}
		else
			this._applied = true;
	},

	apply: function() {
		if (! this._applied) {
			if (! this._muted) {
				this._player.mute();
				this._muted = true;
			}

			if (this._player.getPlayerState() == Player.PLAYING) {
				this._applied = true;

				this._player.seekToStart(true);
				this._player.pauseVideo();

				debug('Playback paused');

				this._player.unMute();
				this._muted = false;
			}
		}
		else {
			if (this._player.getPlayerState() == Player.PLAYING)
				this._focused = true;
		}
	}
});

/*
 * Set video quality.
 */

var VideoQuality = new PlayerOption('video_quality', {
	_applied: false,
	_muted: false,
	_buffered: false,
	_player: null,

	_qualities: [, 'small', 'medium', 'large', 'hd720', 'hd1080', 'highres'],

	label: 'Quality',
	tooltip: 'Set default video quality',
	states: ['AUTO', '240p', '360p', '480p', '720p', '1080p', 'ORIGINAL'],

	configure: function() {
		this._applied = ! this.get();
	},

	apply: function() {
		if (! this._applied) {
			if (! this._muted) {
				this._player.mute();
				this._muted = true;
			}

			if (this._player.getPlayerState() > Player.UNSTARTED) {
				if (this._player.getAvailableQualityLevels().length) {
					this._applied = true;

					var quality = this._qualities[this.get()];

					if (quality && quality != this._player.getPlaybackQuality()) {
						this._buffered = false;

						this._player.seekToStart(true);
						this._player.setPlaybackQuality(quality);

						debug('Quality changed to', quality);

						// Sometimes buffering event doesn't occur after the quality has changed.
						this.apply();

						return;
					}
				}
			}
			else
				return;

			this._player.unMute();
			this._muted = false;
		}
		else {
			switch (this._player.getPlayerState()) {
				case Player.BUFFERING:
					this._buffered = true;
					break;

				case Player.PLAYING:
					this._buffered = true;
					// no break;

				default:
					if (this._buffered && this._muted) {
						this._player.unMute();
						this._muted = false;
					}
			}
		}
	}
});

/*
 * Set player size.
 */

var PlayerSize = new PlayerOption('player_size', {
	label: 'Size',
	tooltip: 'Set default player size',
	states: ['AUTO', 'WIDE', 'FIT'],

	apply: function() {
		var mode = this.get();

		switch (mode) {
			case 2: // FIT
				DH.append(document.body, {
					tag: 'style',
					attributes: {
						'type': 'text/css'
					},
					children: [
						'.watch-medium .watch7-playlist-bar {',
							'width: 945px;',
						'}',
						'.watch-medium #watch7-playlist-tray-container {',
							'height: 533px;',
						'}',
						'.watch-medium.watch-playlist-collapsed #watch7-playlist-tray-container {',
							'height: 0;',
						'}',
						'.watch-medium #player-api {',
							'width: 945px;',
							'height: 560px;',
						'}'
					]
				});
				// no break;

			case 1: // WIDE
				DH.addClass(DH.id('watch7-container'), 'watch-wide watch-medium watch-playlist-collapsed');
				break;

			default:
				return;
		}

		debug('Size set to', ['wide', 'fit'][mode - 1]);
	}
});

/*
 * Abstract UI class.
 */

function UI(buttons) {
	this.buttons = buttons;
	this.button = DH.build(this._def.button(bind(this.toggle, this)));
	this.panel = DH.build(this._def.panel(buttons));
}

UI.setVisible = function(node, visible) {
	DH[visible ? 'delClass' : 'addClass'](node, 'hid');
	DH.style(node, {
		'display': visible ? 'block' : 'none'
	});
};

UI.prototype = {
	_def: {
		icon: {
			tag: 'img',
			attributes: {
				'src': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAA4ElEQVQoz32RMU4CQRhG38xqQ0e7CbHCnnxHEM/AEUiIthZegFAYErIhegTuwAWIGYiWWGKypY0bkgUZCxZ2JIuvmnkz8//fzECA2ppqqnbozJ8NOZfA2tVKZwE0lFcGbADwoExeo6KCujxTzb1LLBBxDgsRpK/xmtuK5Uf3BEZvNKgXakEHmNAq5t+sjHxw5tp9gJosT27xHxe8By0m2rc4kPFpAPTAoDJkHyJQj2Fl9Zv4K51Z4OdsgB1YcC8kQO4MOQSjsUvKb9pn2crLa1ua4zOnAMRzrlhxly4PBn4BWEpBljV5iJUAAAAASUVORK5CYII='}
		},

		button: function(click) {
			return {
				tag: 'span',
				children: {
					tag: 'button',
					attributes: {
						'type': 'button',
						'role': 'button',
						'class': 'action-panel-trigger yt-uix-button yt-uix-button-text yt-uix-button-empty yt-uix-tooltip',
						'data-button-toggle': 'true',
						'data-trigger-for': 'action-panel-yays',
						'data-tooltip-text': _('Player settings')
					},
					children: {
						tag: 'span',
						attributes: {
							'class': 'yt-uix-button-icon-wrapper'
						},
						children: [this.icon, {
							tag: 'span',
							attributes: {
								'class': 'yt-uix-button-valign'
							}
						}]
					},
					listeners: {
						'click': click
					}
				}
			};
		},

		panel: function(buttons) {
			return [{
				style: {
					'margin-bottom': '10px'
				},
				children: [{
					tag: 'strong',
					children: _('Player settings')
				}, {
					tag: 'a',
					attributes: {
						'href': Meta.site,
						'target': '_blank'
					},
					style: {
						'margin-left': '4px',
						'vertical-align': 'super',
						'font-size': '10px'
					},
					children: _('Help')
				}]
			}, {
				style: {
					'text-align': 'center',
				},
				children: map(bind(Button.prototype.render.call, Button.prototype.render), buttons)
			}];
		}
	},

	buttons: null,
	button: null,
	panel: null,

	refresh: function() {
		each(this.buttons, function(i, button) { button.refresh(); });
	},

	toggle: emptyFn
};

/*
 * WatchUI class.
 */

function WatchUI() {
	UI.call(this, [
		VideoQuality.button(Button),
		PlayerSize.button(Button),
		AutoPlay.button(Button)
	]);

	this.panel = DH.build({
		attributes: {
			'id': 'action-panel-yays',
			'class': 'action-panel-content hid',
			'data-panel-loaded': 'true'
		},
		style: {
			'display': 'none',
			'color': '#333'
		},
		children: this.panel
	});

	DH.append(DH.id('watch7-secondary-actions'), this.button);
	DH.prepend(DH.id('watch7-action-panels'), this.panel);

	PlayerSize.apply();
}

WatchUI.prototype = extend(UI, {
	toggle: function() {
		this.refresh();
	}
});

/*
 * ChannelUI class.
 */

function ChannelUI() {
	UI.call(this, [
		VideoQuality.button(Button),
		AutoPlay.button(Button)
	]);

	this.panel = DH.build({
		attributes: {
			'class': 'hid'
		},
		style: {
			'display': 'none',
			'margin-top': '7px'
		},
		children: this.panel
	});

	DH.append(DH.walk(DH.id('flag-video-panel'), '../h3/div'), [' ', this.button]);
	DH.insertAfter(DH.id('flag-video-panel'), this.panel);
}

ChannelUI.prototype = extend(UI, {
	_def: merge({
		button: function(click) {
			return {
				tag: 'button',
				style: {
					'padding': '0 4px'
				},
				attributes: {
					'type': 'button',
					'role': 'button',
					'class': 'yt-uix-button yt-uix-button-default yt-uix-tooltip yt-uix-tooltip-reverse yt-uix-button-empty',
					'title': _('Player settings')
				},
				children: this.icon,
				listeners: {
					'click': click
				}
			};
		}
	}, UI.prototype._def, false),

	toggle: function() {
		if (DH.hasClass(this.panel, 'hid')) {
			each(this.panel.parentNode.childNodes, function(i, node) {
				if (node.nodeType == DH.ELEMENT_NODE && node.tagName.toLowerCase() == 'div')
					UI.setVisible(node, false);
			});

			this.refresh();

			UI.setVisible(this.panel, true);
		}
		else
			UI.setVisible(this.panel, false);
	}
});


/*
 * Player state change callback.
 */

unsafeWindow[Meta.ns].onPlayerStateChange = asyncProxy(function(state) {
	debug('State changed to', ['unstarted', 'ended', 'playing', 'paused', 'buffering'][state + 1]);

	AutoPlay.apply();

	// Pausing playback doesn't have any effect if we rebuffer the video in the new quality level immediately.
	asyncCall(VideoQuality.apply, VideoQuality);
});

/*
 * Player ready callback.
 */

var onPlayerReady = asyncProxy(function() {
	var element = DH.id('movie_player') || DH.id('movie_player-flash') || DH.id('movie_player-html5');

	if (element) {
		try {
			Player.initialize(DH.unwrap(element)).onReady(function(player) {
				debug('Player ready');

				each([AutoPlay, VideoQuality, PlayerSize], function(i, option) {
					option.init(player);
				});

				AutoPlay.apply();
				VideoQuality.apply();

				player.addEventListener('onStateChange', Meta.ns + '.onPlayerStateChange');
			});
		}
		catch (e) {
			debug(e);
		}
	}
});

each(['onYouTubePlayerReady', 'ytPlayerOnYouTubePlayerReady'], function(i, callback) {
	unsafeWindow[callback] = extendFn(unsafeWindow[callback], onPlayerReady);
});

onPlayerReady();

var page = DH.id('page');
if (page) {
	if (DH.hasClass(page, 'watch'))
		new WatchUI();
	else if (DH.hasClass(page, 'channel'))
		new ChannelUI();
}

} // YAYS

if (window.top === window.self) {
	if (this['unsafeWindow']) { // Greasemonkey.
		YAYS(unsafeWindow);
	}
	else {
		var node = document.createElement('script');
		node.setAttribute('type', 'text/javascript');
		node.text = '('.concat(YAYS.toString(), ')(window);');

		document.body.appendChild(node);
		document.body.removeChild(node);
	}
}