Greasy Fork is available in English.

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.

// ==UserScript==
// @name         YouTube Mute and Skip Ads
// @namespace    https://github.com/ion1/userscripts
// @version      0.0.28
// @author       ion
// @description  Mutes, blurs and skips ads on YouTube. Speeds up ad playback. Clicks "yes" on "are you there?" on YouTube Music.
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @homepage     https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads
// @homepageURL  https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads
// @match        *://www.youtube.com/*
// @match        *://music.youtube.com/*
// @grant        GM_addStyle
// @run-at       document-body
// ==/UserScript==

(n=>{if(typeof GM_addStyle=="function"){GM_addStyle(n);return}const e=document.createElement("style");e.textContent=n,document.head.append(e)})(` /* Keep these in sync with the watchers. */
#movie_player
  :is(.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-skip-ad-button) {
  anchor-name: --youtube-mute-skip-ads-unclickable-button;
}

body:has(
    #movie_player
      :is(
        .ytp-ad-skip-button,
        .ytp-ad-skip-button-modern,
        .ytp-skip-ad-button
      ):not([style*="display: none"], [aria-hidden="true"])
  )::after {
  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"
    "Unfortunately, YouTube has started to block automated clicks based on isTrusted being false.\\A\\A"
    "Please click on the skip button manually.";
  white-space: pre-line;
  pointer-events: none;
  z-index: 9999;
  position: fixed;
  position-anchor: --youtube-mute-skip-ads-unclickable-button;
  padding: 1.5em;
  border-radius: 1.5em;
  margin-bottom: 1em;
  bottom: anchor(--youtube-mute-skip-ads-unclickable-button top);
  right: anchor(--youtube-mute-skip-ads-unclickable-button right);
  max-width: 25em;
  font-size: 1.4rem;
  line-height: 2rem;
  font-weight: 400;
  color: rgb(240 240 240);
  background-color: rgb(0 0 0 / 0.7);
  backdrop-filter: blur(10px);
  animation: fade-in 3s linear;
}

@keyframes fade-in {
  0% {
    opacity: 0;
  }
  67% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

#movie_player.ad-showing video {
  filter: blur(100px) opacity(0.25) grayscale(0.5);
}

#movie_player.ad-showing .ytp-title,
#movie_player.ad-showing .ytp-title-channel,
.ytp-visit-advertiser-link,
.ytp-ad-visit-advertiser-button,
ytmusic-app:has(#movie_player.ad-showing)
  ytmusic-player-bar
  :is(.title, .subtitle) {
  filter: blur(4px) opacity(0.5) grayscale(0.5);
  transition: 0.05s filter linear;
}

:is(#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-visit-advertiser-link,.ytp-ad-visit-advertiser-button,ytmusic-app:has(#movie_player.ad-showing) ytmusic-player-bar :is(.title,.subtitle)):is(:hover,:focus-within) {
    filter: none;
  }

/* These popups are showing up on top of the video with a hidden dismiss button
 * since 2024-09-25.
 */
.ytp-suggested-action-badge {
  visibility: hidden !important;
}

#movie_player.ad-showing .caption-window,
.ytp-ad-player-overlay-flyout-cta,
.ytp-ad-player-overlay-layout__player-card-container, /* Seen since 2024-04-06. */
.ytp-ad-action-interstitial-slot, /* Added on 2024-08-25. */
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 {
  filter: blur(10px) opacity(0.25) grayscale(0.5);
  transition: 0.05s filter linear;
}

: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) {
    filter: none;
  }

.ytp-ad-action-interstitial-background-container /* Added on 2024-08-25. */ {
  /* An image ad in place of the video. */
  filter: blur(10px) opacity(0.25) grayscale(0.5);
} `);

