Auto Close YouTube Ads

Close and/or Mute YouTube ads automatically!

  1. // ==UserScript==
  2. // @name Auto Close YouTube Ads
  3. // @namespace fuz/acya
  4. // @version 1.4.8
  5. // @description Close and/or Mute YouTube ads automatically!
  6. // @author fuzetsu
  7. // @run-at document-body
  8. // @match *://*.youtube.com/*
  9. // @exclude *://*.youtube.com/subscribe_embed?*
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_deleteValue
  13. // @grant GM_registerMenuCommand
  14. // @require https://cdn.jsdelivr.net/gh/fuzetsu/userscripts@ec863aa92cea78a20431f92e80ac0e93262136df/wait-for-elements/wait-for-elements.js
  15. // @require https://cdn.jsdelivr.net/gh/kufii/My-UserScripts@23586fd0a72b587a1786f7bb9088e807a5b53e79/libs/gm_config.js
  16. // ==/UserScript==
  17. /* globals GM_getValue GM_setValue GM_deleteValue GM_registerMenuCommand GM_config waitForElems waitForUrl */
  18. /**
  19. * This section of the code holds the css selectors that point different parts of YouTube's
  20. * user interface. If the script ever breaks and you don't want to wait for me to fix it
  21. * chances are that it can be fixed by just updating these selectors here.
  22. */
  23. const CSS = {
  24. // the button used to skip an ad
  25. skipButton:
  26. '.videoAdUiSkipButton,.ytp-ad-skip-button,.ytp-ad-skip-button-modern,.ytp-skip-ad-button',
  27. // the area showing the countdown to the skip button showing
  28. preSkipButton: '.videoAdUiPreSkipButton,.ytp-ad-preview-container,.ytp-preview-ad',
  29. // little x that closes banner ads
  30. closeBannerAd: '.close-padding.contains-svg,a.close-button,.ytp-ad-overlay-close-button',
  31. // button that toggle mute on the video
  32. muteButton: '.ytp-mute-button',
  33. // the slider bar handle that represents the current volume
  34. muteIndicator: '.ytp-volume-slider-handle',
  35. // container for ad on video
  36. adArea: '.videoAdUi,.ytp-ad-player-overlay,.ytp-ad-player-overlay-layout',
  37. // container that shows ad length eg 3:23
  38. adLength: '.videoAdUiAttribution,.ytp-ad-duration-remaining',
  39. // container for header ad on the home page
  40. homeAdContainer: '#masthead-ad'
  41. }
  42.  
  43. const util = {
  44. log: (...args) => console.log(`%c${SCRIPT_NAME}:`, 'font-weight: bold;color: purple;', ...args),
  45. clearTicks: ticks => {
  46. ticks.forEach(tick =>
  47. !tick ? null : typeof tick === 'number' ? clearInterval(tick) : tick.stop()
  48. )
  49. ticks.length = 0
  50. },
  51. keepTrying: (wait, action) => {
  52. const tick = setInterval(() => action() && clearInterval(tick), wait)
  53. return tick
  54. },
  55. storeGet: key => {
  56. if (typeof GM_getValue === 'undefined') {
  57. const value = localStorage.getItem(key)
  58. return value === 'true' ? true : value === 'false' ? false : value
  59. }
  60. return GM_getValue(key)
  61. },
  62. storeSet: (key, value) =>
  63. typeof GM_setValue === 'undefined' ? localStorage.setItem(key, value) : GM_setValue(key, value),
  64. storeDel: key =>
  65. typeof GM_deleteValue === 'undefined' ? localStorage.removeItem(key) : GM_deleteValue(key),
  66. q: (query, context) => (context || document).querySelector(query),
  67. qq: (query, context) => Array.from((context || document).querySelectorAll(query)),
  68. get: (obj, str) => util.getPath(obj, str.split('.').reverse()),
  69. getPath: (obj, path) =>
  70. obj == null ? null : path.length > 0 ? util.getPath(obj[path.pop()], path) : obj
  71. }
  72.  
  73. const SCRIPT_NAME = 'Auto Close YouTube Ads'
  74. const SHORT_AD_MSG_LENGTH = 12000
  75. const TICKS = []
  76. let DONT_SKIP = false
  77. const CONFIG_VERSION = 2
  78.  
  79. const config = GM_config([
  80. {
  81. key: 'muteAd',
  82. label: 'Mute ads?',
  83. type: 'bool',
  84. default: true
  85. },
  86. {
  87. key: 'hideAd',
  88. label: 'Hide video ads?',
  89. type: 'bool',
  90. default: false
  91. },
  92. {
  93. key: 'secWaitBanner',
  94. label: 'Banner ad close delay (seconds)',
  95. type: 'number',
  96. default: 3,
  97. min: 0
  98. },
  99. {
  100. key: 'secWaitVideo',
  101. label: 'Video ad skip delay (seconds)',
  102. type: 'number',
  103. default: 3,
  104. min: 0
  105. },
  106. {
  107. key: 'minAdLengthForSkip',
  108. label: 'Dont skip video shorter than this (seconds)',
  109. type: 'number',
  110. default: 0,
  111. min: 0
  112. },
  113. {
  114. key: 'muteEvenIfNotSkipping',
  115. label: 'Mute video even if not skipping',
  116. type: 'bool',
  117. default: true
  118. },
  119. {
  120. key: 'debug',
  121. label: 'Show extra debug information.',
  122. type: 'bool',
  123. default: false
  124. },
  125. {
  126. key: 'version',
  127. type: 'hidden',
  128. default: CONFIG_VERSION
  129. }
  130. ])
  131.  
  132. let conf = config.load()
  133.  
  134. config.onsave = cfg => (conf = cfg)
  135.  
  136. function createMessageElement() {
  137. const elem = document.createElement('div')
  138. elem.setAttribute(
  139. 'style',
  140. '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;'
  141. )
  142. return elem
  143. }
  144. function showMessage(container, text, ms) {
  145. const message = createMessageElement()
  146. message.textContent = text
  147. container.appendChild(message)
  148. util.log(`showing message [${ms}ms]: ${text}`)
  149. setTimeout(() => message.remove(), ms)
  150. }
  151.  
  152. function setupCancelDiv(ad) {
  153. const skipArea = util.q(CSS.preSkipButton, ad)
  154. const skipText = skipArea && skipArea.textContent.trim().replace(/\s+/g, ' ')
  155. if (skipText) {
  156. if (['will begin', 'will play', 'plays soon'].some(snip => skipText.includes(snip))) return
  157. const cancelClass = 'acya-cancel-skip'
  158. let cancelDiv = util.q('.' + cancelClass)
  159. if (cancelDiv) cancelDiv.remove()
  160. cancelDiv = createMessageElement()
  161. cancelDiv.className = cancelClass
  162. cancelDiv.textContent = (conf.muteAd ? 'Un-mute & ' : '') + 'Cancel Auto Skip'
  163. cancelDiv.onclick = () => {
  164. util.log('cancel clicked')
  165. DONT_SKIP = true
  166. cancelDiv.remove()
  167. if (conf.hideAd) {
  168. ad.style.zIndex = ''
  169. ad.style.background = ''
  170. }
  171. const muteButton = getMuteButton()
  172. const muteIndicator = getMuteIndicator()
  173. if (conf.muteAd && muteButton && muteIndicator && isMuted(muteIndicator)) muteButton.click()
  174. }
  175. ad.appendChild(cancelDiv)
  176. } else {
  177. util.log("skip button area wasn't there for some reason.. couldn't place cancel button.")
  178. }
  179. }
  180.  
  181. function parseTime(str) {
  182. const [minutes, seconds] = str
  183. .split(' ')
  184. .pop()
  185. .split(':')
  186. .map(num => parseInt(num))
  187. util.log(str, minutes, seconds)
  188. return minutes * 60 + seconds || 0
  189. }
  190.  
  191. const getMuteButton = () => util.qq(CSS.muteButton).find(elem => elem.offsetParent)
  192. const getMuteIndicator = () => util.qq(CSS.muteIndicator).find(elem => elem.offsetParent)
  193. const isMuted = m => m.style.left === '0px'
  194.  
  195. function getAdLength(ad) {
  196. if (!ad) return 0
  197. const time = ad.querySelector(CSS.adLength)
  198. return time ? parseTime(time.textContent) : 0
  199. }
  200.  
  201. function waitForAds() {
  202. DONT_SKIP = false
  203. TICKS.push(
  204. waitForElems({
  205. sel: CSS.skipButton,
  206. onmatch: btn => {
  207. util.log('found skip button')
  208. util.keepTrying(500, () => {
  209. if (!btn) return true
  210. // if not visible
  211. if (btn.offsetParent == null) return
  212. setTimeout(() => {
  213. if (DONT_SKIP) {
  214. util.log('not skipping...')
  215. DONT_SKIP = false
  216. return
  217. }
  218. util.log('clicking skip button')
  219. btn.click()
  220. }, conf.secWaitVideo * 1000)
  221. return true
  222. })
  223. }
  224. }),
  225. waitAndClick(CSS.closeBannerAd, conf.secWaitBanner * 1000),
  226. waitForElems({
  227. sel: CSS.adArea,
  228. onmatch: ad => {
  229. util.log('Video ad detected')
  230. // reset don't skip
  231. DONT_SKIP = false
  232. const adLength = getAdLength(ad)
  233. const isShort = adLength < conf.minAdLengthForSkip
  234. const debug = () =>
  235. conf.debug
  236. ? `[DEBUG adLength = ${adLength}, minAdLengthForSkip = ${conf.minAdLengthForSkip}]`
  237. : ''
  238. if (isShort && !conf.muteEvenIfNotSkipping) {
  239. DONT_SKIP = true
  240. return showMessage(
  241. ad,
  242. `Shot AD detected, will not skip or mute. ${debug()}`,
  243. SHORT_AD_MSG_LENGTH
  244. )
  245. }
  246. if (conf.hideAd) {
  247. ad.style.zIndex = 10
  248. ad.style.background = 'black'
  249. }
  250. // show option to cancel automatic skip
  251. if (!isShort) setupCancelDiv(ad)
  252. if (!conf.muteAd) return
  253. const muteButton = getMuteButton()
  254. const muteIndicator = getMuteIndicator()
  255. if (!muteIndicator) return util.log('unable to determine mute state, skipping mute')
  256. if (isMuted(muteIndicator)) {
  257. util.log('Audio is already muted')
  258. } else {
  259. util.log('Muting audio')
  260. muteButton.click()
  261. }
  262. // wait for the ad to disappear before unmuting
  263. util.keepTrying(250, () => {
  264. if (!ad.offsetParent) {
  265. if (isMuted(muteIndicator)) {
  266. muteButton.click()
  267. util.log('Video ad ended, unmuting audio')
  268. } else {
  269. util.log('Video ad ended, audio already unmuted')
  270. }
  271. return true
  272. }
  273. })
  274. if (isShort) {
  275. DONT_SKIP = true
  276. return showMessage(
  277. ad,
  278. `Short AD detected, will not skip but will mute. ${debug()}`,
  279. SHORT_AD_MSG_LENGTH
  280. )
  281. }
  282. }
  283. })
  284. )
  285. }
  286.  
  287. const waitAndClick = (sel, ms, cb) =>
  288. waitForElems({
  289. sel: sel,
  290. onmatch: btn => {
  291. util.log('Found ad, closing in', ms, 'ms')
  292. setTimeout(() => {
  293. btn.click()
  294. if (cb) cb(btn)
  295. }, ms)
  296. }
  297. })
  298.  
  299. util.log('Started')
  300.  
  301. if (window.self === window.top) {
  302. let videoUrl
  303. // close home ad whenever encountered
  304. waitForElems({ sel: CSS.homeAdContainer, onmatch: ad => ad.remove() })
  305. // wait for video page
  306. waitForUrl(/^https:\/\/www\.youtube\.com\/watch\?.*v=.+/, () => {
  307. if (videoUrl && location.href !== videoUrl) {
  308. util.log('Changed video, removing old wait')
  309. util.clearTicks(TICKS)
  310. }
  311. videoUrl = location.href
  312. util.log('Entered video, waiting for ads')
  313. waitForAds()
  314. TICKS.push(
  315. waitForUrl(
  316. url => url !== videoUrl,
  317. () => {
  318. videoUrl = null
  319. util.clearTicks(TICKS)
  320. util.log('Left video, stopped waiting for ads')
  321. },
  322. true
  323. )
  324. )
  325. })
  326. } else {
  327. if (/^https:\/\/www\.youtube\.com\/embed\//.test(location.href)) {
  328. util.log('Found embedded video, waiting for ads')
  329. waitForAds()
  330. }
  331. }
  332.  
  333. GM_registerMenuCommand('Auto Close Youtube Ads - Manage Settings', config.setup)

QingJ © 2025

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