🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Greasy Fork is available in English.

📂 缓存分发状态(共享加速已生效)
🕒 页面同步时间:2026/01/11 15:06:25
🔄 下次更新时间:2026/01/11 16:06:25
手动刷新缓存

Auto Close YouTube Ads

Close and/or Mute YouTube ads automatically!

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         Auto Close YouTube Ads
// @namespace    fuz/acya
// @version      1.4.8
// @description  Close and/or Mute YouTube ads automatically!
// @author       fuzetsu
// @run-at       document-body
// @match        *://*.youtube.com/*
// @exclude      *://*.youtube.com/subscribe_embed?*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @require      https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
// @require      https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@23586fd0a72b587a1786f7bb9088e807a5b53e79/libs/gm_config.js
// ==/UserScript==
/* globals GM_getValue GM_setValue GM_deleteValue GM_registerMenuCommand GM_config waitForElems waitForUrl */
/**
 * This section of the code holds the css selectors that point different parts of YouTube's
 * user interface. If the script ever breaks and you don't want to wait for me to fix it
 * chances are that it can be fixed by just updating these selectors here.
 */
const CSS = {
  // the button used to skip an ad
  skipButton:
    '.videoAdUiSkipButton,.ytp-ad-skip-button,.ytp-ad-skip-button-modern,.ytp-skip-ad-button',
  // the area showing the countdown to the skip button showing
  preSkipButton: '.videoAdUiPreSkipButton,.ytp-ad-preview-container,.ytp-preview-ad',
  // little x that closes banner ads
  closeBannerAd: '.close-padding.contains-svg,a.close-button,.ytp-ad-overlay-close-button',
  // button that toggle mute on the video
  muteButton: '.ytp-mute-button',
  // the slider bar handle that represents the current volume
  muteIndicator: '.ytp-volume-slider-handle',
  // container for ad on video
  adArea: '.videoAdUi,.ytp-ad-player-overlay,.ytp-ad-player-overlay-layout',
  // container that shows ad length eg 3:23
  adLength: '.videoAdUiAttribution,.ytp-ad-duration-remaining',
  // container for header ad on the home page
  homeAdContainer: '#masthead-ad'
}

