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-08-27 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         YouTube Mute and Skip Ads
// @namespace    https://github.com/ion1/userscripts
// @version      0.0.24
// @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==

(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. */
#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,
.ytp-suggested-action-badge,
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,.ytp-suggested-action-badge,ytmusic-app:has(#movie_player.ad-showing) ytmusic-player-bar :is(.title,.subtitle)):is(:hover,:focus-within) {
    filter: none;
  }

#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';

  var __defProp = Object.defineProperty;
  var __typeError = (msg) => {
    throw TypeError(msg);
  };
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
  var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
  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);
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
  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;
  const logPrefix = "youtube-mute-skip-ads:";
  const _Watcher = class _Watcher {
    constructor(name, elem) {
      __privateAdd(this, _Watcher_instances);
      __publicField(this, "name");
      __publicField(this, "element");
      __privateAdd(this, _onCreatedCallbacks);
      __privateAdd(this, _onRemovedCallbacks);
      __privateAdd(this, _nodeObserver);
      __privateAdd(this, _nodeWatchers);
      __privateAdd(this, _textObserver);
      __privateAdd(this, _onTextChangedCallbacks);
      __privateAdd(this, _onAttrChangedCallbacks);
      __publicField(this, "visibilityAncestor");
      __privateAdd(this, _visibilityObserver);
      __privateAdd(this, _isVisible);
      __privateAdd(this, _visibilityWatchers);
      this.name = name;
      this.element = null;
      __privateSet(this, _onCreatedCallbacks, []);
      __privateSet(this, _onRemovedCallbacks, []);
      __privateSet(this, _nodeObserver, null);
      __privateSet(this, _nodeWatchers, []);
      __privateSet(this, _textObserver, null);
      __privateSet(this, _onTextChangedCallbacks, []);
      __privateSet(this, _onAttrChangedCallbacks, []);
      this.visibilityAncestor = null;
      __privateSet(this, _visibilityObserver, null);
      __privateSet(this, _isVisible, null);
      __privateSet(this, _visibilityWatchers, []);
      if (elem != null) {
        __privateMethod(this, _Watcher_instances, connect_fn).call(this, elem);
      }
    }
    onCreated(onCreatedCb) {
      __privateGet(this, _onCreatedCallbacks).push(onCreatedCb);
      if (this.element != null) {
        const onRemovedCb = onCreatedCb(this.element);
        if (onRemovedCb) {
          __privateGet(this, _onRemovedCallbacks).push(onRemovedCb);
        }
      }
      return this;
    }
    descendant(selector, name) {
      var _a;
      const watcher2 = new _Watcher(`${this.name} → ${name}`);
      __privateGet(this, _nodeWatchers).push({ selector, name, watcher: watcher2 });
      if (this.element != null) {
        for (const descElem of getDescendantsBy(this.element, selector, name)) {
          __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, descElem, this.element);
        }
        __privateMethod(this, _Watcher_instances, registerNodeObserver_fn).call(this);
      }
      return watcher2;
    }
    id(idName) {
      return this.descendant("id", idName);
    }
    klass(className) {
      return this.descendant("class", className);
    }
    tag(tagName) {
      return this.descendant("tag", tagName);
    }
    visible() {
      var _a;
      const watcher2 = new _Watcher(`${this.name} (visible)`);
      __privateGet(this, _visibilityWatchers).push(watcher2);
      if (this.element != null) {
        const visibilityAncestor = __privateMethod(this, _Watcher_instances, assertVisibilityAncestor_fn).call(this);
        if (__privateGet(this, _isVisible)) {
          __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, this.element, visibilityAncestor);
        }
        __privateMethod(this, _Watcher_instances, registerVisibilityObserver_fn).call(this);
      }
      return watcher2;
    }
    /// `null` implies null textContent. `undefined` implies that the watcher is
    /// being disconnected.
    text(callback) {
      __privateGet(this, _onTextChangedCallbacks).push(callback);
      if (this.element != null) {
        callback(this.element, this.element.textContent);
        __privateMethod(this, _Watcher_instances, registerTextObserver_fn).call(this);
      }
      return this;
    }
    /// `null` implies no such attribute. `undefined` implies that the watcher is
    /// being disconnected.
    attr(name, callback) {
      __privateGet(this, _onAttrChangedCallbacks).push({ name, callback, observer: null });
      if (this.element != null) {
        callback(this.element, this.element.getAttribute(name));
        __privateMethod(this, _Watcher_instances, registerAttrObservers_fn).call(this);
      }
      return this;
    }
  };
  _onCreatedCallbacks = new WeakMap();
  _onRemovedCallbacks = new WeakMap();
  _nodeObserver = new WeakMap();
  _nodeWatchers = new WeakMap();
  _textObserver = new WeakMap();
  _onTextChangedCallbacks = new WeakMap();
  _onAttrChangedCallbacks = new WeakMap();
  _visibilityObserver = new WeakMap();
  _isVisible = new WeakMap();
  _visibilityWatchers = new WeakMap();
  _Watcher_instances = new WeakSet();
  assertElement_fn = function() {
    if (this.element == null) {
      throw new Error(`Watcher not connected to an element`);
    }
    return this.element;
  };
  assertVisibilityAncestor_fn = function() {
    if (this.visibilityAncestor == null) {
      throw new Error(`Watcher is missing a visibilityAncestor`);
    }
    return this.visibilityAncestor;
  };
  connect_fn = function(element, visibilityAncestor) {
    var _a;
    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 __privateGet(this, _onCreatedCallbacks)) {
      const onRemovedCb = onCreatedCb(this.element);
      if (onRemovedCb) {
        __privateGet(this, _onRemovedCallbacks).push(onRemovedCb);
      }
    }
    for (const { selector, name, watcher: watcher2 } of __privateGet(this, _nodeWatchers)) {
      for (const descElem of getDescendantsBy(this.element, selector, name)) {
        __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, descElem, this.element);
      }
    }
    for (const callback of __privateGet(this, _onTextChangedCallbacks)) {
      callback(this.element, this.element.textContent);
    }
    for (const { name, callback } of __privateGet(this, _onAttrChangedCallbacks)) {
      callback(this.element, this.element.getAttribute(name));
    }
    __privateMethod(this, _Watcher_instances, registerNodeObserver_fn).call(this);
    __privateMethod(this, _Watcher_instances, registerTextObserver_fn).call(this);
    __privateMethod(this, _Watcher_instances, registerAttrObservers_fn).call(this);
    __privateMethod(this, _Watcher_instances, registerVisibilityObserver_fn).call(this);
  };
  disconnect_fn = function() {
    var _a, _b;
    if (this.element == null) {
      return;
    }
    for (const child of __privateGet(this, _nodeWatchers)) {
      __privateMethod(_a = child.watcher, _Watcher_instances, disconnect_fn).call(_a);
    }
    for (const callback of __privateGet(this, _onTextChangedCallbacks)) {
      callback(this.element, void 0);
    }
    for (const { callback } of __privateGet(this, _onAttrChangedCallbacks)) {
      callback(this.element, void 0);
    }
    for (const child of __privateGet(this, _visibilityWatchers)) {
      __privateMethod(_b = child, _Watcher_instances, disconnect_fn).call(_b);
    }
    __privateMethod(this, _Watcher_instances, deregisterNodeObserver_fn).call(this);
    __privateMethod(this, _Watcher_instances, deregisterTextObserver_fn).call(this);
    __privateMethod(this, _Watcher_instances, deregisterAttrObservers_fn).call(this);
    __privateMethod(this, _Watcher_instances, deregisterVisibilityObserver_fn).call(this);
    while (__privateGet(this, _onRemovedCallbacks).length > 0) {
      const onRemovedCb = __privateGet(this, _onRemovedCallbacks).shift();
      onRemovedCb();
    }
    this.element = null;
  };
  registerNodeObserver_fn = function() {
    if (__privateGet(this, _nodeObserver) != null) {
      return;
    }
    if (__privateGet(this, _nodeWatchers).length === 0) {
      return;
    }
    const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
    __privateSet(this, _nodeObserver, new MutationObserver((mutations) => {
      var _a, _b;
      for (const mut of mutations) {
        for (const node of mut.addedNodes) {
          for (const { selector, name, watcher: watcher2 } of __privateGet(this, _nodeWatchers)) {
            for (const descElem of getSelfOrDescendantsBy(
              node,
              selector,
              name
            )) {
              __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, descElem, elem);
            }
          }
        }
        for (const node of mut.removedNodes) {
          for (const { selector, name, watcher: watcher2 } of __privateGet(this, _nodeWatchers)) {
            for (const _descElem of getSelfOrDescendantsBy(
              node,
              selector,
              name
            )) {
              __privateMethod(_b = watcher2, _Watcher_instances, disconnect_fn).call(_b);
            }
          }
        }
      }
    }));
    __privateGet(this, _nodeObserver).observe(elem, {
      subtree: true,
      childList: true
    });
  };
  registerTextObserver_fn = function() {
    if (__privateGet(this, _textObserver) != null) {
      return;
    }
    if (__privateGet(this, _onTextChangedCallbacks).length === 0) {
      return;
    }
    const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
    __privateSet(this, _textObserver, new MutationObserver((_mutations) => {
      for (const callback of __privateGet(this, _onTextChangedCallbacks)) {
        callback(elem, elem.textContent);
      }
    }));
    __privateGet(this, _textObserver).observe(elem, {
      subtree: true,
      // This is needed when elements are replaced to update their text.
      childList: true,
      characterData: true
    });
  };
  registerAttrObservers_fn = function() {
    const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
    for (const handler of __privateGet(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_fn = function() {
    if (__privateGet(this, _visibilityObserver) != null) {
      return;
    }
    if (__privateGet(this, _visibilityWatchers).length === 0) {
      return;
    }
    __privateSet(this, _isVisible, false);
    const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
    const visibilityAncestor = __privateMethod(this, _Watcher_instances, assertVisibilityAncestor_fn).call(this);
    __privateSet(this, _visibilityObserver, new IntersectionObserver(
      (entries) => {
        var _a, _b;
        const oldVisible = __privateGet(this, _isVisible);
        for (const entry of entries) {
          __privateSet(this, _isVisible, entry.isIntersecting);
        }
        if (__privateGet(this, _isVisible) !== oldVisible) {
          if (__privateGet(this, _isVisible)) {
            for (const watcher2 of __privateGet(this, _visibilityWatchers)) {
              __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, elem, visibilityAncestor);
            }
          } else {
            for (const watcher2 of __privateGet(this, _visibilityWatchers)) {
              __privateMethod(_b = watcher2, _Watcher_instances, disconnect_fn).call(_b);
            }
          }
        }
      },
      {
        root: visibilityAncestor
      }
    ));
    __privateGet(this, _visibilityObserver).observe(elem);
  };
  deregisterNodeObserver_fn = function() {
    if (__privateGet(this, _nodeObserver) == null) {
      return;
    }
    __privateGet(this, _nodeObserver).disconnect();
    __privateSet(this, _nodeObserver, null);
  };
  deregisterTextObserver_fn = function() {
    if (__privateGet(this, _textObserver) == null) {
      return;
    }
    __privateGet(this, _textObserver).disconnect();
    __privateSet(this, _textObserver, null);
  };
  deregisterAttrObservers_fn = function() {
    for (const handler of __privateGet(this, _onAttrChangedCallbacks)) {
      if (handler.observer == null) {
        continue;
      }
      handler.observer.disconnect();
      handler.observer = null;
    }
  };
  deregisterVisibilityObserver_fn = function() {
    if (__privateGet(this, _visibilityObserver) == null) {
      return;
    }
    __privateGet(this, _visibilityObserver).disconnect();
    __privateSet(this, _visibilityObserver, null);
    __privateSet(this, _isVisible, null);
  };
  let Watcher = _Watcher;
  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 == null ? void 0 : videoElem.cloneNode(true)
      );
      return null;
    }
    return videoElem;
  }
  function callMoviePlayerMethod(name, onSuccess, args) {
    var _a;
    try {
      const movieElem = document.getElementById("movie_player");
      if (movieElem == null) {
        console.warn(logPrefix, "movie_player element not found");
        return;
      }
      const method = (_a = Object.getOwnPropertyDescriptor(
        movieElem,
        name
      )) == null ? void 0 : _a.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;
    }
    mute(video);
    speedup(video);
    const cancelRemovedCallback = cancelPlayback(video);
    return function onRemoved() {
      cancelRemovedCallback();
    };
  }
  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);
      }
    }
  }
  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");
    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 (duration - currentTime < 1) {
      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.klass("ytp-featured-product").klass("ytp-suggested-action-badge-dismiss-button-icon").visible().onCreated(click("suggested action close"));
  watcher.tag("ytmusic-you-there-renderer").tag("button").visible().onCreated(click("are-you-there"));

})();