YouTube Mute and Skip Ads

Mutes, blurs and skips ads on YouTube. Speeds up ad playback. Clicks "yes" on "are you there?" on YouTube Music.

当前为 2024-09-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Mute and Skip Ads
  3. // @namespace https://github.com/ion1/userscripts
  4. // @version 0.0.26
  5. // @author ion
  6. // @description Mutes, blurs and skips ads on YouTube. Speeds up ad playback. Clicks "yes" on "are you there?" on YouTube Music.
  7. // @license MIT
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  9. // @homepage https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads
  10. // @homepageURL https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads
  11. // @match *://www.youtube.com/*
  12. // @match *://music.youtube.com/*
  13. // @grant GM_addStyle
  14. // @run-at document-body
  15. // ==/UserScript==
  16.  
  17. (e=>{if(typeof GM_addStyle=="function"){GM_addStyle(e);return}const n=document.createElement("style");n.textContent=e,document.head.append(n)})(` /* Keep these in sync with the watchers. */
  18. #movie_player
  19. :is(.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-skip-ad-button) {
  20. anchor-name: --youtube-mute-skip-ads-unclickable-button;
  21. }
  22.  
  23. body:has(
  24. #movie_player
  25. :is(
  26. .ytp-ad-skip-button,
  27. .ytp-ad-skip-button-modern,
  28. .ytp-skip-ad-button
  29. ):not([style*="display: none"], [aria-hidden="true"])
  30. )::after {
  31. content: "\u{1D606}\u{1D5FC}\u{1D602}\u{1D601}\u{1D602}\u{1D5EF}\u{1D5F2}-\u{1D5FA}\u{1D602}\u{1D601}\u{1D5F2}-\u{1D600}\u{1D5F8}\u{1D5F6}\u{1D5FD}-\u{1D5EE}\u{1D5F1}\u{1D600}\\A\\A"
  32. "Unfortunately, YouTube has started to block automated clicks based on isTrusted being false.\\A\\A"
  33. "Please click on the skip button manually.";
  34. white-space: pre-line;
  35. pointer-events: none;
  36. z-index: 9999;
  37. position: fixed;
  38. position-anchor: --youtube-mute-skip-ads-unclickable-button;
  39. padding: 1.5em;
  40. border-radius: 1.5em;
  41. margin-bottom: 1em;
  42. bottom: anchor(--youtube-mute-skip-ads-unclickable-button top);
  43. right: anchor(--youtube-mute-skip-ads-unclickable-button right);
  44. max-width: 25em;
  45. font-size: 1.4rem;
  46. line-height: 2rem;
  47. font-weight: 400;
  48. color: rgb(240 240 240);
  49. background-color: rgb(0 0 0 / 0.7);
  50. backdrop-filter: blur(10px);
  51. animation: fade-in 3s linear;
  52. }
  53.  
  54. @keyframes fade-in {
  55. 0% {
  56. opacity: 0;
  57. }
  58. 67% {
  59. opacity: 0;
  60. }
  61. 100% {
  62. opacity: 1;
  63. }
  64. }
  65.  
  66. #movie_player.ad-showing video {
  67. filter: blur(100px) opacity(0.25) grayscale(0.5);
  68. }
  69.  
  70. #movie_player.ad-showing .ytp-title,
  71. #movie_player.ad-showing .ytp-title-channel,
  72. .ytp-visit-advertiser-link,
  73. .ytp-ad-visit-advertiser-button,
  74. .ytp-suggested-action-badge,
  75. ytmusic-app:has(#movie_player.ad-showing)
  76. ytmusic-player-bar
  77. :is(.title, .subtitle) {
  78. filter: blur(4px) opacity(0.5) grayscale(0.5);
  79. transition: 0.05s filter linear;
  80. }
  81.  
  82. :is(#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-visit-advertiser-link,.ytp-ad-visit-advertiser-button,.ytp-suggested-action-badge,ytmusic-app:has(#movie_player.ad-showing) ytmusic-player-bar :is(.title,.subtitle)):is(:hover,:focus-within) {
  83. filter: none;
  84. }
  85.  
  86. #movie_player.ad-showing .caption-window,
  87. .ytp-ad-player-overlay-flyout-cta,
  88. .ytp-ad-player-overlay-layout__player-card-container, /* Seen since 2024-04-06. */
  89. .ytp-ad-action-interstitial-slot, /* Added on 2024-08-25. */
  90. ytd-action-companion-ad-renderer,
  91. ytd-display-ad-renderer,
  92. ytd-ad-slot-renderer,
  93. ytd-promoted-sparkles-web-renderer,
  94. ytd-player-legacy-desktop-watch-ads-renderer,
  95. ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"],
  96. ytd-merch-shelf-renderer {
  97. filter: blur(10px) opacity(0.25) grayscale(0.5);
  98. transition: 0.05s filter linear;
  99. }
  100.  
  101. :is(#movie_player.ad-showing .caption-window,.ytp-ad-player-overlay-flyout-cta,.ytp-ad-player-overlay-layout__player-card-container,.ytp-ad-action-interstitial-slot,ytd-action-companion-ad-renderer,ytd-display-ad-renderer,ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer,ytd-player-legacy-desktop-watch-ads-renderer,ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"],ytd-merch-shelf-renderer):is(:hover,:focus-within) {
  102. filter: none;
  103. }
  104.  
  105. .ytp-ad-action-interstitial-background-container /* Added on 2024-08-25. */ {
  106. /* An image ad in place of the video. */
  107. filter: blur(10px) opacity(0.25) grayscale(0.5);
  108. } `);
  109.  
  110. (function () {
  111. 'use strict';
  112.  
  113. var __defProp = Object.defineProperty;
  114. var __typeError = (msg) => {
  115. throw TypeError(msg);
  116. };
  117. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  118. var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  119. var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
  120. var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
  121. var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
  122. var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
  123. var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
  124. var _onCreatedCallbacks, _onRemovedCallbacks, _nodeObserver, _nodeWatchers, _textObserver, _onTextChangedCallbacks, _onAttrChangedCallbacks, _visibilityObserver, _isVisible, _visibilityWatchers, _Watcher_instances, assertElement_fn, assertVisibilityAncestor_fn, connect_fn, disconnect_fn, registerNodeObserver_fn, registerTextObserver_fn, registerAttrObservers_fn, registerVisibilityObserver_fn, deregisterNodeObserver_fn, deregisterTextObserver_fn, deregisterAttrObservers_fn, deregisterVisibilityObserver_fn;
  125. const logPrefix = "youtube-mute-skip-ads:";
  126. const _Watcher = class _Watcher {
  127. constructor(name, elem) {
  128. __privateAdd(this, _Watcher_instances);
  129. __publicField(this, "name");
  130. __publicField(this, "element");
  131. __privateAdd(this, _onCreatedCallbacks);
  132. __privateAdd(this, _onRemovedCallbacks);
  133. __privateAdd(this, _nodeObserver);
  134. __privateAdd(this, _nodeWatchers);
  135. __privateAdd(this, _textObserver);
  136. __privateAdd(this, _onTextChangedCallbacks);
  137. __privateAdd(this, _onAttrChangedCallbacks);
  138. __publicField(this, "visibilityAncestor");
  139. __privateAdd(this, _visibilityObserver);
  140. __privateAdd(this, _isVisible);
  141. __privateAdd(this, _visibilityWatchers);
  142. this.name = name;
  143. this.element = null;
  144. __privateSet(this, _onCreatedCallbacks, []);
  145. __privateSet(this, _onRemovedCallbacks, []);
  146. __privateSet(this, _nodeObserver, null);
  147. __privateSet(this, _nodeWatchers, []);
  148. __privateSet(this, _textObserver, null);
  149. __privateSet(this, _onTextChangedCallbacks, []);
  150. __privateSet(this, _onAttrChangedCallbacks, []);
  151. this.visibilityAncestor = null;
  152. __privateSet(this, _visibilityObserver, null);
  153. __privateSet(this, _isVisible, null);
  154. __privateSet(this, _visibilityWatchers, []);
  155. if (elem != null) {
  156. __privateMethod(this, _Watcher_instances, connect_fn).call(this, elem);
  157. }
  158. }
  159. onCreated(onCreatedCb) {
  160. __privateGet(this, _onCreatedCallbacks).push(onCreatedCb);
  161. if (this.element != null) {
  162. const onRemovedCb = onCreatedCb(this.element);
  163. if (onRemovedCb) {
  164. __privateGet(this, _onRemovedCallbacks).push(onRemovedCb);
  165. }
  166. }
  167. return this;
  168. }
  169. descendant(selector, name) {
  170. var _a;
  171. const watcher2 = new _Watcher(`${this.name} ${name}`);
  172. __privateGet(this, _nodeWatchers).push({ selector, name, watcher: watcher2 });
  173. if (this.element != null) {
  174. for (const descElem of getDescendantsBy(this.element, selector, name)) {
  175. __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, descElem, this.element);
  176. }
  177. __privateMethod(this, _Watcher_instances, registerNodeObserver_fn).call(this);
  178. }
  179. return watcher2;
  180. }
  181. id(idName) {
  182. return this.descendant("id", idName);
  183. }
  184. klass(className) {
  185. return this.descendant("class", className);
  186. }
  187. tag(tagName) {
  188. return this.descendant("tag", tagName);
  189. }
  190. visible() {
  191. var _a;
  192. const watcher2 = new _Watcher(`${this.name} (visible)`);
  193. __privateGet(this, _visibilityWatchers).push(watcher2);
  194. if (this.element != null) {
  195. const visibilityAncestor = __privateMethod(this, _Watcher_instances, assertVisibilityAncestor_fn).call(this);
  196. if (__privateGet(this, _isVisible)) {
  197. __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, this.element, visibilityAncestor);
  198. }
  199. __privateMethod(this, _Watcher_instances, registerVisibilityObserver_fn).call(this);
  200. }
  201. return watcher2;
  202. }
  203. /// `null` implies null textContent. `undefined` implies that the watcher is
  204. /// being disconnected.
  205. text(callback) {
  206. __privateGet(this, _onTextChangedCallbacks).push(callback);
  207. if (this.element != null) {
  208. callback(this.element, this.element.textContent);
  209. __privateMethod(this, _Watcher_instances, registerTextObserver_fn).call(this);
  210. }
  211. return this;
  212. }
  213. /// `null` implies no such attribute. `undefined` implies that the watcher is
  214. /// being disconnected.
  215. attr(name, callback) {
  216. __privateGet(this, _onAttrChangedCallbacks).push({ name, callback, observer: null });
  217. if (this.element != null) {
  218. callback(this.element, this.element.getAttribute(name));
  219. __privateMethod(this, _Watcher_instances, registerAttrObservers_fn).call(this);
  220. }
  221. return this;
  222. }
  223. };
  224. _onCreatedCallbacks = new WeakMap();
  225. _onRemovedCallbacks = new WeakMap();
  226. _nodeObserver = new WeakMap();
  227. _nodeWatchers = new WeakMap();
  228. _textObserver = new WeakMap();
  229. _onTextChangedCallbacks = new WeakMap();
  230. _onAttrChangedCallbacks = new WeakMap();
  231. _visibilityObserver = new WeakMap();
  232. _isVisible = new WeakMap();
  233. _visibilityWatchers = new WeakMap();
  234. _Watcher_instances = new WeakSet();
  235. assertElement_fn = function() {
  236. if (this.element == null) {
  237. throw new Error(`Watcher not connected to an element`);
  238. }
  239. return this.element;
  240. };
  241. assertVisibilityAncestor_fn = function() {
  242. if (this.visibilityAncestor == null) {
  243. throw new Error(`Watcher is missing a visibilityAncestor`);
  244. }
  245. return this.visibilityAncestor;
  246. };
  247. connect_fn = function(element, visibilityAncestor) {
  248. var _a;
  249. if (this.element != null) {
  250. if (this.element !== element) {
  251. console.error(
  252. logPrefix,
  253. `Watcher already connected to`,
  254. this.element,
  255. `while trying to connect to`,
  256. element
  257. );
  258. }
  259. return;
  260. }
  261. this.element = element;
  262. this.visibilityAncestor = visibilityAncestor ?? null;
  263. for (const onCreatedCb of __privateGet(this, _onCreatedCallbacks)) {
  264. const onRemovedCb = onCreatedCb(this.element);
  265. if (onRemovedCb) {
  266. __privateGet(this, _onRemovedCallbacks).push(onRemovedCb);
  267. }
  268. }
  269. for (const { selector, name, watcher: watcher2 } of __privateGet(this, _nodeWatchers)) {
  270. for (const descElem of getDescendantsBy(this.element, selector, name)) {
  271. __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, descElem, this.element);
  272. }
  273. }
  274. for (const callback of __privateGet(this, _onTextChangedCallbacks)) {
  275. callback(this.element, this.element.textContent);
  276. }
  277. for (const { name, callback } of __privateGet(this, _onAttrChangedCallbacks)) {
  278. callback(this.element, this.element.getAttribute(name));
  279. }
  280. __privateMethod(this, _Watcher_instances, registerNodeObserver_fn).call(this);
  281. __privateMethod(this, _Watcher_instances, registerTextObserver_fn).call(this);
  282. __privateMethod(this, _Watcher_instances, registerAttrObservers_fn).call(this);
  283. __privateMethod(this, _Watcher_instances, registerVisibilityObserver_fn).call(this);
  284. };
  285. disconnect_fn = function() {
  286. var _a, _b;
  287. if (this.element == null) {
  288. return;
  289. }
  290. for (const child of __privateGet(this, _nodeWatchers)) {
  291. __privateMethod(_a = child.watcher, _Watcher_instances, disconnect_fn).call(_a);
  292. }
  293. for (const callback of __privateGet(this, _onTextChangedCallbacks)) {
  294. callback(this.element, void 0);
  295. }
  296. for (const { callback } of __privateGet(this, _onAttrChangedCallbacks)) {
  297. callback(this.element, void 0);
  298. }
  299. for (const child of __privateGet(this, _visibilityWatchers)) {
  300. __privateMethod(_b = child, _Watcher_instances, disconnect_fn).call(_b);
  301. }
  302. __privateMethod(this, _Watcher_instances, deregisterNodeObserver_fn).call(this);
  303. __privateMethod(this, _Watcher_instances, deregisterTextObserver_fn).call(this);
  304. __privateMethod(this, _Watcher_instances, deregisterAttrObservers_fn).call(this);
  305. __privateMethod(this, _Watcher_instances, deregisterVisibilityObserver_fn).call(this);
  306. while (__privateGet(this, _onRemovedCallbacks).length > 0) {
  307. const onRemovedCb = __privateGet(this, _onRemovedCallbacks).shift();
  308. onRemovedCb();
  309. }
  310. this.element = null;
  311. };
  312. registerNodeObserver_fn = function() {
  313. if (__privateGet(this, _nodeObserver) != null) {
  314. return;
  315. }
  316. if (__privateGet(this, _nodeWatchers).length === 0) {
  317. return;
  318. }
  319. const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
  320. __privateSet(this, _nodeObserver, new MutationObserver((mutations) => {
  321. var _a, _b;
  322. for (const mut of mutations) {
  323. for (const node of mut.addedNodes) {
  324. for (const { selector, name, watcher: watcher2 } of __privateGet(this, _nodeWatchers)) {
  325. for (const descElem of getSelfOrDescendantsBy(
  326. node,
  327. selector,
  328. name
  329. )) {
  330. __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, descElem, elem);
  331. }
  332. }
  333. }
  334. for (const node of mut.removedNodes) {
  335. for (const { selector, name, watcher: watcher2 } of __privateGet(this, _nodeWatchers)) {
  336. for (const _descElem of getSelfOrDescendantsBy(
  337. node,
  338. selector,
  339. name
  340. )) {
  341. __privateMethod(_b = watcher2, _Watcher_instances, disconnect_fn).call(_b);
  342. }
  343. }
  344. }
  345. }
  346. }));
  347. __privateGet(this, _nodeObserver).observe(elem, {
  348. subtree: true,
  349. childList: true
  350. });
  351. };
  352. registerTextObserver_fn = function() {
  353. if (__privateGet(this, _textObserver) != null) {
  354. return;
  355. }
  356. if (__privateGet(this, _onTextChangedCallbacks).length === 0) {
  357. return;
  358. }
  359. const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
  360. __privateSet(this, _textObserver, new MutationObserver((_mutations) => {
  361. for (const callback of __privateGet(this, _onTextChangedCallbacks)) {
  362. callback(elem, elem.textContent);
  363. }
  364. }));
  365. __privateGet(this, _textObserver).observe(elem, {
  366. subtree: true,
  367. // This is needed when elements are replaced to update their text.
  368. childList: true,
  369. characterData: true
  370. });
  371. };
  372. registerAttrObservers_fn = function() {
  373. const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
  374. for (const handler of __privateGet(this, _onAttrChangedCallbacks)) {
  375. if (handler.observer != null) {
  376. continue;
  377. }
  378. const { name, callback } = handler;
  379. handler.observer = new MutationObserver((_mutations) => {
  380. callback(elem, elem.getAttribute(name));
  381. });
  382. handler.observer.observe(elem, {
  383. attributes: true,
  384. attributeFilter: [name]
  385. });
  386. }
  387. };
  388. registerVisibilityObserver_fn = function() {
  389. if (__privateGet(this, _visibilityObserver) != null) {
  390. return;
  391. }
  392. if (__privateGet(this, _visibilityWatchers).length === 0) {
  393. return;
  394. }
  395. __privateSet(this, _isVisible, false);
  396. const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
  397. const visibilityAncestor = __privateMethod(this, _Watcher_instances, assertVisibilityAncestor_fn).call(this);
  398. __privateSet(this, _visibilityObserver, new IntersectionObserver(
  399. (entries) => {
  400. var _a, _b;
  401. const oldVisible = __privateGet(this, _isVisible);
  402. for (const entry of entries) {
  403. __privateSet(this, _isVisible, entry.isIntersecting);
  404. }
  405. if (__privateGet(this, _isVisible) !== oldVisible) {
  406. if (__privateGet(this, _isVisible)) {
  407. for (const watcher2 of __privateGet(this, _visibilityWatchers)) {
  408. __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, elem, visibilityAncestor);
  409. }
  410. } else {
  411. for (const watcher2 of __privateGet(this, _visibilityWatchers)) {
  412. __privateMethod(_b = watcher2, _Watcher_instances, disconnect_fn).call(_b);
  413. }
  414. }
  415. }
  416. },
  417. {
  418. root: visibilityAncestor
  419. }
  420. ));
  421. __privateGet(this, _visibilityObserver).observe(elem);
  422. };
  423. deregisterNodeObserver_fn = function() {
  424. if (__privateGet(this, _nodeObserver) == null) {
  425. return;
  426. }
  427. __privateGet(this, _nodeObserver).disconnect();
  428. __privateSet(this, _nodeObserver, null);
  429. };
  430. deregisterTextObserver_fn = function() {
  431. if (__privateGet(this, _textObserver) == null) {
  432. return;
  433. }
  434. __privateGet(this, _textObserver).disconnect();
  435. __privateSet(this, _textObserver, null);
  436. };
  437. deregisterAttrObservers_fn = function() {
  438. for (const handler of __privateGet(this, _onAttrChangedCallbacks)) {
  439. if (handler.observer == null) {
  440. continue;
  441. }
  442. handler.observer.disconnect();
  443. handler.observer = null;
  444. }
  445. };
  446. deregisterVisibilityObserver_fn = function() {
  447. if (__privateGet(this, _visibilityObserver) == null) {
  448. return;
  449. }
  450. __privateGet(this, _visibilityObserver).disconnect();
  451. __privateSet(this, _visibilityObserver, null);
  452. __privateSet(this, _isVisible, null);
  453. };
  454. let Watcher = _Watcher;
  455. function getSelfOrDescendantsBy(node, selector, name) {
  456. if (!(node instanceof HTMLElement)) {
  457. return [];
  458. }
  459. if (selector === "id" || selector === "class" || selector === "tag") {
  460. if (selector === "id" && node.id === name || selector === "class" && node.classList.contains(name) || selector === "tag" && node.tagName.toLowerCase() === name.toLowerCase()) {
  461. return [node];
  462. } else {
  463. return getDescendantsBy(node, selector, name);
  464. }
  465. } else {
  466. const impossible = selector;
  467. throw new Error(`Impossible selector type: ${JSON.stringify(impossible)}`);
  468. }
  469. }
  470. function getDescendantsBy(node, selector, name) {
  471. if (!(node instanceof HTMLElement)) {
  472. return [];
  473. }
  474. let cssSelector = "";
  475. if (selector === "id") {
  476. cssSelector += "#";
  477. } else if (selector === "class") {
  478. cssSelector += ".";
  479. } else if (selector === "tag") ;
  480. else {
  481. const impossible = selector;
  482. throw new Error(`Impossible selector type: ${JSON.stringify(impossible)}`);
  483. }
  484. cssSelector += CSS.escape(name);
  485. return Array.from(node.querySelectorAll(cssSelector));
  486. }
  487. const videoSelector = "#movie_player video";
  488. function getVideoElement() {
  489. const videoElem = document.querySelector(videoSelector);
  490. if (!(videoElem instanceof HTMLVideoElement)) {
  491. console.error(
  492. logPrefix,
  493. "Expected",
  494. JSON.stringify(videoSelector),
  495. "to be a video element, got:",
  496. videoElem == null ? void 0 : videoElem.cloneNode(true)
  497. );
  498. return null;
  499. }
  500. return videoElem;
  501. }
  502. function callMoviePlayerMethod(name, onSuccess, args) {
  503. var _a;
  504. try {
  505. const movieElem = document.getElementById("movie_player");
  506. if (movieElem == null) {
  507. console.warn(logPrefix, "movie_player element not found");
  508. return;
  509. }
  510. const method = (_a = Object.getOwnPropertyDescriptor(
  511. movieElem,
  512. name
  513. )) == null ? void 0 : _a.value;
  514. if (method == null) {
  515. console.warn(
  516. logPrefix,
  517. `movie_player element has no ${JSON.stringify(name)} property`
  518. );
  519. return;
  520. }
  521. if (!(typeof method === "function")) {
  522. console.warn(
  523. logPrefix,
  524. `movie_player element property ${JSON.stringify(name)} is not a function`
  525. );
  526. return;
  527. }
  528. const result = method.apply(movieElem, args);
  529. if (onSuccess != null) {
  530. onSuccess(result);
  531. }
  532. return result;
  533. } catch (e) {
  534. console.warn(
  535. logPrefix,
  536. `movie_player method ${JSON.stringify(name)} failed:`,
  537. e
  538. );
  539. return;
  540. }
  541. }
  542. function disableVisibilityChecks() {
  543. for (const eventName of ["visibilitychange", "blur", "focus"]) {
  544. document.addEventListener(
  545. eventName,
  546. (ev) => {
  547. ev.stopImmediatePropagation();
  548. },
  549. { capture: true }
  550. );
  551. }
  552. document.hasFocus = () => true;
  553. Object.defineProperties(document, {
  554. visibilityState: { value: "visible" },
  555. hidden: { value: false }
  556. });
  557. }
  558. function adIsPlaying(_elem) {
  559. console.info(logPrefix, "An ad is playing, muting and speeding up");
  560. const video = getVideoElement();
  561. if (video == null) {
  562. return;
  563. }
  564. const onRemovedCallbacks = [
  565. mute(video),
  566. speedup(video),
  567. cancelPlayback(video)
  568. ];
  569. return function onRemoved() {
  570. for (const callback of onRemovedCallbacks) {
  571. callback();
  572. }
  573. };
  574. }
  575. function mute(video) {
  576. console.debug(logPrefix, "Muting");
  577. video.muted = true;
  578. return unmute;
  579. }
  580. function unmute() {
  581. {
  582. return;
  583. }
  584. }
  585. function speedup(video) {
  586. const originalRate = video.playbackRate;
  587. for (let rate = 16; rate >= 2; rate /= 2) {
  588. try {
  589. video.playbackRate = rate;
  590. break;
  591. } catch (e) {
  592. console.debug(logPrefix, `Setting playback rate to`, rate, `failed:`, e);
  593. }
  594. }
  595. return function onRemoved() {
  596. console.debug(logPrefix, `Restoring playback rate:`, originalRate);
  597. try {
  598. video.playbackRate = originalRate;
  599. } catch (e) {
  600. console.debug(
  601. logPrefix,
  602. `Restoring playback rate to`,
  603. originalRate,
  604. `failed:`,
  605. e
  606. );
  607. }
  608. };
  609. }
  610. function cancelPlayback(video) {
  611. let shouldResume = false;
  612. function doCancelPlayback() {
  613. console.info(logPrefix, "Attempting to cancel playback");
  614. callMoviePlayerMethod("cancelPlayback", () => {
  615. shouldResume = true;
  616. });
  617. }
  618. if (video.paused) {
  619. console.debug(
  620. logPrefix,
  621. "Ad paused, waiting for it to play before canceling playback"
  622. );
  623. video.addEventListener("play", doCancelPlayback);
  624. } else {
  625. doCancelPlayback();
  626. }
  627. return function onRemoved() {
  628. video.removeEventListener("play", doCancelPlayback);
  629. if (shouldResume) {
  630. resumePlaybackIfNotAtEnd();
  631. }
  632. };
  633. }
  634. function resumePlaybackIfNotAtEnd() {
  635. const currentTime = callMoviePlayerMethod("getCurrentTime");
  636. const duration = callMoviePlayerMethod("getDuration");
  637. const isAtLiveHead = callMoviePlayerMethod("isAtLiveHead");
  638. if (currentTime == null || duration == null || typeof currentTime !== "number" || typeof duration !== "number" || isNaN(currentTime) || isNaN(duration)) {
  639. console.warn(
  640. logPrefix,
  641. `movie_player methods getCurrentTime/getDuration failed, got time: ${JSON.stringify(currentTime)}, duration: ${JSON.stringify(duration)}`
  642. );
  643. return;
  644. }
  645. if (isAtLiveHead == null || typeof isAtLiveHead !== "boolean") {
  646. console.warn(
  647. logPrefix,
  648. `movie_player method isAtLiveHead failed, got: ${JSON.stringify(isAtLiveHead)}`
  649. );
  650. return;
  651. }
  652. const atEnd = duration - currentTime < 1;
  653. if (atEnd && !isAtLiveHead) {
  654. console.info(
  655. logPrefix,
  656. `Video is at the end (${currentTime}/${duration}), not attempting to resume playback`
  657. );
  658. return;
  659. }
  660. console.info(logPrefix, "Attempting to resume playback");
  661. callMoviePlayerMethod("playVideo");
  662. }
  663. function click(description) {
  664. return (elem) => {
  665. if (elem.getAttribute("aria-hidden")) {
  666. console.info(logPrefix, "Not clicking (aria-hidden):", description);
  667. } else {
  668. console.info(logPrefix, "Clicking:", description);
  669. elem.click();
  670. }
  671. };
  672. }
  673. disableVisibilityChecks();
  674. const watcher = new Watcher("body", document.body);
  675. const adPlayerOverlayClasses = [
  676. "ytp-ad-player-overlay",
  677. "ytp-ad-player-overlay-layout"
  678. // Seen since 2024-04-06.
  679. ];
  680. for (const adPlayerOverlayClass of adPlayerOverlayClasses) {
  681. watcher.klass(adPlayerOverlayClass).onCreated(adIsPlaying);
  682. }
  683. const adSkipButtonClasses = [
  684. "ytp-ad-skip-button",
  685. "ytp-ad-skip-button-modern",
  686. // Seen since 2023-11-10.
  687. "ytp-skip-ad-button"
  688. // Seen since 2024-04-06.
  689. ];
  690. for (const adSkipButtonClass of adSkipButtonClasses) {
  691. watcher.id("movie_player").klass(adSkipButtonClass).visible().attr("aria-hidden", (elem, value) => {
  692. if (value === null) {
  693. click(`skip (${adSkipButtonClass})`)(elem);
  694. }
  695. });
  696. }
  697. watcher.klass("ytp-ad-overlay-close-button").visible().onCreated(click("overlay close"));
  698. watcher.klass("ytp-featured-product").klass("ytp-suggested-action-badge-dismiss-button-icon").visible().onCreated(click("suggested action close"));
  699. watcher.tag("ytmusic-you-there-renderer").tag("button").visible().onCreated(click("are-you-there"));
  700.  
  701. })();