YouTube Mute and Skip Ads

Mutes, blurs and skips ads on YouTube. Reloads the page (maintaining the position in the video) 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. Clicks "yes" on "are you there?" on YouTube Music.

目前為 2023-03-11 提交的版本,檢視 最新版本

// ==UserScript==
// @name         YouTube Mute and Skip Ads
// @namespace    https://github.com/ion1/userscripts
// @version      0.0.7
// @author       ion
// @description  Mutes, blurs and skips ads on YouTube. Reloads the page (maintaining the position in the video) 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. Clicks "yes" on "are you there?" on YouTube Music.
// @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/*
// @run-at       document-body
// ==/UserScript==

(n=>{const e=document.createElement("style");e.dataset.source="vite-plugin-monkey",e.innerText=n,document.head.appendChild(e)})(` #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-ad-visit-advertiser-button {
  filter: blur(4px) opacity(0.5) grayscale(0.5);
}

@media (prefers-reduced-motion: no-preference) {

#movie_player.ad-showing .ytp-title,
#movie_player.ad-showing .ytp-title-channel,
.ytp-ad-visit-advertiser-button {
    transition: 0.05s filter linear;
}
  }

: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(0.25) grayscale(0.5);
}

@media (prefers-reduced-motion: no-preference) {

#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 {
    transition: 0.05s filter linear;
}
  }

: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-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;

  animation: none;
}

@media (prefers-reduced-motion: no-preference) {
    .youtube-mute-skip-ads-notification.fade-out {
      animation: youtube-mute-skip-ads-fade-out 3s;
      animation-fill-mode: forwards;
    }
  }

.youtube-mute-skip-ads-notification > div {
    font-size: 3rem;
    line-height: 1.3;
    text-align: center;
    background-color: rgb(0 0 0 / 0.7);
    color: white;
    padding: 2rem;
    border-radius: 1rem;
  }

.youtube-mute-skip-ads-notification > div > :first-child {
      font-size: 6rem;
    }

@keyframes youtube-mute-skip-ads-fade-out {
  0% {
    opacity: 1;
  }
  90% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
 `);