(function () {
  'use strict';

  const logPrefix = "youtube-mute-skip-ads:";
  class Watcher {
    name;
    element;
    #onCreatedCallbacks;
    #onRemovedCallbacks;
    #nodeObserver;
    #nodeWatchers;
    #textObserver;
    #onTextChangedCallbacks;
    #onAttrChangedCallbacks;
    visibilityAncestor;
    #visibilityObserver;
    #isVisible;
    #visibilityWatchers;
    constructor(name, elem) {
      this.name = name;
      this.element = null;
      this.#onCreatedCallbacks = [];
      this.#onRemovedCallbacks = [];
      this.#nodeObserver = null;
      this.#nodeWatchers = [];
      this.#textObserver = null;
      this.#onTextChangedCallbacks = [];
      this.#onAttrChangedCallbacks = [];
      this.visibilityAncestor = null;
      this.#visibilityObserver = null;
      this.#isVisible = null;
      this.#visibilityWatchers = [];
      if (elem != null) {
        this.#connect(elem);
      }
    }
    #assertElement() {
      if (this.element == null) {
        throw new Error(`Watcher not connected to an element`);
      }
      return this.element;
    }
    #assertVisibilityAncestor() {
      if (this.visibilityAncestor == null) {
        throw new Error(`Watcher is missing a visibilityAncestor`);
      }
      return this.visibilityAncestor;
    }
    #connect(element, visibilityAncestor) {
      if (this.element != null) {
        if (this.element !== element) {
          console.error(
            logPrefix,
            `Watcher already connected to`,
            this.element,
            `while trying to connect to`,
            element
          );
        }
        return;
      }
      this.element = element;
      this.visibilityAncestor = visibilityAncestor ?? null;
      for (const onCreatedCb of this.#onCreatedCallbacks) {
        const onRemovedCb = onCreatedCb(this.element);
        if (onRemovedCb) {
          this.#onRemovedCallbacks.push(onRemovedCb);
        }
      }
      for (const { selector, name, watcher: watcher2 } of this.#nodeWatchers) {
        for (const descElem of getDescendantsBy(this.element, selector, name)) {
          watcher2.#connect(descElem, this.element);
        }
      }
      for (const callback of this.#onTextChangedCallbacks) {
        callback(this.element, this.element.textContent);
      }
      for (const { name, callback } of this.#onAttrChangedCallbacks) {
        callback(this.element, this.element.getAttribute(name));
      }
      this.#registerNodeObserver();
      this.#registerTextObserver();
      this.#registerAttrObservers();
      this.#registerVisibilityObserver();
    }
    #disconnect() {
      if (this.element == null) {
        return;
      }
      for (const child of this.#nodeWatchers) {
        child.watcher.#disconnect();
      }
      for (const callback of this.#onTextChangedCallbacks) {
        callback(this.element, void 0);
      }
      for (const { callback } of this.#onAttrChangedCallbacks) {
        callback(this.element, void 0);
      }
      for (const child of this.#visibilityWatchers) {
        child.#disconnect();
      }
      this.#deregisterNodeObserver();
      this.#deregisterTextObserver();
      this.#deregisterAttrObservers();
      this.#deregisterVisibilityObserver();
      while (this.#onRemovedCallbacks.length > 0) {
        const onRemovedCb = this.#onRemovedCallbacks.shift();
        onRemovedCb();
      }
      this.element = null;
    }
    #registerNodeObserver() {
      if (this.#nodeObserver != null) {
        return;
      }
      if (this.#nodeWatchers.length === 0) {
        return;
      }
      const elem = this.#assertElement();
      this.#nodeObserver = new MutationObserver((mutations) => {
        for (const mut of mutations) {
          for (const node of mut.addedNodes) {
            for (const { selector, name, watcher: watcher2 } of this.#nodeWatchers) {
              for (const descElem of getSelfOrDescendantsBy(
                node,
                selector,
                name
              )) {
                watcher2.#connect(descElem, elem);
              }
            }
          }
          for (const node of mut.removedNodes) {
            for (const { selector, name, watcher: watcher2 } of this.#nodeWatchers) {
              for (const _descElem of getSelfOrDescendantsBy(
                node,
                selector,
                name
              )) {
                watcher2.#disconnect();
              }
            }
          }
        }
      });
      this.#nodeObserver.observe(elem, {
        subtree: true,
        childList: true
      });
    }
    #registerTextObserver() {
      if (this.#textObserver != null) {
        return;
      }
      if (this.#onTextChangedCallbacks.length === 0) {
        return;
      }
      const elem = this.#assertElement();
      this.#textObserver = new MutationObserver((_mutations) => {
        for (const callback of this.#onTextChangedCallbacks) {
          callback(elem, elem.textContent);
        }
      });
      this.#textObserver.observe(elem, {
        subtree: true,
        // This is needed when elements are replaced to update their text.
        childList: true,
        characterData: true
      });
    }
    #registerAttrObservers() {
      const elem = this.#assertElement();
      for (const handler of this.#onAttrChangedCallbacks) {
        if (handler.observer != null) {
          continue;
        }
        const { name, callback } = handler;
        handler.observer = new MutationObserver((_mutations) => {
          callback(elem, elem.getAttribute(name));
        });
        handler.observer.observe(elem, {
          attributes: true,
          attributeFilter: [name]
        });
      }
    }
    #registerVisibilityObserver() {
      if (this.#visibilityObserver != null) {
        return;
      }
      if (this.#visibilityWatchers.length === 0) {
        return;
      }
      this.#isVisible = false;
      const elem = this.#assertElement();
      const visibilityAncestor = this.#assertVisibilityAncestor();
      this.#visibilityObserver = new IntersectionObserver(
        (entries) => {
          const oldVisible = this.#isVisible;
          for (const entry of entries) {
            this.#isVisible = entry.isIntersecting;
          }
          if (this.#isVisible !== oldVisible) {
            if (this.#isVisible) {
              for (const watcher2 of this.#visibilityWatchers) {
                watcher2.#connect(elem, visibilityAncestor);
              }
            } else {
              for (const watcher2 of this.#visibilityWatchers) {
                watcher2.#disconnect();
              }
            }
          }
        },
        {
          root: visibilityAncestor
        }
      );
      this.#visibilityObserver.observe(elem);
    }
    #deregisterNodeObserver() {
      if (this.#nodeObserver == null) {
        return;
      }
      this.#nodeObserver.disconnect();
      this.#nodeObserver = null;
    }
    #deregisterTextObserver() {
      if (this.#textObserver == null) {
        return;
      }
      this.#textObserver.disconnect();
      this.#textObserver = null;
    }
    #deregisterAttrObservers() {
      for (const handler of this.#onAttrChangedCallbacks) {
        if (handler.observer == null) {
          continue;
        }
        handler.observer.disconnect();
        handler.observer = null;
      }
    }
    #deregisterVisibilityObserver() {
      if (this.#visibilityObserver == null) {
        return;
      }
      this.#visibilityObserver.disconnect();
      this.#visibilityObserver = null;
      this.#isVisible = null;
    }
    onCreated(onCreatedCb) {
      this.#onCreatedCallbacks.push(onCreatedCb);
      if (this.element != null) {
        const onRemovedCb = onCreatedCb(this.element);
        if (onRemovedCb) {
          this.#onRemovedCallbacks.push(onRemovedCb);
        }
      }
      return this;
    }
    descendant(selector, name) {
      const watcher2 = new Watcher(`${this.name} → ${name}`);
      this.#nodeWatchers.push({ selector, name, watcher: watcher2 });
      if (this.element != null) {
        for (const descElem of getDescendantsBy(this.element, selector, name)) {
          watcher2.#connect(descElem, this.element);
        }
        this.#registerNodeObserver();
      }
      return watcher2;
    }
    id(idName) {
      return this.descendant("id", idName);
    }
    klass(className) {
      return this.descendant("class", className);
    }
    tag(tagName) {
      return this.descendant("tag", tagName);
    }
    visible() {
      const watcher2 = new Watcher(`${this.name} (visible)`);
      this.#visibilityWatchers.push(watcher2);
      if (this.element != null) {
        const visibilityAncestor = this.#assertVisibilityAncestor();
        if (this.#isVisible) {
          watcher2.#connect(this.element, visibilityAncestor);
        }
        this.#registerVisibilityObserver();
      }
      return watcher2;
    }
    /// `null` implies null textContent. `undefined` implies that the watcher is
    /// being disconnected.
    text(callback) {
      this.#onTextChangedCallbacks.push(callback);
      if (this.element != null) {
        callback(this.element, this.element.textContent);
        this.#registerTextObserver();
      }
      return this;
    }
    /// `null` implies no such attribute. `undefined` implies that the watcher is
    /// being disconnected.
    attr(name, callback) {
      this.#onAttrChangedCallbacks.push({ name, callback, observer: null });
      if (this.element != null) {
        callback(this.element, this.element.getAttribute(name));
        this.#registerAttrObservers();
      }
      return this;
    }
  }
  function getSelfOrDescendantsBy(node, selector, name) {
    if (!(node instanceof HTMLElement)) {
      return [];
    }
    if (selector === "id" || selector === "class" || selector === "tag") {
      if (selector === "id" && node.id === name || selector === "class" && node.classList.contains(name) || selector === "tag" && node.tagName.toLowerCase() === name.toLowerCase()) {
        return [node];
      } else {
        return getDescendantsBy(node, selector, name);
      }
    } else {
      const impossible = selector;
      throw new Error(`Impossible selector type: ${JSON.stringify(impossible)}`);
    }
  }
  function getDescendantsBy(node, selector, name) {
    if (!(node instanceof HTMLElement)) {
      return [];
    }
    let cssSelector = "";
    if (selector === "id") {
      cssSelector += "#";
    } else if (selector === "class") {
      cssSelector += ".";
    } else if (selector === "tag") ;
    else {
      const impossible = selector;
      throw new Error(`Impossible selector type: ${JSON.stringify(impossible)}`);
    }
    cssSelector += CSS.escape(name);
    return Array.from(node.querySelectorAll(cssSelector));
  }
  const videoSelector = "#movie_player video";
  function getVideoElement() {
    const videoElem = document.querySelector(videoSelector);
    if (!(videoElem instanceof HTMLVideoElement)) {
      console.error(
        logPrefix,
        "Expected",
        JSON.stringify(videoSelector),
        "to be a video element, got:",
        videoElem?.cloneNode(true)
      );
      return null;
    }
    return videoElem;
  }
  function callMoviePlayerMethod(name, onSuccess, args) {
    try {
      const movieElem = document.getElementById("movie_player");
      if (movieElem == null) {
        console.warn(logPrefix, "movie_player element not found");
        return;
      }
      const method = Object.getOwnPropertyDescriptor(
        movieElem,
        name
      )?.value;
      if (method == null) {
        console.warn(
          logPrefix,
          `movie_player element has no ${JSON.stringify(name)} property`
        );
        return;
      }
      if (!(typeof method === "function")) {
        console.warn(
          logPrefix,
          `movie_player element property ${JSON.stringify(name)} is not a function`
        );
        return;
      }
      const result = method.apply(movieElem, args);
      if (onSuccess != null) {
        onSuccess(result);
      }
      return result;
    } catch (e) {
      console.warn(
        logPrefix,
        `movie_player method ${JSON.stringify(name)} failed:`,
        e
      );
      return;
    }
  }
  function disableVisibilityChecks() {
    for (const eventName of ["visibilitychange", "blur", "focus"]) {
      document.addEventListener(
        eventName,
        (ev) => {
          ev.stopImmediatePropagation();
        },
        { capture: true }
      );
    }
    document.hasFocus = () => true;
    Object.defineProperties(document, {
      visibilityState: { value: "visible" },
      hidden: { value: false }
    });
  }
  function adIsPlaying(_elem) {
    console.info(logPrefix, "An ad is playing, muting and speeding up");
    const video = getVideoElement();
    if (video == null) {
      return;
    }
    const onRemovedCallbacks = [
      mute(video),
      speedup(video),
      cancelPlayback(video)
    ];
    return function onRemoved() {
      for (const callback of onRemovedCallbacks) {
        callback();
      }
    };
  }
  function mute(video) {
    console.debug(logPrefix, "Muting");
    video.muted = true;
    return unmute;
  }
  function unmute() {
    {
      return;
    }
  }
  function speedup(video) {
    for (let rate = 16; rate >= 2; rate /= 2) {
      try {
        video.playbackRate = rate;
        break;
      } catch (e) {
        console.debug(logPrefix, `Setting playback rate to`, rate, `failed:`, e);
      }
    }
    return function onRemoved() {
      const originalRate = callMoviePlayerMethod("getPlaybackRate");
      if (originalRate == null || typeof originalRate !== "number" || isNaN(originalRate)) {
        console.warn(
          logPrefix,
          `Restoring playback rate failed:`,
          `unable to query the current playback rate, got: ${JSON.stringify(originalRate)}.`,
          `Falling back to 1.`
        );
        restorePlaybackRate(video, 1);
        return;
      }
      restorePlaybackRate(video, originalRate);
    };
  }
  function restorePlaybackRate(video, originalRate) {
    try {
      video.playbackRate = originalRate;
    } catch (e) {
      console.debug(
        logPrefix,
        `Restoring playback rate to`,
        originalRate,
        `failed:`,
        e
      );
    }
  }
  function cancelPlayback(video) {
    let shouldResume = false;
    function doCancelPlayback() {
      console.info(logPrefix, "Attempting to cancel playback");
      callMoviePlayerMethod("cancelPlayback", () => {
        shouldResume = true;
      });
    }
    if (video.paused) {
      console.debug(
        logPrefix,
        "Ad paused, waiting for it to play before canceling playback"
      );
      video.addEventListener("play", doCancelPlayback);
    } else {
      doCancelPlayback();
    }
    return function onRemoved() {
      video.removeEventListener("play", doCancelPlayback);
      if (shouldResume) {
        resumePlaybackIfNotAtEnd();
      }
    };
  }
  function resumePlaybackIfNotAtEnd() {
    const currentTime = callMoviePlayerMethod("getCurrentTime");
    const duration = callMoviePlayerMethod("getDuration");
    const isAtLiveHead = callMoviePlayerMethod("isAtLiveHead");
    if (currentTime == null || duration == null || typeof currentTime !== "number" || typeof duration !== "number" || isNaN(currentTime) || isNaN(duration)) {
      console.warn(
        logPrefix,
        `movie_player methods getCurrentTime/getDuration failed, got time: ${JSON.stringify(currentTime)}, duration: ${JSON.stringify(duration)}`
      );
      return;
    }
    if (isAtLiveHead == null || typeof isAtLiveHead !== "boolean") {
      console.warn(
        logPrefix,
        `movie_player method isAtLiveHead failed, got: ${JSON.stringify(isAtLiveHead)}`
      );
      return;
    }
    const atEnd = duration - currentTime < 1;
    if (atEnd && !isAtLiveHead) {
      console.info(
        logPrefix,
        `Video is at the end (${currentTime}/${duration}), not attempting to resume playback`
      );
      return;
    }
    console.info(logPrefix, "Attempting to resume playback");
    callMoviePlayerMethod("playVideo");
  }
  function click(description) {
    return (elem) => {
      if (elem.getAttribute("aria-hidden")) {
        console.info(logPrefix, "Not clicking (aria-hidden):", description);
      } else {
        console.info(logPrefix, "Clicking:", description);
        elem.click();
      }
    };
  }
  disableVisibilityChecks();
  const watcher = new Watcher("body", document.body);
  const adPlayerOverlayClasses = [
    "ytp-ad-player-overlay",
    "ytp-ad-player-overlay-layout"
    // Seen since 2024-04-06.
  ];
  for (const adPlayerOverlayClass of adPlayerOverlayClasses) {
    watcher.klass(adPlayerOverlayClass).onCreated(adIsPlaying);
  }
  const adSkipButtonClasses = [
    "ytp-ad-skip-button",
    "ytp-ad-skip-button-modern",
    // Seen since 2023-11-10.
    "ytp-skip-ad-button"
    // Seen since 2024-04-06.
  ];
  for (const adSkipButtonClass of adSkipButtonClasses) {
    watcher.id("movie_player").klass(adSkipButtonClass).visible().attr("aria-hidden", (elem, value) => {
      if (value === null) {
        click(`skip (${adSkipButtonClass})`)(elem);
      }
    });
  }
  watcher.klass("ytp-ad-overlay-close-button").visible().onCreated(click("overlay close"));
  watcher.tag("ytmusic-you-there-renderer").tag("button").visible().onCreated(click("are-you-there"));

})();