352 lines
11 KiB
JavaScript
352 lines
11 KiB
JavaScript
// ==UserScript==
|
|
// @name Auto Close YouTube Ads
|
|
// @namespace http://fuzetsu.acypa.com
|
|
// @version 1.4.1
|
|
// @description Close and/or Mute YouTube ads automatically!
|
|
// @author fuzetsu
|
|
// @match *://*.youtube.com/*
|
|
// @exclude *://*.youtube.com/subscribe_embed?*
|
|
// @grant GM_getValue
|
|
// @grant GM_setValue
|
|
// @grant GM_deleteValue
|
|
// @grant GM_registerMenuCommand
|
|
// @require https://gitcdn.xyz/repo/fuzetsu/userscripts/b38eabf72c20fa3cf7da84ecd2cefe0d4a2116be/wait-for-elements/wait-for-elements.js
|
|
// @require https://gitcdn.xyz/repo/kufii/My-UserScripts/fa4555701cf5a22eae44f06d9848df6966788fa8/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',
|
|
// the area showing the countdown to the skip button showing
|
|
preSkipButton: '.videoAdUiPreSkipButton,.ytp-ad-preview-container',
|
|
// 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',
|
|
// 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 = GM_config([
|
|
{
|
|
key: 'muteAd',
|
|
label: 'Mute ads?',
|
|
type: 'bool',
|
|
default: true
|
|
},
|
|
{
|
|
key: 'hideAd',
|
|
label: 'Hide video ads?',
|
|
type: 'bool',
|
|
default: true
|
|
},
|
|
{
|
|
key: 'secWaitBanner',
|
|
label: 'Banner ad close delay (seconds)',
|
|
type: 'number',
|
|
default: 0,
|
|
min: 0
|
|
},
|
|
{
|
|
key: 'secWaitVideo',
|
|
label: 'Video ad skip delay (seconds)',
|
|
type: 'number',
|
|
default: 0,
|
|
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: 1
|
|
}
|
|
])
|
|
|
|
const configVersion = 2
|
|
let conf = config.load()
|
|
|
|
config.onsave = cfg => (conf = cfg)
|
|
|
|
// config upgrade procedure
|
|
function upgradeConfig() {
|
|
let lastVersion
|
|
while (conf.version < configVersion && lastVersion !== conf.version) {
|
|
util.log('upgrading config version, current = ', conf.version, ', target = ', configVersion)
|
|
lastVersion = conf.version
|
|
switch (conf.version) {
|
|
case 1: {
|
|
const oldConf = {
|
|
muteAd: util.storeGet('MUTE_AD'),
|
|
hideAd: util.storeGet('HIDE_AD'),
|
|
secWait: util.storeGet('SEC_WAIT')
|
|
}
|
|
|
|
if (oldConf.muteAd != null) conf.muteAd = !!oldConf.muteAd
|
|
if (oldConf.hideAd != null) conf.hideAd = !!oldConf.hideAd
|
|
if (oldConf.secWait != null && !isNaN(oldConf.secWait))
|
|
conf.secWaitBanner = conf.secWaitVideo = parseInt(oldConf.secWait)
|
|
|
|
conf.version = 2
|
|
|
|
config.save(conf)
|
|
;['SEC_WAIT', 'HIDE_AD', 'MUTE_AD'].forEach(util.storeDel)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
upgradeConfig()
|
|
|
|
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: 10px;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 && !['will begin', 'will play'].some(snip => skipText.includes(snip))) {
|
|
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()
|
|
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.q(CSS.muteButton)
|
|
const getMuteIndicator = () => util.q(CSS.muteIndicator)
|
|
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 => {
|
|
// 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')
|
|
muteButton.click()
|
|
util.log('Video ad detected, muting audio')
|
|
// wait for the ad to disappear before unmuting
|
|
util.keepTrying(250, () => {
|
|
if (!util.q(CSS.adArea)) {
|
|
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)
|