Close and/or Mute YouTube ads automatically!
// ==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)