youtube-adb

A script to remove YouTube ads, including static ads and video ads, without interfering with the network and ensuring safety.

  1. // ==UserScript==
  2. // @name youtube-adb
  3. // @name:zh-CN YouTube去广告
  4. // @name:zh-TW YouTube去廣告
  5. // @name:zh-HK YouTube去廣告
  6. // @name:zh-MO YouTube去廣告
  7. // @namespace https://github.com/iamfugui/youtube-adb
  8. // @version 6.21
  9. // @description A script to remove YouTube ads, including static ads and video ads, without interfering with the network and ensuring safety.
  10. // @description:zh-CN 脚本用于移除YouTube广告,包括静态广告和视频广告。不会干扰网络,安全。
  11. // @description:zh-TW 腳本用於移除 YouTube 廣告,包括靜態廣告和視頻廣告。不會干擾網路,安全。
  12. // @description:zh-HK 腳本用於移除 YouTube 廣告,包括靜態廣告和視頻廣告。不會干擾網路,安全。
  13. // @description:zh-MO 腳本用於移除 YouTube 廣告,包括靜態廣告和視頻廣告。不會干擾網路,安全。
  14. // @match *://*.youtube.com/*
  15. // @exclude *://accounts.youtube.com/*
  16. // @exclude *://www.youtube.com/live_chat_replay*
  17. // @exclude *://www.youtube.com/persist_identity*
  18. // @icon https://www.google.com/s2/favicons?sz=64&domain=YouTube.com
  19. // @grant none
  20. // @license MIT
  21. // ==/UserScript==
  22.  
  23. (function() {
  24. `use strict`;
  25.  
  26. let video;
  27. //界面广告选择器
  28. const cssSelectorArr = [
  29. `#masthead-ad`,//首页顶部横幅广告.
  30. `ytd-rich-item-renderer.style-scope.ytd-rich-grid-row #content:has(.ytd-display-ad-renderer)`,//首页视频排版广告.
  31. `.video-ads.ytp-ad-module`,//播放器底部广告.
  32. `tp-yt-paper-dialog:has(yt-mealbar-promo-renderer)`,//播放页会员促销广告.
  33. `ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]`,//播放页右上方推荐广告.
  34. `#related #player-ads`,//播放页评论区右侧推广广告.
  35. `#related ytd-ad-slot-renderer`,//播放页评论区右侧视频排版广告.
  36. `ytd-ad-slot-renderer`,//搜索页广告.
  37. `yt-mealbar-promo-renderer`,//播放页会员推荐广告.
  38. `ytd-popup-container:has(a[href="/premium"])`,//会员拦截广告
  39. `ad-slot-renderer`,//M播放页第三方推荐广告
  40. `ytm-companion-ad-renderer`,//M可跳过的视频广告链接处
  41. ];
  42. window.dev=false;//开发使用
  43.  
  44. /**
  45. * 将标准时间格式化
  46. * @param {Date} time 标准时间
  47. * @param {String} format 格式
  48. * @return {String}
  49. */
  50. function moment(time) {
  51. // 获取年⽉⽇时分秒
  52. let y = time.getFullYear()
  53. let m = (time.getMonth() + 1).toString().padStart(2, `0`)
  54. let d = time.getDate().toString().padStart(2, `0`)
  55. let h = time.getHours().toString().padStart(2, `0`)
  56. let min = time.getMinutes().toString().padStart(2, `0`)
  57. let s = time.getSeconds().toString().padStart(2, `0`)
  58. return `${y}-${m}-${d} ${h}:${min}:${s}`
  59. }
  60.  
  61. /**
  62. * 输出信息
  63. * @param {String} msg 信息
  64. * @return {undefined}
  65. */
  66. function log(msg) {
  67. if(!window.dev){
  68. return false;
  69. }
  70. console.log(window.location.href);
  71. console.log(`${moment(new Date())} ${msg}`);
  72. }
  73.  
  74. /**
  75. * 设置运行标志
  76. * @param {String} name
  77. * @return {undefined}
  78. */
  79. function setRunFlag(name){
  80. let style = document.createElement(`style`);
  81. style.id = name;
  82. (document.head || document.body).appendChild(style);//将节点附加到HTML.
  83. }
  84.  
  85. /**
  86. * 获取运行标志
  87. * @param {String} name
  88. * @return {undefined|Element}
  89. */
  90. function getRunFlag(name){
  91. return document.getElementById(name);
  92. }
  93.  
  94. /**
  95. * 检查是否设置了运行标志
  96. * @param {String} name
  97. * @return {Boolean}
  98. */
  99. function checkRunFlag(name){
  100. if(getRunFlag(name)){
  101. return true;
  102. }else{
  103. setRunFlag(name)
  104. return false;
  105. }
  106. }
  107.  
  108. /**
  109. * 生成去除广告的css元素style并附加到HTML节点上
  110. * @param {String} styles 样式文本
  111. * @return {undefined}
  112. */
  113. function generateRemoveADHTMLElement(id) {
  114. //如果已经设置过,退出.
  115. if (checkRunFlag(id)) {
  116. log(`屏蔽页面广告节点已生成`);
  117. return false
  118. }
  119.  
  120. //设置移除广告样式.
  121. let style = document.createElement(`style`);//创建style元素.
  122. (document.head || document.body).appendChild(style);//将节点附加到HTML.
  123. style.appendChild(document.createTextNode(generateRemoveADCssText(cssSelectorArr)));//附加样式节点到元素节点.
  124. log(`生成屏蔽页面广告节点成功`);
  125. }
  126.  
  127. /**
  128. * 生成去除广告的css文本
  129. * @param {Array} cssSelectorArr 待设置css选择器数组
  130. * @return {String}
  131. */
  132. function generateRemoveADCssText(cssSelectorArr){
  133. cssSelectorArr.forEach((selector,index)=>{
  134. cssSelectorArr[index]=`${selector}{display:none!important}`;//遍历并设置样式.
  135. });
  136. return cssSelectorArr.join(` `);//拼接成字符串.
  137. }
  138.  
  139. /**
  140. * 触摸事件
  141. * @return {undefined}
  142. */
  143. function nativeTouch(){
  144. // 创建 Touch 对象
  145. let touch = new Touch({
  146. identifier: Date.now(),
  147. target: this,
  148. clientX: 12,
  149. clientY: 34,
  150. radiusX: 56,
  151. radiusY: 78,
  152. rotationAngle: 0,
  153. force: 1
  154. });
  155.  
  156. // 创建 TouchEvent 对象
  157. let touchStartEvent = new TouchEvent(`touchstart`, {
  158. bubbles: true,
  159. cancelable: true,
  160. view: window,
  161. touches: [touch],
  162. targetTouches: [touch],
  163. changedTouches: [touch]
  164. });
  165.  
  166. // 分派 touchstart 事件到目标元素
  167. this.dispatchEvent(touchStartEvent);
  168.  
  169. // 创建 TouchEvent 对象
  170. let touchEndEvent = new TouchEvent(`touchend`, {
  171. bubbles: true,
  172. cancelable: true,
  173. view: window,
  174. touches: [],
  175. targetTouches: [],
  176. changedTouches: [touch]
  177. });
  178.  
  179. // 分派 touchend 事件到目标元素
  180. this.dispatchEvent(touchEndEvent);
  181. }
  182.  
  183.  
  184. /**
  185. * 获取dom
  186. * @return {undefined}
  187. */
  188. function getVideoDom(){
  189. video = document.querySelector(`.ad-showing video`) || document.querySelector(`video`);
  190. }
  191.  
  192.  
  193. /**
  194. * 自动播放
  195. * @return {undefined}
  196. */
  197. function playAfterAd(){
  198. if(video.paused && video.currentTime<1){
  199. video.play();
  200. log(`自动播放视频`);
  201. }
  202. }
  203.  
  204.  
  205. /**
  206. * 移除YT拦截广告拦截弹窗并且关闭关闭遮罩层
  207. * @return {undefined}
  208. */
  209. function closeOverlay(){
  210. //移除YT拦截广告拦截弹窗
  211. const premiumContainers = [...document.querySelectorAll(`ytd-popup-container`)];
  212. const matchingContainers = premiumContainers.filter(container => container.querySelector(`a[href="/premium"]`));
  213.  
  214. if(matchingContainers.length>0){
  215. matchingContainers.forEach(container => container.remove());
  216. log(`移除YT拦截器`);
  217. }
  218.  
  219. // 获取所有具有指定标签的元素
  220. const backdrops = document.querySelectorAll(`tp-yt-iron-overlay-backdrop`);
  221. // 查找具有特定样式的元素
  222. const targetBackdrop = Array.from(backdrops).find(
  223. (backdrop) => backdrop.style.zIndex === `2201`
  224. );
  225. // 如果找到该元素,清空其类并移除 open 属性
  226. if (targetBackdrop) {
  227. targetBackdrop.className = ``; // 清空所有类
  228. targetBackdrop.removeAttribute(`opened`); // 移除 open 属性
  229. log(`关闭遮罩层`);
  230. }
  231. }
  232.  
  233.  
  234. /**
  235. * 跳过广告
  236. * @return {undefined}
  237. */
  238. function skipAd(mutationsList, observer) {
  239. const skipButton = document.querySelector(`.ytp-ad-skip-button`) || document.querySelector(`.ytp-skip-ad-button`) || document.querySelector(`.ytp-ad-skip-button-modern`);
  240. const shortAdMsg = document.querySelector(`.video-ads.ytp-ad-module .ytp-ad-player-overlay`) || document.querySelector(`.ytp-ad-button-icon`);
  241.  
  242. if((skipButton || shortAdMsg) && window.location.href.indexOf(`https://m.youtube.com/`) === -1){ //移动端静音有bug
  243. video.muted = true;
  244. }
  245.  
  246. if(skipButton){
  247. const delayTime = 0.5;
  248. setTimeout(skipAd,delayTime*1000);//如果click和call没有跳过更改,直接更改广告时间
  249. if(video.currentTime>delayTime){
  250. video.currentTime = video.duration;//强制
  251. log(`特殊账号跳过按钮广告`);
  252. return;
  253. }
  254. skipButton.click();//PC
  255. nativeTouch.call(skipButton);//Phone
  256. log(`按钮跳过广告`);
  257. }else if(shortAdMsg){
  258. video.currentTime = video.duration;//强制
  259. log(`强制结束了该广告`);
  260. }
  261.  
  262. }
  263.  
  264. /**
  265. * 去除播放中的广告
  266. * @return {undefined}
  267. */
  268. function removePlayerAD(id){
  269. //如果已经在运行,退出.
  270. if (checkRunFlag(id)) {
  271. log(`去除播放中的广告功能已在运行`);
  272. return false
  273. }
  274.  
  275. //监听视频中的广告并处理
  276. const targetNode = document.body;//直接监听body变动
  277. const config = {childList: true, subtree: true };// 监听目标节点本身与子树下节点的变动
  278. const observer = new MutationObserver(()=>{getVideoDom();closeOverlay();skipAd();playAfterAd();});//处理视频广告相关
  279. observer.observe(targetNode, config);// 以上述配置开始观察广告节点
  280. log(`运行去除播放中的广告功能成功`);
  281. }
  282.  
  283. /**
  284. * main函数
  285. */
  286. function main(){
  287. generateRemoveADHTMLElement(`removeADHTMLElement`);//移除界面中的广告.
  288. removePlayerAD(`removePlayerAD`);//移除播放中的广告.
  289. }
  290.  
  291. if (document.readyState === `loading`) {
  292. document.addEventListener(`DOMContentLoaded`, main);// 此时加载尚未完成
  293. log(`YouTube去广告脚本即将调用:`);
  294. } else {
  295. main();// 此时`DOMContentLoaded` 已经被触发
  296. log(`YouTube去广告脚本快速调用:`);
  297. }
  298.  
  299. let resumeVideo = () => {
  300. const videoelem = document.body.querySelector('video.html5-main-video')
  301. if (videoelem && videoelem.paused) {
  302. console.log('resume video')
  303. videoelem.play()
  304. }
  305. }
  306.  
  307. let removePop = node => {
  308. const elpopup = node.querySelector('.ytd-popup-container > .ytd-popup-container > .ytd-enforcement-message-view-model')
  309.  
  310. if (elpopup) {
  311. elpopup.parentNode.remove()
  312. console.log('remove popup', elpopup)
  313. const bdelems = document
  314. .getElementsByTagName('tp-yt-iron-overlay-backdrop')
  315. for (var x = (bdelems || []).length; x--;)
  316. bdelems[x].remove()
  317. resumeVideo()
  318. }
  319.  
  320. if (node.tagName.toLowerCase() === 'tp-yt-iron-overlay-backdrop') {
  321. node.remove()
  322. resumeVideo()
  323. console.log('remove backdrop', node)
  324. }
  325. }
  326.  
  327. let obs = new MutationObserver(mutations => mutations.forEach(mutation => {
  328. if (mutation.type === 'childList') {
  329. Array.from(mutation.addedNodes)
  330. .filter(node => node.nodeType === 1)
  331. .map(node => removePop(node))
  332. }
  333. }))
  334.  
  335. // have the observer observe foo for changes in children
  336. obs.observe(document.body, {
  337. childList: true,
  338. subtree: true
  339. })
  340. })();