YouTube Mute and Skip Ads

Mutes, blurs and skips ads on YouTube. Reloads the page if unskippable ads are too long. The consistent skipping (rather than ad blocking) on the desktop seems to make more ads skippable on mobile as well.

当前为 2023-03-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Mute and Skip Ads
  3. // @namespace https://github.com/ion1/userscripts
  4. // @version 0.0.2
  5. // @author ion
  6. // @description Mutes, blurs and skips ads on YouTube. Reloads the page if unskippable ads are too long. The consistent skipping (rather than ad blocking) on the desktop seems to make more ads skippable on mobile as well.
  7. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  8. // @homepage https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads
  9. // @homepageURL https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads
  10. // @match *://www.youtube.com/*
  11. // @match *://music.youtube.com/*
  12. // ==/UserScript==
  13.  
  14. (t=>{const e=document.createElement("style");e.dataset.source="vite-plugin-monkey",e.innerText=t,document.head.appendChild(e)})(" #movie_player.ad-showing video{filter:blur(100px) opacity(.25) grayscale(.5)}#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-ad-visit-advertiser-button{filter:blur(4px) opacity(.5) grayscale(.5);transition:.05s filter ease}:is(#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-ad-visit-advertiser-button):hover,:is(#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-ad-visit-advertiser-button):focus-within{filter:none}#movie_player.ad-showing .caption-window,.ytp-ad-player-overlay-flyout-cta,ytd-action-companion-ad-renderer,ytd-display-ad-renderer,ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer{filter:blur(10px) opacity(.25) grayscale(.5);transition:.05s filter ease}:is(#movie_player.ad-showing .caption-window,.ytp-ad-player-overlay-flyout-cta,ytd-action-companion-ad-renderer,ytd-display-ad-renderer,ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer):hover,:is(#movie_player.ad-showing .caption-window,.ytp-ad-player-overlay-flyout-cta,ytd-action-companion-ad-renderer,ytd-display-ad-renderer,ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer):focus-within{filter:none}.youtube-mute-skip-ads-reload-notification{pointer-events:none;position:fixed;left:0;top:0;width:100%;height:100%;z-index:9999;display:flex;align-items:center;justify-content:center}.youtube-mute-skip-ads-reload-notification>div{font-size:6rem;line-height:1.3;text-align:center;background-color:#000000b3;color:#fff;padding:2rem;border-radius:1rem}.youtube-mute-skip-ads-reload-notification>div>small{font-size:3rem} ");
  15.  
  16. (function() {
  17. "use strict";
  18. const main = "";
  19. const adMaxCount = 1;
  20. const adMaxTime = 7;
  21. const playerId = "movie_player";
  22. const videoSelector = "#movie_player video";
  23. const muteButtonClass = "ytp-mute-button";
  24. const adUIClass = "ytp-ad-player-overlay-skip-or-preview";
  25. const adBadgeClass = "ytp-ad-simple-ad-badge";
  26. const preskipClass = "ytp-ad-preview-text";
  27. const skipContainerClass = "ytp-ad-skip-button-slot";
  28. const skipButtonClass = "ytp-ad-skip-button";
  29. const overlayCloseButtonClass = "ytp-ad-overlay-close-button";
  30. function getSelfOrChildrenByClassName(node, className) {
  31. if (!(node instanceof Element)) {
  32. return [];
  33. } else if (node.classList.contains(className)) {
  34. return [node];
  35. } else {
  36. return node.getElementsByClassName(className);
  37. }
  38. }
  39. function setVideoProperties(props) {
  40. const video = document.querySelector(videoSelector);
  41. if (!(video instanceof HTMLVideoElement)) {
  42. return;
  43. }
  44. if (props.muted != null) {
  45. video.muted = props.muted;
  46. }
  47. }
  48. function toggleMuteTwice() {
  49. for (const elem of document.getElementsByClassName(muteButtonClass)) {
  50. if (!(elem instanceof HTMLElement)) {
  51. continue;
  52. }
  53. elem.click();
  54. elem.click();
  55. }
  56. }
  57. function adUIAdded(_elem) {
  58. console.info("youtube-mute-skip-ads: An ad is playing, muting");
  59. setVideoProperties({ muted: true });
  60. }
  61. function adUIRemoved(_elem) {
  62. console.info("youtube-mute-skip-ads: An ad is no longer playing, unmuting");
  63. toggleMuteTwice();
  64. }
  65. function reloadPage() {
  66. const playerElem = document.getElementById(playerId);
  67. if (playerElem == null || !("getCurrentTime" in playerElem)) {
  68. return;
  69. }
  70. const notifDiv = document.createElement("div");
  71. notifDiv.setAttribute("class", "youtube-mute-skip-ads-reload-notification");
  72. notifDiv.innerHTML = `<div aria-live="assertive" aria-atomic="true">Reloading<br/><small>(Youtube Mute and Skip Ads)</small></div>`;
  73. document.body.append(notifDiv);
  74. const currentTime = playerElem.getCurrentTime();
  75. var searchParams = new URLSearchParams(window.location.search);
  76. searchParams.set("t", "" + Math.floor(currentTime) + "s");
  77. console.info(
  78. "youtube-mute-skip-ads: Reloading with t =",
  79. searchParams.get("t")
  80. );
  81. window.location.search = searchParams.toString();
  82. }
  83. function adBadgeAdded(elem) {
  84. var _a, _b;
  85. const adCounter = (_b = (_a = elem.textContent) == null ? void 0 : _a.match(
  86. /^[^0-9]*([0-9]+)\/([0-9]+)[^0-9]*$/
  87. )) == null ? void 0 : _b[1];
  88. console.debug(
  89. "youtube-mute-skip-ads: Ad badge added with counter =",
  90. adCounter
  91. );
  92. if (adCounter != null && Number(adCounter) > adMaxCount) {
  93. console.info(
  94. "youtube-mute-skip-ads: Ad counter exceeds maximum, reloading page:",
  95. adCounter,
  96. ">",
  97. adMaxCount
  98. );
  99. reloadPage();
  100. }
  101. }
  102. function preskipAdded(elem) {
  103. var _a, _b;
  104. const adTime = (_b = (_a = elem.textContent) == null ? void 0 : _a.match(/^[^0-9]*([0-9]+)[^0-9]*$/)) == null ? void 0 : _b[1];
  105. console.debug(
  106. "youtube-mute-skip-ads: Ad preskip added with countdown =",
  107. adTime
  108. );
  109. if (adTime == null) {
  110. console.info("youtube-mute-skip-ads: No ad countdown, reloading page");
  111. reloadPage();
  112. }
  113. if (Number(adTime) > adMaxTime) {
  114. console.info(
  115. "youtube-mute-skip-ads: Ad countdown exceeds maximum, reloading page:",
  116. adTime,
  117. ">",
  118. adMaxTime
  119. );
  120. reloadPage();
  121. }
  122. }
  123. function clickSkipIfVisible(button) {
  124. const isVisible = button.offsetParent !== null;
  125. if (isVisible) {
  126. console.info("youtube-mute-skip-ads: Skipping");
  127. button.click();
  128. }
  129. return isVisible;
  130. }
  131. function skipAdded(elem) {
  132. var _a;
  133. console.debug("youtube-mute-skip-ads: Skip added");
  134. const button = (_a = elem.getElementsByClassName(skipButtonClass)) == null ? void 0 : _a[0];
  135. if (!(button instanceof HTMLElement)) {
  136. console.error(
  137. "youtube-mute-skip-ads: Failed to find skip button:",
  138. elem.outerHTML
  139. );
  140. return;
  141. }
  142. if (!clickSkipIfVisible(button)) {
  143. console.info("youtube-mute-skip-ads: Skip button is invisible, waiting");
  144. const skipObserver = new MutationObserver(() => {
  145. clickSkipIfVisible(button);
  146. });
  147. skipObserver.observe(elem, {
  148. attributes: true,
  149. attributeFilter: ["style"],
  150. subtree: true
  151. });
  152. }
  153. }
  154. function overlayCloseAdded(elem) {
  155. if (!(elem instanceof HTMLElement)) {
  156. console.error(
  157. "youtube-mute-skip-ads: Overlay close added, not an HTMLElement?",
  158. elem.outerHTML
  159. );
  160. return;
  161. }
  162. console.info("youtube-mute-skip-ads: Overlay close added, clicking");
  163. elem.click();
  164. }
  165. const addedMap = [
  166. { className: adUIClass, func: adUIAdded },
  167. { className: adBadgeClass, func: adBadgeAdded },
  168. { className: preskipClass, func: preskipAdded },
  169. { className: skipContainerClass, func: skipAdded },
  170. { className: overlayCloseButtonClass, func: overlayCloseAdded }
  171. ];
  172. const removedMap = [{ className: adUIClass, func: adUIRemoved }];
  173. for (const { className, func } of addedMap) {
  174. for (const elem of document.getElementsByClassName(className)) {
  175. func(elem);
  176. }
  177. }
  178. const observer = new MutationObserver((mutations) => {
  179. for (const mut of mutations) {
  180. if (mut.type === "childList") {
  181. for (const parentNode of mut.addedNodes) {
  182. for (const { className, func } of addedMap) {
  183. for (const node of getSelfOrChildrenByClassName(
  184. parentNode,
  185. className
  186. )) {
  187. func(node);
  188. }
  189. }
  190. }
  191. for (const parentNode of mut.removedNodes) {
  192. for (const { className, func } of removedMap) {
  193. for (const node of getSelfOrChildrenByClassName(
  194. parentNode,
  195. className
  196. )) {
  197. func(node);
  198. }
  199. }
  200. }
  201. }
  202. }
  203. });
  204. observer.observe(document.body, {
  205. childList: true,
  206. subtree: true
  207. });
  208. })();