const util = {
  log: (...args) => console.log(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: purple;', ...args),
  clearTicks: ticks => {
    ticks.forEach(tick =>
      !tick ? null : typeof tick === 'number' ? clearInterval(tick) : tick.stop()
    )
    ticks.length = 0
  },
  keepTrying: (wait, action) => {
    const tick = setInterval(() => action() && clearInterval(tick), wait)
    return tick
  },
  storeGet: key => {
    if (typeof GM_getValue === 'undefined') {
      const value = localStorage.getItem(key)
      return value === 'true' ? true : value === 'false' ? false : value
    }
    return GM_getValue(key)
  },
  storeSet: (key, value) =>
    typeof GM_setValue === 'undefined' ? localStorage.setItem(key, value) : GM_setValue(key, value),
  storeDel: key =>
    typeof GM_deleteValue === 'undefined' ? localStorage.removeItem(key) : GM_deleteValue(key),
  q: (query, context) => (context || document).querySelector(query),
  qq: (query, context) => Array.from((context || document).querySelectorAll(query)),
  get: (obj, str) => util.getPath(obj, str.split('.').reverse()),
  getPath: (obj, path) =>
    obj == null ? null : path.length > 0 ? util.getPath(obj[path.pop()], path) : obj
}

const SCRIPT_NAME = 'Auto Close YouTube Ads'
const SHORT_AD_MSG_LENGTH = 12000
const TICKS = []
let DONT_SKIP = false
const CONFIG_VERSION = 2

const config = GM_config([
  {
    key: 'muteAd',
    label: 'Mute ads?',
    type: 'bool',
    default: true
  },
  {
    key: 'hideAd',
    label: 'Hide video ads?',
    type: 'bool',
    default: false
  },
  {
    key: 'secWaitBanner',
    label: 'Banner ad close delay (seconds)',
    type: 'number',
    default: 3,
    min: 0
  },
  {
    key: 'secWaitVideo',
    label: 'Video ad skip delay (seconds)',
    type: 'number',
    default: 3,
    min: 0
  },
  {
    key: 'minAdLengthForSkip',
    label: 'Dont skip video shorter than this (seconds)',
    type: 'number',
    default: 0,
    min: 0
  },
  {
    key: 'muteEvenIfNotSkipping',
    label: 'Mute video even if not skipping',
    type: 'bool',
    default: true
  },
  {
    key: 'debug',
    label: 'Show extra debug information.',
    type: 'bool',
    default: false
  },
  {
    key: 'version',
    type: 'hidden',
    default: CONFIG_VERSION
  }
])

let conf = config.load()

config.onsave = cfg => (conf = cfg)

function createMessageElement() {
  const elem = document.createElement('div')
  elem.setAttribute(
    'style',
    'border: 1px solid white;border-right: none;background: rgb(0,0,0,0.75);color:white;position: absolute;right: 0;z-index: 1000;top: 30px;padding: 10px;padding-right: 20px;cursor: pointer;pointer-events: all;'
  )
  return elem
}
function showMessage(container, text, ms) {
  const message = createMessageElement()
  message.textContent = text
  container.appendChild(message)
  util.log(`showing message [${ms}ms]: ${text}`)
  setTimeout(() => message.remove(), ms)
}

function setupCancelDiv(ad) {
  const skipArea = util.q(CSS.preSkipButton, ad)
  const skipText = skipArea && skipArea.textContent.trim().replace(/\s+/g, ' ')
  if (skipText) {
    if (['will begin', 'will play', 'plays soon'].some(snip => skipText.includes(snip))) return
    const cancelClass = 'acya-cancel-skip'
    let cancelDiv = util.q('.' + cancelClass)
    if (cancelDiv) cancelDiv.remove()
    cancelDiv = createMessageElement()
    cancelDiv.className = cancelClass
    cancelDiv.textContent = (conf.muteAd ? 'Un-mute & ' : '') + 'Cancel Auto Skip'
    cancelDiv.onclick = () => {
      util.log('cancel clicked')
      DONT_SKIP = true
      cancelDiv.remove()
      if (conf.hideAd) {
        ad.style.zIndex = ''
        ad.style.background = ''
      }
      const muteButton = getMuteButton()
      const muteIndicator = getMuteIndicator()
      if (conf.muteAd && muteButton && muteIndicator && isMuted(muteIndicator)) muteButton.click()
    }
    ad.appendChild(cancelDiv)
  } else {
    util.log("skip button area wasn't there for some reason.. couldn't place cancel button.")
  }
}

function parseTime(str) {
  const [minutes, seconds] = str
    .split(' ')
    .pop()
    .split(':')
    .map(num => parseInt(num))
  util.log(str, minutes, seconds)
  return minutes * 60 + seconds || 0
}

const getMuteButton = () => util.qq(CSS.muteButton).find(elem => elem.offsetParent)
const getMuteIndicator = () => util.qq(CSS.muteIndicator).find(elem => elem.offsetParent)
const isMuted = m => m.style.left === '0px'

function getAdLength(ad) {
  if (!ad) return 0
  const time = ad.querySelector(CSS.adLength)
  return time ? parseTime(time.textContent) : 0
}

function waitForAds() {
  DONT_SKIP = false
  TICKS.push(
    waitForElems({
      sel: CSS.skipButton,
      onmatch: btn => {
        util.log('found skip button')
        util.keepTrying(500, () => {
          if (!btn) return true
          // if not visible
          if (btn.offsetParent == null) return
          setTimeout(() => {
            if (DONT_SKIP) {
              util.log('not skipping...')
              DONT_SKIP = false
              return
            }
            util.log('clicking skip button')
            btn.click()
          }, conf.secWaitVideo * 1000)
          return true
        })
      }
    }),
    waitAndClick(CSS.closeBannerAd, conf.secWaitBanner * 1000),
    waitForElems({
      sel: CSS.adArea,
      onmatch: ad => {
        util.log('Video ad detected')
        // reset don't skip
        DONT_SKIP = false
        const adLength = getAdLength(ad)
        const isShort = adLength < conf.minAdLengthForSkip
        const debug = () =>
          conf.debug
            ? `[DEBUG adLength = ${adLength}, minAdLengthForSkip = ${conf.minAdLengthForSkip}]`
            : ''
        if (isShort && !conf.muteEvenIfNotSkipping) {
          DONT_SKIP = true
          return showMessage(
            ad,
            `Shot AD detected, will not skip or mute. ${debug()}`,
            SHORT_AD_MSG_LENGTH
          )
        }
        if (conf.hideAd) {
          ad.style.zIndex = 10
          ad.style.background = 'black'
        }
        // show option to cancel automatic skip
        if (!isShort) setupCancelDiv(ad)
        if (!conf.muteAd) return
        const muteButton = getMuteButton()
        const muteIndicator = getMuteIndicator()
        if (!muteIndicator) return util.log('unable to determine mute state, skipping mute')
        if (isMuted(muteIndicator)) {
          util.log('Audio is already muted')
        } else {
          util.log('Muting audio')
          muteButton.click()
        }
        // wait for the ad to disappear before unmuting
        util.keepTrying(250, () => {
          if (!ad.offsetParent) {
            if (isMuted(muteIndicator)) {
              muteButton.click()
              util.log('Video ad ended, unmuting audio')
            } else {
              util.log('Video ad ended, audio already unmuted')
            }
            return true
          }
        })
        if (isShort) {
          DONT_SKIP = true
          return showMessage(
            ad,
            `Short AD detected, will not skip but will mute. ${debug()}`,
            SHORT_AD_MSG_LENGTH
          )
        }
      }
    })
  )
}

const waitAndClick = (sel, ms, cb) =>
  waitForElems({
    sel: sel,
    onmatch: btn => {
      util.log('Found ad, closing in', ms, 'ms')
      setTimeout(() => {
        btn.click()
        if (cb) cb(btn)
      }, ms)
    }
  })

util.log('Started')

if (window.self === window.top) {
  let videoUrl
  // close home ad whenever encountered
  waitForElems({ sel: CSS.homeAdContainer, onmatch: ad => ad.remove() })
  // wait for video page
  waitForUrl(/^https:\/\/www\.youtube\.com\/watch\?.*v=.+/, () => {
    if (videoUrl && location.href !== videoUrl) {
      util.log('Changed video, removing old wait')
      util.clearTicks(TICKS)
    }
    videoUrl = location.href
    util.log('Entered video, waiting for ads')
    waitForAds()
    TICKS.push(
      waitForUrl(
        url => url !== videoUrl,
        () => {
          videoUrl = null
          util.clearTicks(TICKS)
          util.log('Left video, stopped waiting for ads')
        },
        true
      )
    )
  })
} else {
  if (/^https:\/\/www\.youtube\.com\/embed\//.test(location.href)) {
    util.log('Found embedded video, waiting for ads')
    waitForAds()
  }
}

GM_registerMenuCommand('Auto Close Youtube Ads - Manage Settings', config.setup)