Yays! (Yet Another Youtube Script)

A lightweight and non-intrusive userscript that control video playback and set the preferred player size and playback quality on YouTube.

  1. // ==UserScript==
  2. // @name Yays! (Yet Another Youtube Script)
  3. // @namespace youtube
  4. // @description A lightweight and non-intrusive userscript that control video playback and set the preferred player size and playback quality on YouTube.
  5. // @version 1.15.1
  6. // @author Eugene Nouvellieu <eugenox_gmail_com>
  7. // @license MIT License
  8. // @include http*://*.youtube.com/*
  9. // @include http*://youtube.com/*
  10. // @run-at document-end
  11. // @noframes
  12. // @grant unsafeWindow
  13. // @grant GM_deleteValue
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @grant GM_xmlhttpRequest
  17. // @homepageURL https://eugenox.appspot.com/script/yays
  18. // @icon https://eugenox.appspot.com/blob/yays/yays.icon.png
  19. // ==/UserScript==
  20.  
  21. // Copyright (c) 2012-2015 Eugene Nouvellieu <eugenox_gmail_com>
  22. //
  23. // Permission is hereby granted, free of charge, to any person obtaining a copy of
  24. // this software and associated documentation files (the "Software"), to deal in
  25. // the Software without restriction, including without limitation the rights to
  26. // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  27. // the Software, and to permit persons to whom the Software is furnished to do so,
  28. // subject to the following conditions:
  29. //
  30. // The above copyright notice and this permission notice shall be included in all
  31. // copies or substantial portions of the Software.
  32. //
  33. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  34. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  35. // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  36. // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  37. // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  38. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  39.  
  40. function YAYS(unsafeWindow) {
  41.  
  42. 'use strict';
  43.  
  44. /*
  45. * Meta.
  46. */
  47.  
  48. var Meta = {
  49. title: 'Yays! (Yet Another Youtube Script)',
  50. version: '1.15.1',
  51. releasedate: 'Aug 16, 2015',
  52. site: 'https://eugenox.appspot.com/script/yays',
  53. ns: 'yays'
  54. };
  55.  
  56. /*
  57. * Utility functions.
  58. */
  59.  
  60. function each(iterable, callback, scope) {
  61. if ('length' in iterable) {
  62. for (var i = 0, len = iterable.length; i < len; ++i) {
  63. callback.call(scope, i, iterable[i]);
  64. }
  65. }
  66. else {
  67. for (var key in iterable) {
  68. if (iterable.hasOwnProperty(key)) {
  69. callback.call(scope, key, iterable[key]);
  70. }
  71. }
  72. }
  73. }
  74.  
  75. function map() {
  76. var
  77. args = Array.prototype.constructor.apply([], arguments),
  78. callback = args.shift() || bind(Array.prototype.constructor, []),
  79. results = [],
  80. i, len;
  81.  
  82. if (args.length > 1) {
  83. var getter = function(arg) { return arg[i]; };
  84. len = Math.max.apply(Math, map(function(arg) { return arg.length; }, args));
  85.  
  86. for (i = 0; i < len; ++i) {
  87. results.push(callback.apply(null, map(getter, args)));
  88. }
  89. }
  90. else {
  91. var arg = args[0];
  92. len = arg.length;
  93.  
  94. for (i = 0; i < len; ++i) {
  95. results.push(callback(arg[i]));
  96. }
  97. }
  98.  
  99. return results;
  100. }
  101.  
  102. function unique(values) {
  103. values.sort();
  104.  
  105. for (var i = 0, j; i < values.length; ) {
  106. j = i;
  107.  
  108. while (values[j] === values[j - 1]) {
  109. j++;
  110. }
  111.  
  112. if (j - i) {
  113. values.splice(i, j - i);
  114. }
  115. else {
  116. ++i;
  117. }
  118. }
  119.  
  120. return values;
  121. }
  122.  
  123. function combine(keys, values) {
  124. var object = {};
  125.  
  126. map(function(key, value) { object[key] = value; }, keys, values);
  127.  
  128. return object;
  129. }
  130.  
  131. function merge(target, source, override) {
  132. override = override === undefined || override;
  133.  
  134. for (var key in source) {
  135. if (override || ! target.hasOwnProperty(key)) {
  136. target[key] = source[key];
  137. }
  138. }
  139.  
  140. return target;
  141. }
  142.  
  143. function extend(base, proto) {
  144. function T() {}
  145. T.prototype = base.prototype;
  146.  
  147. return merge(new T(), proto);
  148. }
  149.  
  150. function noop() {
  151. return;
  152. }
  153.  
  154. function bind(func, scope, args) {
  155. if (args && args.length > 0) {
  156. return func.bind.apply(func, [scope].concat(args));
  157. }
  158.  
  159. return func.bind(scope);
  160. }
  161.  
  162. function intercept(original, extension) {
  163. original = original || noop;
  164.  
  165. return function() {
  166. extension.apply(this, arguments);
  167.  
  168. return original.apply(this, arguments);
  169. };
  170. }
  171.  
  172. function asyncCall(func, scope, args) {
  173. window.setTimeout(bind(func, scope, args), 0);
  174. }
  175.  
  176. function asyncProxy(func) {
  177. return function() {
  178. asyncCall(func, this, Array.prototype.slice.call(arguments));
  179. };
  180. }
  181.  
  182. function buildURL(path, parameters) {
  183. var query = [];
  184. each(parameters, function(key, value) { query.push(key.concat('=', encodeURIComponent(value))); });
  185.  
  186. return path.concat('?', query.join('&'));
  187. }
  188.  
  189. function parseJSON(data) {
  190. if (typeof JSON != 'undefined') {
  191. return JSON.parse(data);
  192. }
  193.  
  194. return eval('(' + data + ')');
  195. }
  196.  
  197. /*
  198. * Script context.
  199. */
  200.  
  201. var Context = (function() {
  202. function BasicContext(context, namespace) {
  203. if (context) {
  204. this._scope = this._createNamespace(context._scope, namespace);
  205. this._ns = context._ns + '.' + namespace;
  206. }
  207. else {
  208. this._scope = unsafeWindow;
  209. this._ns = 'window';
  210. }
  211. }
  212.  
  213. BasicContext.prototype = {
  214. _scope: null,
  215. _ns: null,
  216.  
  217. _createNamespace: function(scope, name) {
  218. return scope[name] = {};
  219. },
  220.  
  221. protect: function(entity) {
  222. return entity;
  223. },
  224.  
  225. publish: function(name, entity) {
  226. this._scope[name] = this.protect(entity);
  227.  
  228. return this._ns + '.' + name;
  229. },
  230.  
  231. revoke: function(name) {
  232. delete this._scope[name];
  233. }
  234. };
  235.  
  236. var Context;
  237.  
  238. if (typeof exportFunction == 'function') {
  239. Context = function(context, namespace) {
  240. BasicContext.call(this, context, namespace);
  241. };
  242.  
  243. Context.prototype = extend(BasicContext, {
  244. _createNamespace: function(scope, name) {
  245. return createObjectIn(scope, {defineAs: name});
  246. },
  247.  
  248. protect: function(entity) {
  249. switch (typeof entity) {
  250. case 'function':
  251. return exportFunction(entity, this._scope);
  252.  
  253. case 'object':
  254. return cloneInto(entity, this._scope);
  255. }
  256. }
  257. });
  258. }
  259. else {
  260. Context = BasicContext;
  261. }
  262.  
  263. return Context;
  264. })();
  265.  
  266. var
  267. pageContext = new Context(),
  268. scriptContext = new Context(pageContext, Meta.ns);
  269.  
  270. /*
  271. * Console singleton.
  272. */
  273.  
  274. var Console = {
  275. debug: function() {
  276. unsafeWindow.console.debug('[' + Meta.ns + ']' + Array.prototype.join.call(arguments, ' '));
  277. }
  278. };
  279.  
  280. /*
  281. * DOM Helper singleton.
  282. */
  283.  
  284. var DH = {
  285. ELEMENT_NODE: 1,
  286.  
  287. build: function(def) {
  288. switch (Object.prototype.toString.call(def)) {
  289. case '[object Object]':
  290. var node = this.createElement(def.tag || 'div');
  291.  
  292. if ('style' in def) {
  293. this.style(node, def.style);
  294. }
  295.  
  296. if ('attributes' in def) {
  297. this.attributes(node, def.attributes);
  298. }
  299.  
  300. if ('listeners' in def) {
  301. this.listeners(node, def.listeners);
  302. }
  303.  
  304. if ('children' in def) {
  305. this.append(node, def.children);
  306. }
  307.  
  308. return node;
  309.  
  310. case '[object String]':
  311. return this.createTextNode(def);
  312.  
  313. default:
  314. return def;
  315. }
  316. },
  317.  
  318. id: bind(document.getElementById, document),
  319. query: bind(document.querySelectorAll, document),
  320. createElement: bind(document.createElement, document),
  321. createTextNode: bind(document.createTextNode, document),
  322.  
  323. style: function(node, style) {
  324. each(style, node.style.setProperty, node.style);
  325. },
  326.  
  327. append: function(node, children) {
  328. each([].concat(children), function(i, child) { node.appendChild(this.build(child)); }, this);
  329. node.normalize();
  330. },
  331.  
  332. insertAfter: function(node, children) {
  333. var parent = node.parentNode, sibling = node.nextSibling;
  334. if (sibling) {
  335. each([].concat(children), function(i, child) { parent.insertBefore(this.build(child), sibling); }, this);
  336. parent.normalize();
  337. }
  338. else {
  339. this.append(parent, children);
  340. }
  341. },
  342.  
  343. prepend: function(node, children) {
  344. if (node.hasChildNodes()) {
  345. each([].concat(children), function(i, child) { node.insertBefore(this.build(child), node.firstChild); }, this);
  346. }
  347. else {
  348. this.append(node, children);
  349. }
  350. },
  351.  
  352. remove: function(node) {
  353. node.parentNode.removeChild(node);
  354. },
  355.  
  356. attributes: function(node, attributes) {
  357. each(attributes, node.setAttribute, node);
  358. },
  359.  
  360. hasClass: function(node, cls) {
  361. return node.hasAttribute('class') && new RegExp('\\b' + cls + '\\b').test(node.getAttribute('class'));
  362. },
  363.  
  364. addClass: function(node, clss) {
  365. node.setAttribute('class', node.hasAttribute('class') ? unique(node.getAttribute('class').concat(' ', clss).trim().split(/ +/)).join(' ') : clss);
  366. },
  367.  
  368. delClass: function(node, clss) {
  369. if (node.hasAttribute('class')) {
  370. node.setAttribute('class', node.getAttribute('class').replace(new RegExp('\\s*\\b(?:' + clss.replace(/ +/g, '|') + ')\\b\\s*', 'g'), ' ').trim());
  371. }
  372. },
  373.  
  374. listeners: function(node, listeners) {
  375. each(listeners, function(type, listener) { this.on(node, type, listener); }, this);
  376. },
  377.  
  378. on: function(node, type, listener) {
  379. node.addEventListener(type, listener, false);
  380. },
  381.  
  382. un: function(node, type, listener) {
  383. node.removeEventListener(type, listener, false);
  384. },
  385.  
  386. unwrap: function(element) {
  387. try {
  388. return XPCNativeWrapper.unwrap(element);
  389. }
  390. catch (e) {
  391. return element;
  392. }
  393. },
  394.  
  395. walk: function(node, path) {
  396. var steps = path.split('/'), step = null;
  397.  
  398. while (node && (step = steps.shift())) {
  399. if (step == '..') {
  400. node = node.parentNode;
  401. continue;
  402. }
  403.  
  404. var selector = /^(\w*)(?:\[(\d+)\])?$/.exec(step), name = selector[1], index = Number(selector[2]) || 0;
  405.  
  406. for (var i = 0, j = 0, nodes = node.childNodes; node = nodes.item(i); ++i) {
  407. if (node.nodeType == this.ELEMENT_NODE && (! name || node.tagName.toLowerCase() == name) && j++ == index) {
  408. break;
  409. }
  410. }
  411. }
  412.  
  413. return node;
  414. },
  415.  
  416. closest: function(node, predicate) {
  417. do {
  418. if (predicate(node)) {
  419. return node;
  420. }
  421. } while ((node = node.parentNode) && node.nodeType == this.ELEMENT_NODE);
  422.  
  423. return null;
  424. }
  425. };
  426.  
  427. /*
  428. * i18n
  429. */
  430.  
  431. var _ = (function() {
  432. var vocabulary = ["Playback", "START", "PAUSE", "STOP", "AUTO PAUSE", "AUTO STOP", "Set default playback state", "Quality", "AUTO", "ORIGINAL", "Set default video quality", "Size", "AUTO", "WIDE", "Set default player size", "Player settings", "Help"];
  433. function translation(language) {
  434. switch (language) {
  435. // Hungarian - eugenox
  436. case 'hu':
  437. return ["Lej\u00e1tsz\u00e1s", "ELIND\u00cdTVA", "SZ\u00dcNETELTETVE", "MEG\u00c1LL\u00cdTVA", "AUTOMATIKUS SZ\u00dcNETELTET\u00c9S", "AUTOMATIKUS MEG\u00c1LL\u00cdT\u00c1S", "Lej\u00e1tsz\u00e1s alap\u00e9rtelmezett \u00e1llapota", "Min\u0151s\u00e9g", "AUTO", "EREDETI", "Vide\u00f3k alap\u00e9rtelmezett felbont\u00e1sa", "M\u00e9ret", "AUTO", "SZ\u00c9LES", "Lej\u00e1tsz\u00f3 alap\u00e9rtelmezett m\u00e9rete", "Lej\u00e1tsz\u00f3 be\u00e1ll\u00edt\u00e1sai", "S\u00fag\u00f3"];
  438. // Dutch - Mike-RaWare
  439. case 'nl':
  440. return [null, null, null, null, null, null, null, "Kwaliteit", "AUTOMATISCH", null, "Stel standaard videokwaliteit in", null, null, null, null, null, null];
  441. // Spanish - yonane, Dinepada, jdarlan
  442. case 'es':
  443. return ["Reproducci\u00f3n autom\u00e1tica", "Iniciar", "Pausar", "Detener", "Auto pausa", "Auto detener", "Fijar reproducci\u00f3n autom\u00e1tica", "Calidad", "Auto", "Original", "Calidad por defecto", "Tama\u00f1o", "Autom\u00e1tico", "Ancho", "Tama\u00f1o del reproductor predeterminado", "Configuraci\u00f3n", "Ayuda"];
  444. // German - xemino, ich01
  445. case 'de':
  446. return ["Wiedergabe", "Start", "Pause", "Stop", "Auto-Pause", "Auto-Stop", "Standardm\u00e4\u00dfigen Wiedergabezustand setzen", "Qualit\u00e4t", "Auto", "Original", "Standard Video Qualit\u00e4t setzen", "Gr\u00f6\u00dfe", "Auto", "Breit", "Standard Player Gr\u00f6\u00dfe setzen", "Einstellungen", "Hilfe"];
  447. // Portuguese - Pitukinha
  448. case 'pt':
  449. return [null, null, null, null, null, null, null, "Qualidade", "AUTOM\u00c1TICO", null, "Defini\u00e7\u00e3o padr\u00e3o de v\u00eddeo", null, null, null, null, "Configura\u00e7\u00e3o do usu\u00e1rio", null];
  450. // Greek - TastyTeo
  451. case 'el':
  452. return ["\u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae", null, "\u03a0\u0391\u03a5\u03a3\u0397", "\u03a3\u03a4\u0391\u039c\u0391\u03a4\u0397\u039c\u0391", "\u0391\u03a5\u03a4\u039f\u039c\u0391\u03a4\u0397 \u03a0\u0391\u03a5\u03a3\u0397", "\u0391\u03a5\u03a4\u039f\u039c\u0391\u03a4\u039f \u03a3\u03a4\u0391\u039c\u0391\u03a4\u0397\u039c\u0391", "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\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", "\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"];
  453. // French - eXa
  454. case 'fr':
  455. return [null, null, null, null, null, null, null, "Qualit\u00e9", "AUTO", "ORIGINAL", "Qualit\u00e9 par d\u00e9faut", "Taille", "AUTO", "LARGE", "Taille par d\u00e9faut du lecteur", "Options du lecteur", "Aide"];
  456. // Slovenian - Paranoia.Com
  457. case 'sl':
  458. return [null, null, null, null, null, null, null, "Kakovost", "Samodejno", null, "Nastavi privzeto kakovost videa", null, null, null, null, "Nastavitve predvajalnika", "Pomo\u010d"];
  459. // Russian - an1k3y
  460. case 'ru':
  461. return [null, null, null, null, null, null, null, "\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\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"];
  462. // Hebrew - baryoni
  463. case 'iw':
  464. return [null, null, null, null, null, null, null, "\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", "\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"];
  465. // Chinese - blankhang
  466. case 'zh':
  467. return ["\u64ad\u653e\u6a21\u5f0f", "\u64ad\u653e", "\u6682\u505c", "\u505c\u6b62", "\u81ea\u52a8\u6682\u505c", "\u81ea\u52a8\u505c\u6b62", "\u8bbe\u7f6e\u9ed8\u8ba4\u64ad\u653e\u6a21\u5f0f", "\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", "\u8bbe\u7f6e\u64ad\u653e\u5668\u9ed8\u8ba4\u5927\u5c0f", "\u64ad\u653e\u5668\u8bbe\u7f6e", "\u5e2e\u52a9"];
  468. // Polish - mkvs
  469. case 'pl':
  470. return ["Odtwarzanie", "URUCHOM", "WSTRZYMAJ", "ZATRZYMAJ", "AUTOMATYCZNIE WSTRZYMAJ", "AUTOMATYCZNIE ZATRZYMAJ", "Ustaw domy\u015blny stan odtwarzania", "Jako\u015b\u0107", "AUTOMATYCZNA", "ORYGINALNA", "Ustaw domy\u015bln\u0105 jako\u015b\u0107 film\u00f3w", "Rozmiar", "AUTOMATYCZNY", "SZEROKI", "Ustaw domy\u015blny rozmiar odtwarzacza", "Ustawienia odtwarzacza", "Pomoc"];
  471. // Swedish - eson
  472. case 'sv':
  473. return ["Uppspelning", "START", "PAUS", "STOPP", "AUTOPAUS", "AUTOSTOPP", "Ange uppspelningsl\u00e4ge", "Kvalitet", "AUTO", "ORIGINAL", "Ange standardkvalitet", "Storlek", "AUTO", "BRED", "Ange standardstorlek", "Inst\u00e4llningar", "Hj\u00e4lp"];
  474. // Ukrainian - mukolah
  475. case 'uk':
  476. return ["\u0421\u0442\u0430\u043d \u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447\u0430", "\u0412\u0406\u0414\u0422\u0412\u041e\u0420\u0418\u0422\u0418", "\u041f\u0420\u0418\u0417\u0423\u041f\u0418\u041d\u0418\u0422\u0418", "\u0417\u0423\u041f\u0418\u041d\u0418\u0422\u0418", "\u0410\u0412\u0422\u041e\u041c\u0410\u0422\u0418\u0427\u041d\u0415 \u041f\u0420\u0418\u0417\u0423\u041f\u0418\u041d\u0415\u041d\u041d\u042f", "\u0410\u0412\u0422\u041e\u041c\u0410\u0422\u0418\u0427\u041d\u0415 \u0417\u0423\u041f\u0418\u041d\u0415\u041d\u041d\u042f", "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0438\u0439 \u0441\u0442\u0430\u043d \u0432\u0456\u0434\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f", "\u042f\u043a\u0456\u0441\u0442\u044c", "\u0410\u0412\u0422\u041e\u041c\u0410\u0422\u0418\u0427\u041d\u041e", "\u041e\u0420\u0418\u0413\u0406\u041d\u0410\u041b\u042c\u041d\u0410", "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0443 \u044f\u043a\u0456\u0441\u0442\u044c \u0432\u0456\u0434\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f", "\u0420\u043e\u0437\u043c\u0456\u0440", "\u0410\u0412\u0422\u041e\u041c\u0410\u0422\u0418\u0427\u041d\u0418\u0419", "\u0428\u0418\u0420\u041e\u041a\u0418\u0419", "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0438\u0439 \u0440\u043e\u0437\u043c\u0456\u0440 \u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447\u0430", "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447\u0430", "\u0414\u043e\u043f\u043e\u043c\u043e\u0433\u0430"];
  477. }
  478.  
  479. return [];
  480. }
  481.  
  482. var dictionary = combine(vocabulary, translation((document.documentElement.lang || 'en').substr(0, 2)));
  483.  
  484. return function(text) {
  485. return dictionary[text] || text;
  486. };
  487. })();
  488.  
  489. /**
  490. * @class ScopedStorage
  491. */
  492. function ScopedStorage(scope, namespace) {
  493. this._scope = scope;
  494. this._ns = namespace;
  495. }
  496.  
  497. ScopedStorage.prototype = {
  498. _scope: null,
  499. _ns: null,
  500.  
  501. getItem: function(key) {
  502. return this._scope.getItem(this._ns + '.' + key);
  503. },
  504.  
  505. setItem: function(key, value) {
  506. this._scope.setItem(this._ns + '.' + key, value);
  507. },
  508.  
  509. removeItem: function(key) {
  510. this._scope.removeItem(this._ns + '.' + key);
  511. }
  512. };
  513.  
  514. var scriptStorage = new ScopedStorage(unsafeWindow.localStorage, Meta.ns);
  515.  
  516. /*
  517. * Configuration handler singleton.
  518. */
  519.  
  520. var Config = (function() {
  521. // Greasemonkey compatible
  522. if (typeof GM_getValue == 'function') {
  523. return {
  524. get: GM_getValue,
  525. set: GM_setValue,
  526. del: GM_deleteValue
  527. };
  528. }
  529.  
  530. var configStorage = new ScopedStorage(scriptStorage, 'config');
  531.  
  532. // HTML5
  533. return {
  534. get: function(key) {
  535. return configStorage.getItem(key);
  536. },
  537.  
  538. set: function(key, value) {
  539. configStorage.setItem(key, value);
  540. },
  541.  
  542. del: function(key) {
  543. configStorage.removeItem(key);
  544. }
  545. };
  546. })();
  547.  
  548. /**
  549. * @class JSONRequest
  550. * Create XHR or JSONP requests.
  551. */
  552. var JSONRequest = (function() {
  553. var Request = null;
  554.  
  555. // XHR
  556. if (typeof GM_xmlhttpRequest == 'function') {
  557. Request = function(url, parameters, callback) {
  558. this._callback = callback;
  559.  
  560. GM_xmlhttpRequest({
  561. method: 'GET',
  562. url: buildURL(url, parameters),
  563. onload: bind(this._onLoad, this)
  564. });
  565. };
  566.  
  567. Request.prototype = {
  568. _onLoad: function(response) {
  569. this._callback(parseJSON(response.responseText));
  570. }
  571. };
  572. }
  573. // JSONP
  574. else {
  575. Request = function(url, parameters, callback) {
  576. this._callback = callback;
  577. this._id = 'jsonp_' + Request.counter++;
  578.  
  579. parameters.callback = scriptContext.publish(this._id, bind(this._onLoad, this));
  580.  
  581. this._scriptNode = document.body.appendChild(DH.build({
  582. tag: 'script',
  583. attributes: {
  584. 'type': 'text/javascript',
  585. 'src': buildURL(url, parameters)
  586. }
  587. }));
  588. };
  589.  
  590. Request.counter = 0;
  591.  
  592. Request.prototype = {
  593. _callback: null,
  594. _id: null,
  595. _scriptNode: null,
  596.  
  597. _onLoad: function(response) {
  598. this._callback(response);
  599.  
  600. scriptContext.revoke(this._id);
  601.  
  602. document.body.removeChild(this._scriptNode);
  603. }
  604. };
  605. }
  606.  
  607. return Request;
  608. })();
  609.  
  610. /*
  611. * Update checker.
  612. */
  613.  
  614. (function() {
  615. if (new Date().valueOf() - Number(scriptStorage.getItem('update_checked_at')) < 86400000) { // 1 day
  616. return;
  617. }
  618.  
  619. var popup = null;
  620.  
  621. new JSONRequest(Meta.site + '/changelog', {version: Meta.version}, function(changelog) {
  622. scriptStorage.setItem('update_checked_at', new Date().valueOf().toFixed());
  623.  
  624. if (changelog && changelog.length) {
  625. popup = renderPopup(changelog);
  626. }
  627. });
  628.  
  629. function renderPopup(changelog) {
  630. return document.body.appendChild(DH.build({
  631. style: {
  632. 'position': 'fixed',
  633. 'bottom': '0',
  634. 'width': '100%',
  635. 'z-index': '1000',
  636. 'background-color': '#f1f1f1',
  637. 'border-top': '1px solid #cccccc'
  638. },
  639. children: {
  640. style: {
  641. 'margin': '15px'
  642. },
  643. children: [{
  644. tag: 'strong',
  645. children: ['There is an update available for ', Meta.title, '.']
  646. }, {
  647. tag: 'p',
  648. style: {
  649. 'margin': '10px 0'
  650. },
  651. children: [
  652. 'You are using version ', {
  653. tag: 'strong',
  654. children: Meta.version
  655. }, ', released on ', {
  656. tag: 'em',
  657. children: Meta.releasedate
  658. }, '. Please consider updating to the latest version.'
  659. ]
  660. }, {
  661. style: {
  662. 'margin': '10px 0',
  663. 'max-height': '150px',
  664. 'overflow-y': 'auto'
  665. },
  666. children: {
  667. tag: 'a',
  668. children: 'Show changes',
  669. listeners: {
  670. click: function(e) {
  671. e.preventDefault();
  672.  
  673. DH.insertAfter(e.target, map(function(entry) {
  674. return {
  675. style: {
  676. 'margin-bottom': '5px'
  677. },
  678. children: [{
  679. tag: 'strong',
  680. children: entry.version
  681. }, ' ', {
  682. tag: 'em',
  683. children: ['(', entry.date, ')']
  684. }, {
  685. style: {
  686. 'padding': '0 0 2px 10px',
  687. 'white-space': 'pre'
  688. },
  689. children: [].concat(entry.note).join('\n')
  690. }]
  691. };
  692. }, [].concat(changelog)));
  693.  
  694. DH.remove(e.target);
  695. }
  696. }
  697. }
  698. }, {
  699. children: map(function(text, handler) {
  700. return DH.build({
  701. tag: 'button',
  702. attributes: {
  703. 'type': 'button',
  704. 'class': 'yt-uix-button yt-uix-button-default'
  705. },
  706. style: {
  707. 'margin-right': '10px',
  708. 'padding': '5px 15px'
  709. },
  710. children: text,
  711. listeners: {
  712. 'click': handler
  713. }
  714. });
  715. }, ['Update', 'Dismiss'], [openDownloadSite, removePopup])
  716. }]
  717. }
  718. }));
  719. }
  720.  
  721. function removePopup() {
  722. document.body.removeChild(popup);
  723. }
  724.  
  725. function openDownloadSite() {
  726. removePopup();
  727. unsafeWindow.open(buildURL(Meta.site + '/download', {version: Meta.version}));
  728. }
  729. })();
  730.  
  731. /*
  732. * Migrations.
  733. */
  734.  
  735. (function(currentVersion) {
  736. var previousVersion = Config.get('version') || scriptStorage.getItem('version') || '1.0';
  737.  
  738. if (previousVersion == currentVersion) {
  739. return;
  740. }
  741.  
  742. previousVersion = map(Number, previousVersion.split('.'));
  743.  
  744. each([
  745. {
  746. // Added "144p" to the quality levels.
  747. version: '1.7', apply: function() {
  748. var videoQuality = Number(Config.get('video_quality'));
  749. if (videoQuality > 0 && videoQuality < 7) {
  750. Config.set('video_quality', ++videoQuality);
  751. }
  752. }
  753. },
  754. {
  755. // Autoplay reworked.
  756. version: '1.8', apply: function() {
  757. switch (Number(Config.get('auto_play'))) {
  758. case 1: // OFF > PAUSE
  759. Config.set('video_playback', 1);
  760. break;
  761.  
  762. case 2: // AUTO > AUTO PAUSE
  763. Config.set('video_playback', 3);
  764. break;
  765. }
  766.  
  767. Config.del('auto_play');
  768. }
  769. },
  770. {
  771. // Added "1440p" to the quality levels.
  772. version: '1.10', apply: function() {
  773. var videoQuality = Number(Config.get('video_quality'));
  774. if (videoQuality > 6) {
  775. Config.set('video_quality', ++videoQuality);
  776. }
  777. }
  778. },
  779. {
  780. // Introduced the ScopedStorage class.
  781. version: '1.14', apply: function() {
  782. // Using a unique ScopedStorage for config outside of GM.
  783. each(['video_playback', 'video_quality', 'player_size', 'version'], function(i, key) {
  784. var value = scriptStorage.getItem(key);
  785.  
  786. if (value) {
  787. Config.set(key, value);
  788.  
  789. scriptStorage.removeItem(key);
  790. }
  791. });
  792.  
  793. // Removed from the config.
  794. Config.del('update_checked_at');
  795. }
  796. },
  797. {
  798. // Removed "FIT" from player sizes.
  799. version: '1.15', apply: function() {
  800. if (Number(Config.get('player_size')) == 2) {
  801. Config.set('player_size', 1);
  802. }
  803. }
  804. }
  805. ], function(i, migration) {
  806. var migrationVersion = map(Number, migration.version.split('.'));
  807.  
  808. for (var j = 0, parts = Math.max(previousVersion.length, migrationVersion.length); j < parts; ++j) {
  809. if ((previousVersion[j] || 0) < (migrationVersion[j] || 0)) {
  810. Console.debug('Applying migration', migration.version);
  811.  
  812. migration.apply();
  813.  
  814. break;
  815. }
  816. }
  817. });
  818.  
  819. Config.set('version', currentVersion);
  820. })(Meta.version);
  821.  
  822. /**
  823. * @class Player
  824. */
  825. function Player(element) {
  826. this._element = element;
  827. this._context = new Context(scriptContext, 'player' + Player._elements.indexOf(element));
  828.  
  829. this._exportApiInterface();
  830.  
  831. Console.debug('Player ready');
  832.  
  833. this._muted = Number(this.isMuted() && ! Number(Player._storage.getItem('muted')));
  834.  
  835. this._addStateChangeListener();
  836. }
  837.  
  838. merge(Player, {
  839. UNSTARTED: -1,
  840. ENDED: 0,
  841. PLAYING: 1,
  842. PAUSED: 2,
  843. BUFFERING: 3,
  844. CUED: 5,
  845.  
  846. _elements: [],
  847.  
  848. _storage: new ScopedStorage(scriptStorage, 'player'),
  849.  
  850. test: function(element) {
  851. return typeof element.getApiInterface == 'function';
  852. },
  853.  
  854. create: function(element) {
  855. switch (element.tagName) {
  856. case 'EMBED':
  857. return new FlashPlayer(element);
  858. case 'DIV':
  859. return new HTML5Player(element);
  860. }
  861.  
  862. throw 'Unknown player type';
  863. },
  864.  
  865. initialize: function(element) {
  866. if (this._elements.indexOf(element) > -1) {
  867. throw 'Player already initialized';
  868. }
  869.  
  870. var index = this._elements.indexOf(null);
  871.  
  872. if (index > -1) {
  873. this._elements[index] = element;
  874. }
  875. else {
  876. this._elements.push(element);
  877. }
  878.  
  879. return this.create(element);
  880. },
  881.  
  882. invalidate: function(player) {
  883. this._elements[this._elements.indexOf(player._element)] = null;
  884.  
  885. player.invalidate();
  886. }
  887. });
  888.  
  889. Player.prototype = {
  890. _element: null,
  891. _context: null,
  892. _muted: 0,
  893.  
  894. _exportApiInterface: function() {
  895. each(this._element.getApiInterface(), function(i, method) {
  896. if (! (method in this)) {
  897. this[method] = bind(this._element[method], this._element);
  898. }
  899. }, this);
  900. },
  901.  
  902. _unexportApiInterface: function() {
  903. each(this._element.getApiInterface(), function(i, method) {
  904. if (this.hasOwnProperty(method)) {
  905. delete this[method];
  906. }
  907. }, this);
  908. },
  909.  
  910. _onStateChange: function(state) {
  911. Console.debug('State changed to', ['unstarted', 'ended', 'playing', 'paused', 'buffering', undefined, 'cued'][state + 1]);
  912.  
  913. this.onStateChange(state);
  914. },
  915.  
  916. _addStateChangeListener: function() {
  917. this.addEventListener('onStateChange', this._context.publish('onPlayerStateChange', asyncProxy(bind(this._onStateChange, this))));
  918. },
  919.  
  920. _removeStateChangeListener: function() {
  921. this.removeEventListener('onStateChange', this._context.publish('onPlayerStateChange', noop));
  922. },
  923.  
  924. invalidate: function() {
  925. this._removeStateChangeListener();
  926. this._unexportApiInterface();
  927.  
  928. delete this.onStateChange;
  929. delete this._element;
  930.  
  931. Console.debug('Player invalidated');
  932.  
  933. this.invalidate = noop;
  934. },
  935.  
  936. onStateChange: noop,
  937.  
  938. isPlayerState: function() {
  939. return Array.prototype.indexOf.call(arguments, this.getPlayerState()) > -1;
  940. },
  941.  
  942. getVideoId: function() {
  943. try {
  944. return this.getVideoData().video_id;
  945. }
  946. catch (e) {
  947. return (this.getVideoUrl().match(/\bv=([\w-]+)/) || [, undefined])[1];
  948. }
  949. },
  950.  
  951. restartPlayback: function() {
  952. if (this.getCurrentTime() > 60) {
  953. Console.debug('Restart threshold exceeded');
  954.  
  955. return;
  956. }
  957.  
  958. var
  959. code = (location.hash + location.search).match(/\bt=(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?/) || new Array(4),
  960. seconds = (Number(code[1]) || 0) * 3600 + (Number(code[2]) || 0) * 60 + (Number(code[3]) || 0);
  961.  
  962. this.seekTo(seconds, true);
  963. },
  964.  
  965. resetState: function() {
  966. this.seekTo(this.getCurrentTime(), true);
  967. },
  968.  
  969. mute: function() {
  970. if (! this._muted++) {
  971. this._element.mute();
  972.  
  973. Player._storage.setItem('muted', '1');
  974.  
  975. Console.debug('Player muted');
  976. }
  977. },
  978.  
  979. unMute: function() {
  980. if (! --this._muted) {
  981. this._element.unMute();
  982.  
  983. Player._storage.setItem('muted', '0');
  984.  
  985. Console.debug('Player unmuted');
  986. }
  987. },
  988.  
  989. playVideo: function() {
  990. this._element.playVideo();
  991.  
  992. Console.debug('Playback started');
  993. },
  994.  
  995. pauseVideo: function() {
  996. this._element.pauseVideo();
  997.  
  998. Console.debug('Playback paused');
  999. },
  1000.  
  1001. stopVideo: function() {
  1002. this._element.stopVideo();
  1003.  
  1004. Console.debug('Playback stopped');
  1005. },
  1006.  
  1007. setPlaybackQuality: function(quality) {
  1008. this._element.setPlaybackQuality(quality);
  1009.  
  1010. Console.debug('Quality changed to', quality);
  1011. },
  1012.  
  1013. setStageMode: noop
  1014. };
  1015.  
  1016. /**
  1017. * @class FlashPlayer
  1018. */
  1019. function FlashPlayer(element) {
  1020. Player.call(this, element);
  1021. }
  1022.  
  1023. FlashPlayer.prototype = extend(Player, {
  1024. _exportApiInterface: function() {
  1025. try {
  1026. Player.prototype._exportApiInterface.call(this);
  1027. }
  1028. catch (e) {
  1029. throw 'Player has not loaded yet';
  1030. }
  1031. },
  1032.  
  1033. _unexportApiInterface: function() {
  1034. try {
  1035. Player.prototype._unexportApiInterface.call(this);
  1036. }
  1037. catch (e) {
  1038. Console.debug('Player has unloaded');
  1039. }
  1040. },
  1041.  
  1042. _removeStateChangeListener: function() {
  1043. try {
  1044. Player.prototype._removeStateChangeListener.call(this);
  1045. }
  1046. catch (e) {
  1047. Console.debug('Player has unloaded');
  1048. }
  1049. }
  1050. });
  1051.  
  1052. /**
  1053. * @class HTML5Player
  1054. */
  1055. function HTML5Player(element) {
  1056. Player.call(this, element);
  1057. }
  1058.  
  1059. HTML5Player.prototype = extend(Player, {
  1060. restartPlayback: function() {
  1061. Player.prototype.restartPlayback.call(this);
  1062.  
  1063. this.restartPlayback = noop;
  1064. },
  1065.  
  1066. setStageMode: function(enable) {
  1067. this.setSizeStyle(true, enable);
  1068. }
  1069. });
  1070.  
  1071. /**
  1072. * @class Button
  1073. */
  1074. function Button(label, tooltip) {
  1075. this._node = DH.build(this._def(tooltip, label, this._indicator = DH.build('-')));
  1076. }
  1077.  
  1078. Button.prototype = {
  1079. _indicator: null,
  1080. _node: null,
  1081.  
  1082. _def: function(tooltip, label, indicator) {
  1083. return {
  1084. tag: 'button',
  1085. attributes: {
  1086. 'type': 'button',
  1087. 'class': 'yt-uix-button yt-uix-button-default yt-uix-tooltip',
  1088. 'title': tooltip
  1089. },
  1090. listeners: {
  1091. 'click': bind(this._onClick, this)
  1092. },
  1093. style: {
  1094. 'margin': '0 0.5%'
  1095. },
  1096. children: [{
  1097. tag: 'span',
  1098. attributes: {
  1099. 'class': 'yt-uix-button-content'
  1100. },
  1101. children: label
  1102. }, {
  1103. tag: 'span',
  1104. style: {
  1105. 'font-size': '14px',
  1106. 'margin-left': '5px'
  1107. },
  1108. attributes: {
  1109. 'class': 'yt-uix-button-content'
  1110. },
  1111. children: indicator
  1112. }]
  1113. };
  1114. },
  1115.  
  1116. _onClick: function() {
  1117. this.handler();
  1118. this.refresh();
  1119. },
  1120.  
  1121. refresh: function() {
  1122. this._indicator.data = this.display();
  1123. },
  1124.  
  1125. render: function() {
  1126. this.refresh();
  1127. return this._node;
  1128. },
  1129.  
  1130. handler: noop,
  1131. display: noop
  1132. };
  1133.  
  1134. /**
  1135. * @class PlayerOption
  1136. */
  1137. function PlayerOption(player, key) {
  1138. this._player = player;
  1139. this._key = key;
  1140. }
  1141.  
  1142. PlayerOption.prototype = {
  1143. _player: null,
  1144. _key: null,
  1145.  
  1146. get: function() {
  1147. return Number(Config.get(this._key) || '0');
  1148. },
  1149.  
  1150. set: function(value) {
  1151. Config.set(this._key, Number(value));
  1152. },
  1153.  
  1154. apply: noop,
  1155. cease: noop
  1156. };
  1157.  
  1158. /**
  1159. * @class PlayerOption.Button
  1160. */
  1161. PlayerOption.Button = function(option) {
  1162. Button.call(this, this.label, this.tooltip);
  1163.  
  1164. this._option = option;
  1165. };
  1166.  
  1167. PlayerOption.Button.extend = function(attributes) {
  1168. var superclass = this;
  1169.  
  1170. function Button(option) {
  1171. superclass.call(this, option);
  1172. }
  1173.  
  1174. Button.prototype = extend(superclass, attributes);
  1175.  
  1176. return Button;
  1177. };
  1178.  
  1179. PlayerOption.Button.prototype = extend(Button, {
  1180. _option: null,
  1181.  
  1182. label: null,
  1183. tooltip: null,
  1184. states: null,
  1185.  
  1186. handler: function() {
  1187. this._option.set((this._option.get() + 1) % this.states.length);
  1188. },
  1189.  
  1190. display: function() {
  1191. return this.states[this._option.get()];
  1192. }
  1193. });
  1194.  
  1195. /**
  1196. * @class SilentPlayerOption
  1197. */
  1198. function SilentPlayerOption(player, key) {
  1199. PlayerOption.call(this, player, key);
  1200. }
  1201.  
  1202. SilentPlayerOption.prototype = extend(PlayerOption, {
  1203. _muted: false,
  1204.  
  1205. mute: function(state) {
  1206. if (this._muted != state) {
  1207. if (state) {
  1208. this._player.mute();
  1209. }
  1210. else {
  1211. this._player.unMute();
  1212. }
  1213.  
  1214. this._muted = state;
  1215. }
  1216. },
  1217.  
  1218. cease: function() {
  1219. this.mute(false);
  1220. }
  1221. });
  1222.  
  1223. /**
  1224. * @class VideoPlayback
  1225. */
  1226. function VideoPlayback(player) {
  1227. SilentPlayerOption.call(this, player, 'video_playback');
  1228.  
  1229. switch (this.get()) {
  1230. case 0: // PLAY
  1231. this._applied = true;
  1232. break;
  1233.  
  1234. case 3: // AUTO PAUSE
  1235. case 4: // AUTO STOP
  1236. // Video is visible or opened in the same window.
  1237. if (this._isVisible() || unsafeWindow.history.length > 1) {
  1238. this._applied = true;
  1239. }
  1240. // Video is opened in a background tab.
  1241. else {
  1242. this._handler = pageContext.protect(bind(this._handler, this));
  1243.  
  1244. DH.on(unsafeWindow, 'focus', this._handler);
  1245. DH.on(unsafeWindow, 'blur', this._handler);
  1246. }
  1247. break;
  1248. }
  1249. }
  1250.  
  1251. VideoPlayback.prototype = extend(SilentPlayerOption, {
  1252. _applied: false,
  1253. _timer: null,
  1254.  
  1255. _handler: function(e) {
  1256. switch (e.type) {
  1257. case 'focus':
  1258. if (this._timer === null) {
  1259. this._timer = window.setTimeout(bind(function() {
  1260. if (this._applied) {
  1261. this._player.resetState();
  1262. this._player.playVideo();
  1263.  
  1264. Console.debug('Playback autostarted');
  1265. }
  1266. else {
  1267. this._applied = true;
  1268.  
  1269. Console.debug('Playback not affected');
  1270.  
  1271. this.mute(false);
  1272. }
  1273.  
  1274. DH.un(unsafeWindow, 'focus', this._handler);
  1275. DH.un(unsafeWindow, 'blur', this._handler);
  1276.  
  1277. this._timer = null;
  1278. }, this), 500);
  1279. }
  1280. break;
  1281.  
  1282. case 'blur':
  1283. if (this._timer !== null) {
  1284. clearTimeout(this._timer);
  1285.  
  1286. this._timer = null;
  1287. }
  1288. break;
  1289. }
  1290. },
  1291.  
  1292. // @see http://www.w3.org/TR/page-visibility/
  1293. _isVisible: function() {
  1294. var doc = unsafeWindow.document;
  1295. return doc.hidden === false || doc.mozHidden === false || doc.webkitHidden === false;
  1296. },
  1297.  
  1298. apply: function() {
  1299. if (! this._applied) {
  1300. this.mute(true);
  1301.  
  1302. if (this._player.isPlayerState(Player.PLAYING)) {
  1303. this._applied = true;
  1304.  
  1305. this._player.restartPlayback();
  1306.  
  1307. if (this.get() % 2) { // (AUTO) PAUSE
  1308. this._player.pauseVideo();
  1309. }
  1310. else { // (AUTO) STOP
  1311. this._player.stopVideo();
  1312. }
  1313.  
  1314. this.mute(false);
  1315. }
  1316. }
  1317. }
  1318. });
  1319.  
  1320. /**
  1321. * @class VideoPlayback.Button
  1322. */
  1323. VideoPlayback.Button = PlayerOption.Button.extend({
  1324. label: _('Playback'),
  1325. tooltip: _('Set default playback state'),
  1326. states: [_('START'), _('PAUSE'), _('STOP'), _('AUTO PAUSE'), _('AUTO STOP')]
  1327. });
  1328.  
  1329. /**
  1330. * @class VideoQuality
  1331. */
  1332. function VideoQuality(player) {
  1333. SilentPlayerOption.call(this, player, 'video_quality');
  1334.  
  1335. this._applied = ! this.get();
  1336. }
  1337.  
  1338. VideoQuality.prototype = extend(SilentPlayerOption, {
  1339. _applied: false,
  1340.  
  1341. apply: function() {
  1342. if (! this._applied) {
  1343. this.mute(true);
  1344.  
  1345. if (this._player.isPlayerState(Player.PLAYING, Player.PAUSED)) {
  1346. this._applied = true;
  1347.  
  1348. var quality = ['tiny', 'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'highres'][this.get() - 1];
  1349.  
  1350. if (quality != this._player.getPlaybackQuality()) {
  1351. this._player.restartPlayback();
  1352. this._player.setPlaybackQuality(quality);
  1353. }
  1354.  
  1355. asyncCall(this.apply, this);
  1356. }
  1357. } else if (this._player.isPlayerState(Player.PLAYING, Player.CUED)) {
  1358. this.mute(false);
  1359. }
  1360. }
  1361. });
  1362.  
  1363. /**
  1364. * @class VideoQuality.Button
  1365. */
  1366. VideoQuality.Button = PlayerOption.Button.extend({
  1367. label: _('Quality'),
  1368. tooltip: _('Set default video quality'),
  1369. states: [_('AUTO'), '144p', '240p', '360p', '480p', '720p', '1080p', '1440p', _('ORIGINAL')]
  1370. });
  1371.  
  1372. /**
  1373. * @class PlayerSize
  1374. */
  1375. function PlayerSize(player) {
  1376. PlayerOption.call(this, player, 'player_size');
  1377. }
  1378.  
  1379. PlayerSize.prototype = extend(PlayerOption, {
  1380. apply: function() {
  1381. var mode = this.get(), rules = [];
  1382.  
  1383. switch (mode) {
  1384. case 4: // 1080p
  1385. rules.push(
  1386. '.watch-stage-mode .player-width {',
  1387. 'left: -960.0px !important;',
  1388. 'width: 1920px !important;',
  1389. '}',
  1390.  
  1391. '.watch-stage-mode .player-height {',
  1392. 'height: 1110px !important;',
  1393. '}',
  1394.  
  1395. '.watch-stage-mode .html5-video-content[style*="640"], .watch-stage-mode .html5-video-content[style*="360"], .watch-stage-mode .html5-main-video[style*="640"], .watch-stage-mode .html5-main-video[style*="360"]', '{',
  1396. 'transform: matrix(3, 0, 0, 3, 640, 360) !important; -o-transform: matrix(3, 0, 0, 3, 640, 360) !important; -moz-transform: matrix(3, 0, 0, 3, 640, 360) !important; -webkit-transform: matrix(3, 0, 0, 3, 640, 360) !important;',
  1397. '}',
  1398.  
  1399. '.watch-stage-mode .html5-video-content[style*="854"], .watch-stage-mode .html5-video-content[style*="480"], .watch-stage-mode .html5-main-video[style*="854"], .watch-stage-mode .html5-main-video[style*="480"]', '{',
  1400. 'transform: matrix(2.24824, 0, 0, 2.24824, 533, 299.578) !important; -o-transform: matrix(2.24824, 0, 0, 2.24824, 533, 299.578) !important; -moz-transform: matrix(2.24824, 0, 0, 2.24824, 533, 299.578) !important; -webkit-transform: matrix(2.24824, 0, 0, 2.24824, 533, 299.578) !important;',
  1401. '}',
  1402.  
  1403. '.watch-stage-mode .html5-video-content[style*="1280"], .watch-stage-mode .html5-video-content[style*="720"], .watch-stage-mode .html5-main-video[style*="1280"], .watch-stage-mode .html5-main-video[style*="720"]', '{',
  1404. 'transform: matrix(1.5, 0, 0, 1.5, 320, 180) !important; -o-transform: matrix(1.5, 0, 0, 1.5, 320, 180) !important; -moz-transform: matrix(1.5, 0, 0, 1.5, 320, 180) !important; -webkit-transform: matrix(1.5, 0, 0, 1.5, 320, 180) !important;',
  1405. '}'
  1406. );
  1407.  
  1408. break;
  1409.  
  1410. case 3: // 720p
  1411. rules.push(
  1412. '.watch-stage-mode .player-width {',
  1413. 'left: -640.0px !important;',
  1414. 'width: 1280px !important;',
  1415. '}',
  1416.  
  1417. '.watch-stage-mode .player-height {',
  1418. 'height: 750px !important;',
  1419. '}',
  1420.  
  1421. '.watch-stage-mode .html5-video-content[style*="640"], .watch-stage-mode .html5-video-content[style*="360"], .watch-stage-mode .html5-main-video[style*="640"], .watch-stage-mode .html5-main-video[style*="360"]', '{',
  1422. 'transform: matrix(2, 0, 0, 2, 320, 180) !important; -o-transform: matrix(2, 0, 0, 2, 320, 180) !important; -moz-transform: matrix(2, 0, 0, 2, 320, 180) !important; -webkit-transform: matrix(2, 0, 0, 2, 320, 180) !important;',
  1423. '}',
  1424.  
  1425. '.watch-stage-mode .html5-video-content[style*="854"], .watch-stage-mode .html5-video-content[style*="480"], .watch-stage-mode .html5-main-video[style*="854"], .watch-stage-mode .html5-main-video[style*="480"]', '{',
  1426. 'transform: matrix(1.49883, 0, 0, 1.49883, 213, 119.719) !important; -o-transform: matrix(1.49883, 0, 0, 1.49883, 213, 119.719) !important; -moz-transform: matrix(1.49883, 0, 0, 1.49883, 213, 119.719) !important; -webkit-transform: matrix(1.49883, 0, 0, 1.49883, 213, 119.719) !important;',
  1427. '}'
  1428. );
  1429.  
  1430. break;
  1431.  
  1432. case 2: // 480p
  1433. rules.push(
  1434. '.watch-stage-mode .player-width {',
  1435. 'left: -427.0px !important;',
  1436. 'width: 854px !important;',
  1437. '}',
  1438.  
  1439. '.watch-stage-mode .player-height {',
  1440. 'height: 510px !important;',
  1441. '}',
  1442.  
  1443. '.watch-stage-mode .html5-video-content[style*="640"], .watch-stage-mode .html5-video-content[style*="360"], .watch-stage-mode .html5-main-video[style*="640"], .watch-stage-mode .html5-main-video[style*="360"]', '{',
  1444. 'transform: matrix(1.33438, 0, 0, 1.33438, 107, 60.1875) !important; -o-transform: matrix(1.33438, 0, 0, 1.33438, 107, 60.1875) !important; -moz-transform: matrix(1.33438, 0, 0, 1.33438, 107, 60.1875) !important; -webkit-transform: matrix(1.33438, 0, 0, 1.33438, 107, 60.1875) !important;',
  1445. '}',
  1446.  
  1447. '.watch-stage-mode .html5-video-content[style*="1280"], .watch-stage-mode .html5-video-content[style*="720"], .watch-stage-mode .html5-main-video[style*="1280"], .watch-stage-mode .html5-main-video[style*="720"]', '{',
  1448. 'transform: matrix(0.667188, 0, 0, 0.667188, -213, -119.812) !important; -o-transform: matrix(0.667188, 0, 0, 0.667188, -213, -119.812) !important; -moz-transform: matrix(0.667188, 0, 0, 0.667188, -213, -119.812) !important; -webkit-transform: matrix(0.667188, 0, 0, 0.667188, -213, -119.812) !important;',
  1449. '}'
  1450. );
  1451.  
  1452. break;
  1453.  
  1454. case 1: // WIDE
  1455. break;
  1456.  
  1457. default:
  1458. return;
  1459. }
  1460.  
  1461. if (rules.length) {
  1462. rules.push(
  1463. '.watch-stage-mode .html5-main-video {',
  1464. 'z-index: -1;',
  1465. '}'
  1466. );
  1467.  
  1468. DH.id('yays-player-size') || DH.append(document.body, {
  1469. tag: 'style',
  1470. attributes: {
  1471. 'id': 'yays-player-size',
  1472. 'type': 'text/css'
  1473. },
  1474. children: rules
  1475. });
  1476. }
  1477.  
  1478. var page = DH.id('page');
  1479.  
  1480. DH.delClass(page, 'watch-non-stage-mode');
  1481. DH.addClass(page, 'watch-stage-mode watch-wide');
  1482.  
  1483. this._player.setStageMode(true);
  1484.  
  1485. Console.debug('Size set to', ['wide', '480p', '720p', '1080p'][mode - 1]);
  1486. }
  1487. });
  1488.  
  1489. /**
  1490. * @class PlayerSize.Button
  1491. */
  1492. PlayerSize.Button = PlayerOption.Button.extend({
  1493. label: _('Size'),
  1494. tooltip: _('Set default player size'),
  1495. states: [_('AUTO'), _('WIDE'), '480p', '720p', '1080p']
  1496. });
  1497.  
  1498. /**
  1499. * @class UI
  1500. * Abstract UI class.
  1501. */
  1502. function UI(content) {
  1503. this.content = content;
  1504. this.button = DH.build(this._def.button(bind(this.toggle, this)));
  1505. this.panel = DH.build(this._def.panel(content));
  1506. }
  1507.  
  1508. merge(UI, {
  1509. instance: null,
  1510.  
  1511. initialize: function(type, content) {
  1512. if (this.instance) {
  1513. this.instance.destroy();
  1514. }
  1515.  
  1516. return this.instance = new type(content);
  1517. }
  1518. });
  1519.  
  1520. UI.prototype = {
  1521. _def: {
  1522. icon: function(def) {
  1523. def = merge({tag: 'img', attributes: {}}, def);
  1524.  
  1525. def.attributes.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAs0lEQVQoz4VRuRGDQAxcQDNLeKEzl2BCl+CQkIyjA3dCCYQugxJowWQ4AocEnnPAWTxjhlUirUar0y2wAGs6Otb4DyZ0PpKZjQAmcpVOMjwQezaTlzzlJvGnA8BGJ7fRABHvsNjDSd4hLloOKMZgDFBgUO48v91RlWg9U6/K1UU6EuIAIYD8Jzyv8EkOgKUe1U8NWvbKlYJW1QwqVpsN7eFHTR6kNCvhnpaG6dKTXbNwZPcX5uNiEvATXN0AAAAASUVORK5CYII=';
  1526.  
  1527. return def;
  1528. },
  1529.  
  1530. button: function(click) {
  1531. return {
  1532. listeners: {
  1533. 'click': click
  1534. }
  1535. };
  1536. },
  1537.  
  1538. panel: function(content) {
  1539. return [{
  1540. style: {
  1541. 'margin-bottom': '10px'
  1542. },
  1543. children: [{
  1544. tag: 'strong',
  1545. children: _('Player settings')
  1546. }, {
  1547. tag: 'a',
  1548. attributes: {
  1549. 'href': Meta.site,
  1550. 'target': '_blank'
  1551. },
  1552. style: {
  1553. 'margin-left': '4px',
  1554. 'vertical-align': 'super',
  1555. 'font-size': '10px'
  1556. },
  1557. children: _('Help')
  1558. }]
  1559. }, {
  1560. style: {
  1561. 'text-align': 'center'
  1562. },
  1563. children: content.render()
  1564. }];
  1565. }
  1566. },
  1567.  
  1568. content: null,
  1569. button: null,
  1570. panel: null,
  1571.  
  1572. destroy: function() {
  1573. DH.remove(this.button);
  1574. DH.remove(this.panel);
  1575. },
  1576.  
  1577. toggle: function() {
  1578. this.content.refresh();
  1579. }
  1580. };
  1581.  
  1582. /**
  1583. * @class UI.Content
  1584. */
  1585. UI.Content = function(buttons) {
  1586. this._buttons = buttons;
  1587. };
  1588.  
  1589. UI.Content.prototype = {
  1590. _buttons: null,
  1591.  
  1592. render: function() {
  1593. return map(function(button) { return button.render(); }, this._buttons);
  1594. },
  1595.  
  1596. refresh: function() {
  1597. each(this._buttons, function(i, button) { button.refresh(); });
  1598. }
  1599. };
  1600.  
  1601. /**
  1602. * @class UI.Requirement
  1603. */
  1604. UI.Requirement = function(queries) {
  1605. this._queries = [].concat(queries);
  1606. };
  1607.  
  1608. UI.Requirement.prototype = {
  1609. _queries: null,
  1610.  
  1611. test: function() {
  1612. return DH.query(this._queries.join(', ')).length >= this._queries.length;
  1613. }
  1614. };
  1615.  
  1616. /**
  1617. * @class WatchUI
  1618. */
  1619. function WatchUI(buttons) {
  1620. UI.call(this, new UI.Content(buttons));
  1621. }
  1622.  
  1623. WatchUI.prototype = extend(UI, {
  1624. _def: {
  1625. panel: function(content) {
  1626. return {
  1627. attributes: {
  1628. 'id': 'action-panel-yays',
  1629. 'class': 'action-panel-content hid',
  1630. 'data-panel-loaded': 'true'
  1631. },
  1632. children: UI.prototype._def.panel(content)
  1633. };
  1634. }
  1635. }
  1636. });
  1637.  
  1638. /**
  1639. * @class Watch8UI
  1640. */
  1641. function Watch8UI(buttons) {
  1642. WatchUI.call(this, buttons);
  1643.  
  1644. DH.append(DH.id('watch8-secondary-actions'), this.button);
  1645. DH.append(DH.id('watch-action-panels'), this.panel);
  1646. }
  1647.  
  1648. Watch8UI.requirement = new UI.Requirement(['#page.watch #watch8-secondary-actions', '#page.watch #watch-action-panels']);
  1649.  
  1650. Watch8UI.prototype = extend(WatchUI, {
  1651. _def: {
  1652. button: function(click) {
  1653. return {
  1654. tag: 'span',
  1655. children: {
  1656. tag: 'button',
  1657. attributes: {
  1658. 'type': 'button',
  1659. 'class': 'action-panel-trigger yt-uix-button yt-uix-button-empty yt-uix-button-has-icon yt-uix-button-opacity yt-uix-button-size-default yt-uix-tooltip',
  1660. 'data-button-toggle': 'true',
  1661. 'data-trigger-for': 'action-panel-yays',
  1662. 'data-tooltip-text': _('Player settings')
  1663. },
  1664. listeners: {
  1665. 'click': click
  1666. },
  1667. children: {
  1668. tag: 'span',
  1669. attributes: {
  1670. 'class': 'yt-uix-button-icon-wrapper'
  1671. },
  1672. children: UI.prototype._def.icon({
  1673. attributes: {
  1674. 'class': 'yt-uix-button-icon'
  1675. }
  1676. })
  1677. }
  1678. }
  1679. };
  1680. },
  1681.  
  1682. panel: WatchUI.prototype._def.panel
  1683. }
  1684. });
  1685.  
  1686. /**
  1687. * @class ChannelUI
  1688. */
  1689. function ChannelUI(buttons) {
  1690. UI.call(this, new UI.Content(buttons));
  1691.  
  1692. DH.append(DH.id('channel-navigation-menu'), {
  1693. tag: 'li',
  1694. children: [this.button, this.panel]
  1695. });
  1696. }
  1697.  
  1698. ChannelUI.requirement = new UI.Requirement('#channel-navigation-menu');
  1699.  
  1700. ChannelUI.prototype = extend(UI, {
  1701. _def: {
  1702. button: function(click) {
  1703. return {
  1704. tag: 'button',
  1705. attributes: {
  1706. 'type': 'button',
  1707. 'role': 'button',
  1708. 'class': 'yt-uix-button yt-uix-button-empty yt-uix-button-epic-nav-item yt-uix-tooltip flip',
  1709. 'data-button-menu-id': 'yays-panel-dropdown',
  1710. 'data-tooltip-text': _('Player settings')
  1711. },
  1712. style: {
  1713. 'position': 'absolute',
  1714. 'right': '20px',
  1715. 'width': '30px'
  1716. },
  1717. listeners: {
  1718. 'click': click
  1719. },
  1720. children: {
  1721. tag: 'span',
  1722. attributes: {
  1723. 'class': 'yt-uix-button-icon-wrapper'
  1724. },
  1725. style: {
  1726. 'opacity': '0.75'
  1727. },
  1728. children: UI.prototype._def.icon()
  1729. }
  1730. };
  1731. },
  1732.  
  1733. panel: function(content) {
  1734. return {
  1735. attributes: {
  1736. 'id': 'yays-panel-dropdown',
  1737. 'class': 'epic-nav-item-dropdown hid'
  1738. },
  1739. style: {
  1740. 'padding': '5px 10px 10px',
  1741. 'width': '450px'
  1742. },
  1743. children: UI.prototype._def.panel(content)
  1744. };
  1745. }
  1746. }
  1747. });
  1748.  
  1749. /*
  1750. * Ready callbacks.
  1751. */
  1752.  
  1753. function onReady(player) {
  1754. var
  1755. videoPlayback = new VideoPlayback(player),
  1756. videoQuality = new VideoQuality(player),
  1757. playerSize = new PlayerSize(player),
  1758. previousVideo = player.getVideoId();
  1759.  
  1760. player.onStateChange = function() {
  1761. try {
  1762. var currentVideo = player.getVideoId();
  1763.  
  1764. if (currentVideo == previousVideo) {
  1765. videoQuality.apply();
  1766. videoPlayback.apply();
  1767. }
  1768. else {
  1769. videoQuality.cease();
  1770. videoPlayback.cease();
  1771.  
  1772. throw null;
  1773. }
  1774. }
  1775. catch (e) {
  1776. Player.invalidate(player);
  1777.  
  1778. asyncCall(onPlayerReady);
  1779. }
  1780. };
  1781.  
  1782. videoQuality.apply();
  1783. videoPlayback.apply();
  1784.  
  1785. if (Watch8UI.requirement.test()) {
  1786. playerSize.apply();
  1787.  
  1788. UI.initialize(Watch8UI, [
  1789. new VideoQuality.Button(videoQuality),
  1790. new PlayerSize.Button(playerSize),
  1791. new VideoPlayback.Button(videoPlayback)
  1792. ]);
  1793. }
  1794. else if (ChannelUI.requirement.test()) {
  1795. UI.initialize(ChannelUI, [
  1796. new VideoQuality.Button(videoQuality),
  1797. new VideoPlayback.Button(videoPlayback)
  1798. ]);
  1799. }
  1800. }
  1801.  
  1802. function onPlayerReady() {
  1803. each(DH.query('video, embed'), function(i, node) {
  1804. var player = DH.closest(node, function(node) { return Player.test(DH.unwrap(node)); });
  1805.  
  1806. if (player) {
  1807. try {
  1808. player = Player.initialize(DH.unwrap(player));
  1809.  
  1810. onReady(player);
  1811.  
  1812. Console.debug('Initialization finished');
  1813. }
  1814. catch (e) {
  1815. Console.debug(e);
  1816. }
  1817. }
  1818. });
  1819. }
  1820.  
  1821. onPlayerReady();
  1822.  
  1823. // FIXME: The call to an exported function is rejected if a function (or an
  1824. // object with methods) is passed as argument.
  1825. //
  1826. // This restriction can be lifted in FF 33+ by setting the 'allowCallbacks'
  1827. // option when exporting the function.
  1828.  
  1829. var node = DH.build({
  1830. tag: 'script',
  1831. attributes: {
  1832. 'type': 'text/javascript'
  1833. }
  1834. });
  1835.  
  1836. node.text = 'function onYouTubePlayerReady() { ' + scriptContext.publish('onPlayerReady', intercept(unsafeWindow.onYouTubePlayerReady, asyncProxy(onPlayerReady))) + '(); }';
  1837.  
  1838. document.documentElement.appendChild(node);
  1839. document.documentElement.removeChild(node);
  1840.  
  1841. } // YAYS
  1842.  
  1843. if (window.top === window.self) {
  1844. if (this.unsafeWindow) { // Greasemonkey.
  1845. YAYS(unsafeWindow);
  1846. }
  1847. else {
  1848. var node = document.createElement('script');
  1849. node.setAttribute('type', 'text/javascript');
  1850. node.text = '(' + YAYS.toString() + ')(window);';
  1851.  
  1852. document.documentElement.appendChild(node);
  1853. document.documentElement.removeChild(node);
  1854. }
  1855. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址