(function() {
  "use strict";
  const main = "";
  const notification = "";
  let visibleElem = null;
  let hideTimerId = null;
  function showNotification(params) {
    const containerElem = document.createElement("div");
    containerElem.setAttribute("class", "youtube-mute-skip-ads-notification");
    const notifElem = document.createElement("div");
    notifElem.setAttribute("aria-live", "assertive");
    notifElem.setAttribute("aria-atomic", "true");
    containerElem.appendChild(notifElem);
    const headerElem = document.createElement("div");
    headerElem.textContent = params.heading;
    notifElem.appendChild(headerElem);
    if (params.description != null) {
      const descrElem = document.createElement("div");
      descrElem.textContent = params.description;
      notifElem.appendChild(descrElem);
    }
    const footerElem = document.createElement("div");
    footerElem.textContent = "(Youtube Mute and Skip Ads)";
    notifElem.appendChild(footerElem);
    if (hideTimerId != null) {
      clearTimeout(hideTimerId);
      hideTimerId = null;
    }
    if (visibleElem != null) {
      document.body.removeChild(visibleElem);
      visibleElem = null;
    }
    document.body.append(containerElem);
    visibleElem = containerElem;
    if (params.fadeOut) {
      containerElem.classList.add("fade-out");
      hideTimerId = setTimeout(() => {
        document.body.removeChild(containerElem);
        visibleElem = null;
        hideTimerId = null;
      }, 3e3);
    }
  }
  const logPrefix = "youtube-mute-skip-ads:";
  const adMaxTime = 7;
  const notificationKey = "youtube-mute-skip-ads-notification";
  const restoreFocusKey = "youtube-mute-skip-ads-restore-focus";
  const playerId = "movie_player";
  const videoSelector = "#movie_player video";
  const muteButtonClass = "ytp-mute-button";
  const adUIClass = "ytp-ad-player-overlay-skip-or-preview";
  const adBadgeClass = "ytp-ad-simple-ad-badge";
  const preskipClass = "ytp-ad-preview-text";
  const skipContainerClass = "ytp-ad-skip-button-slot";
  const skipButtonClass = "ytp-ad-skip-button";
  const overlayCloseButtonClass = "ytp-ad-overlay-close-button";
  const areYouThereTag = "ytmusic-you-there-renderer";
  function reloadNotification(description) {
    showNotification({ heading: "⟳ Reloading", description });
    sessionStorage.setItem(notificationKey, description);
  }
  function reloadedNotification() {
    const description = sessionStorage.getItem(notificationKey);
    sessionStorage.removeItem(notificationKey);
    if (description != null) {
      showNotification({ heading: "✓ Reloaded", description, fadeOut: true });
    }
  }
  reloadedNotification();
  function storeFocusState() {
    var _a;
    const id = (_a = document.activeElement) == null ? void 0 : _a.id;
    if (id != null && id !== "") {
      sessionStorage.setItem(restoreFocusKey, id);
    }
  }
  let focusElementId = sessionStorage.getItem(restoreFocusKey);
  sessionStorage.removeItem(restoreFocusKey);
  function restoreFocusState(elem) {
    console.info(logPrefix, "Restoring focus to", JSON.stringify(elem.id));
    elem.focus();
    focusElementId = null;
  }
  function getSelfOrChildrenBy(node, selector, name) {
    if (!(node instanceof Element)) {
      return [];
    }
    if (selector === "class") {
      if (node.classList.contains(name)) {
        return [node];
      } else {
        return node.getElementsByClassName(name);
      }
    } else if (selector === "tag") {
      if (node.tagName.toLowerCase() === name.toLowerCase()) {
        return [node];
      } else {
        return node.getElementsByTagName(name);
      }
    } else {
      const impossible = selector;
      throw new Error(`Impossible selector: ${JSON.stringify(impossible)}`);
    }
  }
  function setVideoProperties(props) {
    const video = document.querySelector(videoSelector);
    if (!(video instanceof HTMLVideoElement)) {
      console.error(
        logPrefix,
        "Expected",
        JSON.stringify(videoSelector),
        "to be a video element, got:",
        video == null ? void 0 : video.cloneNode(true)
      );
      return;
    }
    if (props.muted != null) {
      video.muted = props.muted;
    }
  }
  function toggleMuteTwice() {
    for (const elem of document.getElementsByClassName(muteButtonClass)) {
      if (!(elem instanceof HTMLElement)) {
        console.error(
          logPrefix,
          "Expected",
          JSON.stringify(muteButtonClass),
          "to be an HTML element, got:",
          elem.cloneNode(true)
        );
        continue;
      }
      elem.click();
      elem.click();
      return;
    }
    console.error(logPrefix, "Failed to find", JSON.stringify(muteButtonClass));
  }
  function adUIAdded(_elem) {
    console.info(logPrefix, "An ad is playing, muting");
    setVideoProperties({ muted: true });
  }
  function adUIRemoved(_elem) {
    console.info(logPrefix, "An ad is no longer playing, unmuting");
    toggleMuteTwice();
  }
  function reloadPage(description) {
    const playerElem = document.getElementById(playerId);
    if (playerElem == null) {
      console.error(
        logPrefix,
        "Expected",
        JSON.stringify(playerId),
        "to be a player element, got:",
        playerElem
      );
      return;
    }
    if (!("getCurrentTime" in playerElem)) {
      console.error(
        logPrefix,
        "The player element doesn't have getCurrentTime:",
        playerElem.cloneNode(true)
      );
      return;
    }
    const currentTime = playerElem.getCurrentTime();
    if (typeof currentTime !== "number") {
      console.error(
        logPrefix,
        "Expected a number, getCurrentTime returned:",
        currentTime
      );
      return;
    }
    reloadNotification(description);
    storeFocusState();
    var searchParams = new URLSearchParams(window.location.search);
    searchParams.set("t", `${Math.floor(currentTime)}s`);
    console.info(logPrefix, "Reloading with t =", searchParams.get("t"));
    window.location.search = searchParams.toString();
  }
  function adBadgeAdded(elem) {
    var _a, _b, _c;
    const numbers = (((_a = elem.textContent) == null ? void 0 : _a.match(/[0-9]+/g)) ?? []).map(Number);
    console.debug(
      logPrefix,
      "Ad badge added with text =",
      JSON.stringify(elem.textContent),
      "numbers =",
      JSON.stringify(numbers)
    );
    if (numbers.length > 0 && !numbers.includes(1)) {
      console.info(logPrefix, "Ad counter exceeds 1, reloading page");
      const adCounterText = (_c = (_b = elem.textContent) == null ? void 0 : _b.match(/[0-9](?:.*[0-9])?/)) == null ? void 0 : _c[0];
      reloadPage(`Ad counter: ${adCounterText}`);
    }
  }
  function preskipAdded(elem) {
    var _a, _b;
    const adTime = (_b = (_a = elem.textContent) == null ? void 0 : _a.match(/^[^0-9]*([0-9]+)[^0-9]*$/)) == null ? void 0 : _b[1];
    console.debug(
      logPrefix,
      "Ad preskip added with countdown =",
      adTime,
      ":",
      elem.cloneNode(true)
    );
    if (adTime == null) {
      console.info(logPrefix, "No ad countdown, reloading page");
      reloadPage("No ad countdown");
    }
    if (Number(adTime) > adMaxTime) {
      console.info(
        logPrefix,
        "Ad countdown exceeds maximum, reloading page:",
        adTime,
        ">",
        adMaxTime
      );
      reloadPage(`Ad countdown: ${Number(adTime)} s`);
    }
  }
  function clickSkipIfVisible(button) {
    const isVisible = button.offsetParent !== null;
    if (isVisible) {
      console.info(logPrefix, "Skipping");
      button.click();
    }
    return isVisible;
  }
  function skipAdded(elem) {
    var _a;
    console.debug(logPrefix, "Skip added");
    const button = (_a = elem.getElementsByClassName(skipButtonClass)) == null ? void 0 : _a[0];
    if (!(button instanceof HTMLElement)) {
      console.error(
        logPrefix,
        "Expected",
        JSON.stringify(skipButtonClass),
        "to be an HTML element, got:",
        elem.cloneNode(true)
      );
      return;
    }
    if (!clickSkipIfVisible(button)) {
      console.info(logPrefix, "Skip button is invisible, waiting");
      const skipObserver = new MutationObserver(() => {
        clickSkipIfVisible(button);
      });
      skipObserver.observe(elem, {
        attributes: true,
        attributeFilter: ["style"],
        subtree: true
      });
    }
  }
  function overlayCloseAdded(elem) {
    if (!(elem instanceof HTMLElement)) {
      console.error(
        logPrefix,
        "Expected overlay close to be an HTML element, got:",
        elem.cloneNode(true)
      );
      return;
    }
    console.info(logPrefix, "Overlay close added, clicking");
    elem.click();
  }
  function areYouThereAdded(elem) {
    for (const button of elem.getElementsByTagName("button")) {
      console.info(logPrefix, "Are-you-there added, clicking button");
      button.click();
      return;
    }
    console.info(
      logPrefix,
      "Are-you-there added but doesn't have a button yet, waiting"
    );
    const buttonObserver = new MutationObserver((mutations) => {
      for (const mut of mutations) {
        if (mut.type === "childList") {
          for (const parentNode of mut.addedNodes) {
            for (const button of getSelfOrChildrenBy(
              parentNode,
              "tag",
              "button"
            )) {
              if (!(button instanceof HTMLElement)) {
                console.error(
                  logPrefix,
                  "Are-you-there button: expected an HTML element, got:",
                  button.cloneNode(true)
                );
                continue;
              }
              console.info(logPrefix, "Are-you-there button added, clicking");
              button.click();
              buttonObserver.disconnect();
              return;
            }
          }
        }
      }
    });
    buttonObserver.observe(elem, {
      childList: true,
      subtree: true
    });
  }
  const addedMap = [
    { selector: "class", name: adUIClass, func: adUIAdded },
    { selector: "class", name: adBadgeClass, func: adBadgeAdded },
    { selector: "class", name: preskipClass, func: preskipAdded },
    { selector: "class", name: skipContainerClass, func: skipAdded },
    { selector: "class", name: overlayCloseButtonClass, func: overlayCloseAdded },
    { selector: "tag", name: areYouThereTag, func: areYouThereAdded }
  ];
  const removedMap = [
    { selector: "class", name: adUIClass, func: adUIRemoved }
  ];
  for (const { selector, name, func } of addedMap) {
    for (const elem of getSelfOrChildrenBy(document.body, selector, name)) {
      func(elem);
    }
  }
  const observer = new MutationObserver((mutations) => {
    if (focusElementId != null) {
      const elem = document.getElementById(focusElementId);
      if (elem) {
        restoreFocusState(elem);
      }
    }
    for (const mut of mutations) {
      if (mut.type === "childList") {
        for (const parentNode of mut.addedNodes) {
          for (const { selector, name, func } of addedMap) {
            for (const node of getSelfOrChildrenBy(parentNode, selector, name)) {
              func(node);
            }
          }
        }
        for (const parentNode of mut.removedNodes) {
          for (const { selector, name, func } of removedMap) {
            for (const node of getSelfOrChildrenBy(parentNode, selector, name)) {
              func(node);
            }
          }
        }
      }
    }
  });
  observer.observe(document.body, {
    childList: true,
    subtree: true
  });
})();

QingJ © 2025

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