- // ==UserScript==
- // @name youtube-adb
- // @name:zh-CN YouTube去广告
- // @name:zh-TW YouTube去廣告
- // @name:zh-HK YouTube去廣告
- // @name:zh-MO YouTube去廣告
- // @namespace https://github.com/iamfugui/youtube-adb
- // @author Hasan-Abbas
- // @version 6.21
- // @description A script to remove YouTube ads, including static ads and video ads, without interfering with the network and ensuring safety.
- // @description:zh-CN 脚本用于移除YouTube广告,包括静态广告和视频广告。不会干扰网络,安全。
- // @description:zh-TW 腳本用於移除 YouTube 廣告,包括靜態廣告和視頻廣告。不會干擾網路,安全。
- // @description:zh-HK 腳本用於移除 YouTube 廣告,包括靜態廣告和視頻廣告。不會干擾網路,安全。
- // @description:zh-MO 腳本用於移除 YouTube 廣告,包括靜態廣告和視頻廣告。不會干擾網路,安全。
- // @match *://*.youtube.com/*
- // @exclude *://accounts.youtube.com/*
- // @exclude *://www.youtube.com/live_chat_replay*
- // @exclude *://www.youtube.com/persist_identity*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=YouTube.com
- // @grant none
- // @license MIT
- // ==/UserScript==
-
- (function () {
- `use strict`;
-
- let video;
- //界面广告选择器
- const cssSelectorArr = [
- `#masthead-ad`,//首页顶部横幅广告.
- `ytd-rich-item-renderer.style-scope.ytd-rich-grid-row #content:has(.ytd-display-ad-renderer)`,//首页视频排版广告.
- `.video-ads.ytp-ad-module`,//播放器底部广告.
- `tp-yt-paper-dialog:has(yt-mealbar-promo-renderer)`,//播放页会员促销广告.
- `ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]`,//播放页右上方推荐广告.
- `#related #player-ads`,//播放页评论区右侧推广广告.
- `#related ytd-ad-slot-renderer`,//播放页评论区右侧视频排版广告.
- `ytd-ad-slot-renderer`,//搜索页广告.
- `yt-mealbar-promo-renderer`,//播放页会员推荐广告.
- `ytd-popup-container:has(a[href="/premium"])`,//会员拦截广告
- `ad-slot-renderer`,//M播放页第三方推荐广告
- `ytm-companion-ad-renderer`,//M可跳过的视频广告链接处
- ];
- window.dev = false;//开发使用
-
- /**
- * 将标准时间格式化
- * @param {Date} time 标准时间
- * @param {String} format 格式
- * @return {String}
- */
- function moment(time) {
- // 获取年⽉⽇时分秒
- let y = time.getFullYear()
- let m = (time.getMonth() + 1).toString().padStart(2, `0`)
- let d = time.getDate().toString().padStart(2, `0`)
- let h = time.getHours().toString().padStart(2, `0`)
- let min = time.getMinutes().toString().padStart(2, `0`)
- let s = time.getSeconds().toString().padStart(2, `0`)
- return `${y}-${m}-${d} ${h}:${min}:${s}`
- }
-
- /**
- * 输出信息
- * @param {String} msg 信息
- * @return {undefined}
- */
- function log(msg) {
- if (!window.dev) {
- return false;
- }
- console.log(window.location.href);
- console.log(`${moment(new Date())} ${msg}`);
- }
-
- /**
- * 设置运行标志
- * @param {String} name
- * @return {undefined}
- */
- function setRunFlag(name) {
- let style = document.createElement(`style`);
- style.id = name;
- (document.head || document.body).appendChild(style);//将节点附加到HTML.
- }
-
- /**
- * 获取运行标志
- * @param {String} name
- * @return {undefined|Element}
- */
- function getRunFlag(name) {
- return document.getElementById(name);
- }
-
- /**
- * 检查是否设置了运行标志
- * @param {String} name
- * @return {Boolean}
- */
- function checkRunFlag(name) {
- if (getRunFlag(name)) {
- return true;
- } else {
- setRunFlag(name)
- return false;
- }
- }
-
- /**
- * 生成去除广告的css元素style并附加到HTML节点上
- * @param {String} styles 样式文本
- * @return {undefined}
- */
- function generateRemoveADHTMLElement(id) {
- //如果已经设置过,退出.
- if (checkRunFlag(id)) {
- log(`屏蔽页面广告节点已生成`);
- return false
- }
-
- //设置移除广告样式.
- let style = document.createElement(`style`);//创建style元素.
- (document.head || document.body).appendChild(style);//将节点附加到HTML.
- style.appendChild(document.createTextNode(generateRemoveADCssText(cssSelectorArr)));//附加样式节点到元素节点.
- log(`生成屏蔽页面广告节点成功`);
- }
-
- /**
- * 生成去除广告的css文本
- * @param {Array} cssSelectorArr 待设置css选择器数组
- * @return {String}
- */
- function generateRemoveADCssText(cssSelectorArr) {
- cssSelectorArr.forEach((selector, index) => {
- cssSelectorArr[index] = `${selector}{display:none!important}`;//遍历并设置样式.
- });
- return cssSelectorArr.join(` `);//拼接成字符串.
- }
-
- /**
- * 触摸事件
- * @return {undefined}
- */
- function nativeTouch() {
- // 创建 Touch 对象
- let touch = new Touch({
- identifier: Date.now(),
- target: this,
- clientX: 12,
- clientY: 34,
- radiusX: 56,
- radiusY: 78,
- rotationAngle: 0,
- force: 1
- });
-
- // 创建 TouchEvent 对象
- let touchStartEvent = new TouchEvent(`touchstart`, {
- bubbles: true,
- cancelable: true,
- view: window,
- touches: [touch],
- targetTouches: [touch],
- changedTouches: [touch]
- });
-
- // 分派 touchstart 事件到目标元素
- this.dispatchEvent(touchStartEvent);
-
- // 创建 TouchEvent 对象
- let touchEndEvent = new TouchEvent(`touchend`, {
- bubbles: true,
- cancelable: true,
- view: window,
- touches: [],
- targetTouches: [],
- changedTouches: [touch]
- });
-
- // 分派 touchend 事件到目标元素
- this.dispatchEvent(touchEndEvent);
- }
-
-
- /**
- * 获取dom
- * @return {undefined}
- */
- function getVideoDom() {
- video = document.querySelector(`.ad-showing video`) || document.querySelector(`video`);
- }
-
-
- /**
- * 自动播放
- * @return {undefined}
- */
- function playAfterAd() {
- if (video.paused && video.currentTime < 1) {
- video.play();
- log(`自动播放视频`);
- }
- }
-
-
- /**
- * 移除YT拦截广告拦截弹窗并且关闭关闭遮罩层
- * @return {undefined}
- */
- function closeOverlay() {
- //移除YT拦截广告拦截弹窗
- const premiumContainers = [...document.querySelectorAll(`ytd-popup-container`)];
- const matchingContainers = premiumContainers.filter(container => container.querySelector(`a[href="/premium"]`));
-
- if (matchingContainers.length > 0) {
- matchingContainers.forEach(container => container.remove());
- log(`移除YT拦截器`);
- }
-
- // 获取所有具有指定标签的元素
- const backdrops = document.querySelectorAll(`tp-yt-iron-overlay-backdrop`);
- // 查找具有特定样式的元素
- const targetBackdrop = Array.from(backdrops).find(
- (backdrop) => backdrop.style.zIndex === `2201`
- );
- // 如果找到该元素,清空其类并移除 open 属性
- if (targetBackdrop) {
- targetBackdrop.className = ``; // 清空所有类
- targetBackdrop.removeAttribute(`opened`); // 移除 open 属性
- log(`关闭遮罩层`);
- }
- }
-
-
- /**
- * 跳过广告
- * @return {undefined}
- */
- function skipAd(mutationsList, observer) {
- const skipButton = document.querySelector(`.ytp-ad-skip-button`) || document.querySelector(`.ytp-skip-ad-button`) || document.querySelector(`.ytp-ad-skip-button-modern`);
- const shortAdMsg = document.querySelector(`.video-ads.ytp-ad-module .ytp-ad-player-overlay`) || document.querySelector(`.ytp-ad-button-icon`);
-
- if ((skipButton || shortAdMsg) && window.location.href.indexOf(`https://m.youtube.com/`) === -1) { //移动端静音有bug
- video.muted = true;
- }
-
- if (skipButton) {
- const delayTime = 0.5;
- setTimeout(skipAd, delayTime * 1000);//如果click和call没有跳过更改,直接更改广告时间
- if (video.currentTime > delayTime) {
- video.currentTime = video.duration;//强制
- log(`特殊账号跳过按钮广告`);
- return;
- }
- skipButton.click();//PC
- nativeTouch.call(skipButton);//Phone
- log(`按钮跳过广告`);
- } else if (shortAdMsg) {
- video.currentTime = video.duration;//强制
- log(`强制结束了该广告`);
- }
-
- }
-
- /**
- * 去除播放中的广告
- * @return {undefined}
- */
- function removePlayerAD(id) {
- //如果已经在运行,退出.
- if (checkRunFlag(id)) {
- log(`去除播放中的广告功能已在运行`);
- return false
- }
-
- //监听视频中的广告并处理
- const targetNode = document.body;//直接监听body变动
- const config = { childList: true, subtree: true };// 监听目标节点本身与子树下节点的变动
- const observer = new MutationObserver(() => { getVideoDom(); closeOverlay(); skipAd(); playAfterAd(); });//处理视频广告相关
- observer.observe(targetNode, config);// 以上述配置开始观察广告节点
- log(`运行去除播放中的广告功能成功`);
- }
-
- /**
- * main函数
- */
- function main() {
- generateRemoveADHTMLElement(`removeADHTMLElement`);//移除界面中的广告.
- removePlayerAD(`removePlayerAD`);//移除播放中的广告.
- }
-
- if (document.readyState === `loading`) {
- document.addEventListener(`DOMContentLoaded`, main);// 此时加载尚未完成
- log(`YouTube去广告脚本即将调用:`);
- } else {
- main();// 此时`DOMContentLoaded` 已经被触发
- log(`YouTube去广告脚本快速调用:`);
- }
-
- let resumeVideo = () => {
- const videoelem = document.body.querySelector('video.html5-main-video')
- if (videoelem && videoelem.paused) {
- console.log('resume video')
- videoelem.play()
- }
- }
-
- let removePop = node => {
- const elpopup = node.querySelector('.ytd-popup-container > .ytd-popup-container > .ytd-enforcement-message-view-model')
-
- if (elpopup) {
- elpopup.parentNode.remove()
- console.log('remove popup', elpopup)
- const bdelems = document
- .getElementsByTagName('tp-yt-iron-overlay-backdrop')
- for (var x = (bdelems || []).length; x--;)
- bdelems[x].remove()
- resumeVideo()
- }
-
- if (node.tagName.toLowerCase() === 'tp-yt-iron-overlay-backdrop') {
- node.remove()
- resumeVideo()
- console.log('remove backdrop', node)
- }
- }
-
- let obs = new MutationObserver(mutations => mutations.forEach(mutation => {
- if (mutation.type === 'childList') {
- Array.from(mutation.addedNodes)
- .filter(node => node.nodeType === 1)
- .map(node => removePop(node))
- }
- }))
-
- // have the observer observe foo for changes in children
- obs.observe(document.body, {
- childList: true,
- subtree: true
- })
- })();