// ==UserScript== // @name B站广告去除 // @namespace https://github.com/yourusername // @version 1.1.2 // @description 去除B站首页右侧的推荐功能,和播放页的广告 // @author TorchMaker // @match *://*.bilibili.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; // 配置默认值 const defaultConfig = { hideRightSidebar: false // 默认不隐藏右侧推荐 }; // 检查GM_* API是否可用 function isGMAPIAvailable() { return typeof GM_getValue === 'function' && typeof GM_setValue === 'function' && typeof GM_registerMenuCommand === 'function'; } // 获取配置 function getConfig() { if (isGMAPIAvailable()) { try { return { hideRightSidebar: GM_getValue('hideRightSidebar', defaultConfig.hideRightSidebar) }; } catch (e) { console.error('获取配置失败:', e); return defaultConfig; } } return defaultConfig; } // 保存配置 function saveConfig(config) { if (isGMAPIAvailable()) { try { GM_setValue('hideRightSidebar', config.hideRightSidebar); } catch (e) { console.error('保存配置失败:', e); } } } // 创建配置菜单 function createConfigMenu() { if (isGMAPIAvailable()) { try { // 注册菜单命令 GM_registerMenuCommand('B站广告覆盖配置', showConfigDialog); } catch (e) { console.error('注册菜单失败:', e); } } } // 检测是否在脚本猫环境中 function isScriptCatEnvironment() { return typeof unsafeWindow !== 'undefined' || typeof ScriptCat !== 'undefined'; } // 显示配置对话框 function showConfigDialog() { try { console.log('显示配置对话框'); const config = getConfig(); // 创建背景遮罩 - 先创建,避免变量提升问题 const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 99998; `; // 创建对话框 const dialog = document.createElement('div'); dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); z-index: 99999; min-width: 300px; max-width: 90%; max-height: 90%; overflow: auto; `; // 创建标题 const title = document.createElement('h2'); title.textContent = 'B站广告覆盖配置'; title.style.cssText = 'margin-top: 0; margin-bottom: 20px; color: #333;'; dialog.appendChild(title); // 创建隐藏右侧推荐选项 const sidebarOption = document.createElement('div'); sidebarOption.style.cssText = 'margin-bottom: 20px;'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'hideRightSidebar'; checkbox.checked = config.hideRightSidebar; const label = document.createElement('label'); label.htmlFor = 'hideRightSidebar'; label.textContent = '隐藏右侧推荐(直播、赛事、番剧等)'; label.style.cssText = 'margin-left: 8px; color: #666;'; sidebarOption.appendChild(checkbox); sidebarOption.appendChild(label); dialog.appendChild(sidebarOption); // 创建按钮容器 const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;'; // 创建保存按钮 const saveButton = document.createElement('button'); saveButton.textContent = '保存'; saveButton.style.cssText = ` padding: 8px 16px; background: #00a1d6; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; `; saveButton.addEventListener('click', () => { try { const newConfig = { hideRightSidebar: checkbox.checked }; saveConfig(newConfig); // 移除对话框和遮罩 if (dialog.parentNode) { document.body.removeChild(dialog); } if (overlay.parentNode) { document.body.removeChild(overlay); } // 应用新配置 if (newConfig.hideRightSidebar) { hideRightSidebar(); } else { showRightSidebar(); } console.log('配置保存成功'); } catch (e) { console.error('保存配置失败:', e); } }); // 创建取消按钮 const cancelButton = document.createElement('button'); cancelButton.textContent = '取消'; cancelButton.style.cssText = ` padding: 8px 16px; background: #f0f0f0; color: #333; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; `; cancelButton.addEventListener('click', () => { // 移除对话框和遮罩 if (dialog.parentNode) { document.body.removeChild(dialog); } if (overlay.parentNode) { document.body.removeChild(overlay); } }); // 点击遮罩关闭对话框 overlay.addEventListener('click', () => { if (dialog.parentNode) { document.body.removeChild(dialog); } if (overlay.parentNode) { document.body.removeChild(overlay); } }); // 阻止对话框内的点击事件冒泡到遮罩 dialog.addEventListener('click', (e) => { e.stopPropagation(); }); buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(saveButton); dialog.appendChild(buttonContainer); // 添加到页面 document.body.appendChild(overlay); document.body.appendChild(dialog); console.log('配置对话框显示成功'); } catch (e) { console.error('显示配置对话框失败:', e); // 如果GM_* API不可用,使用alert作为备选 alert('配置功能需要脚本猫或油猴扩展的GM_* API支持'); } } // 隐藏右侧推荐(只处理.floor-single-card内容) function hideRightSidebar() { // 添加性能计时 const startTime = performance.now(); console.log('开始处理右侧推荐'); // 只处理.floor-single-card元素,不去掉右侧容器 const recommendationCards = document.querySelectorAll('.floor-single-card, .bili-card, .recommend-card, .slide-ad-exp, .video-card-ad-small, .ad-report'); console.log('需要处理的卡片数量:', recommendationCards.length); // 批量处理元素 recommendationCards.forEach(card => { console.log('处理右侧推荐卡片:', card.className, card.id); // 直接隐藏卡片,不占用空间 card.style.display = 'none'; }); // 在hideRightSidebar函数中执行去除广告的功能,处理后续加载的广告 processAds(); // 计算执行时间 const endTime = performance.now(); console.log('右侧推荐处理完成,执行时间:', (endTime - startTime).toFixed(2), 'ms'); } // 显示右侧推荐(只恢复.floor-single-card内容) function showRightSidebar() { // 添加性能计时 const startTime = performance.now(); console.log('开始恢复右侧推荐'); // 只恢复.floor-single-card元素,不处理右侧容器 const recommendationCards = document.querySelectorAll('.floor-single-card, .bili-card, .recommend-card'); console.log('需要恢复的卡片数量:', recommendationCards.length); // 批量恢复样式 recommendationCards.forEach(card => { // 只恢复被我们隐藏的卡片 if (card.style.display === 'none') { console.log('恢复右侧推荐卡片:', card.className); // 恢复原始样式 card.style.display = ''; } }); // 计算执行时间 const endTime = performance.now(); console.log('右侧推荐恢复完成,执行时间:', (endTime - startTime).toFixed(2), 'ms'); } // 广告识别规则 const adRules = { // 基于CSS类名的识别 - 只匹配完整的广告类名 classNames: [ 'ad', 'advertisement', 'promotion', 'bili-ad', 'ad-item', 'ad-box', 'ecommerce-card', 'mall-card', 'feed-card-ad', 'ad-feed', 'ad-sponsor', 'ad-video', 'ad-banner', 'bili-ad-card', 'ad-container', 'ad-wrap', 'ad-content', 'ad-section', 'ad-module', 'ad-placement', 'ad-slot', 'ad-unit', 'bili-ad-tag', 'ad-tag', 'sponsor-tag', 'video-card-ad-small' ], // 基于ID的识别 ids: [ 'ad', 'advertisement', 'promotion', 'bili-ad', 'ad-container', 'slide_ad' ], // 基于属性的识别 - 只匹配明确的广告属性 attributes: [ { name: 'data-type', value: 'ad' }, { name: 'data-ad', value: 'true' }, { name: 'data-role', value: 'ad' }, { name: 'data-bind', value: 'ad' } ], // 基于链接的识别 - 更精确的广告链接模式 linkPatterns: [ /https?:\/\/ad\..+/i, // 广告域名 /https?:\/\/.*?\.com\/.*?\bad\b.*/i, // 包含完整ad单词的链接 /https?:\/\/.*?\.com\/.*?sponsor.*/i, // 包含sponsor的链接 /https?:\/\/.*?\.com\/.*?promo.*/i, // 包含promo的链接 /https?:\/\/.*?\.com\/.*?marketing.*/i, // 包含marketing的链接 /https?:\/\/.*?\.com\/.*?affiliate.*/i, // 包含affiliate的链接 /https?:\/\/.*?\.com\/.*?paid.*/i // 包含paid的链接 ], // 白名单 - 排除的正常元素 whitelist: [ 'video', 'audio', 'img', 'picture', 'button', 'input', 'textarea', 'select', 'form', 'table', 'tr', 'td', 'th', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'a', 'div', 'section', 'article', 'header', 'footer', 'nav', 'aside', 'main', 'content', 'container', 'player', 'comment', 'reply', 'message', 'user', 'profile', 'avatar', 'badge' ] }; // 检查元素是否在白名单中 function isInWhitelist(element) { // 检查元素标签名 if (adRules.whitelist.includes(element.tagName.toLowerCase())) { return true; } // 检查元素类名 const classList = Array.from(element.classList); for (const className of classList) { if (adRules.whitelist.some(whitelistItem => className.includes(whitelistItem) )) { return true; } } // 检查元素ID if (element.id) { if (adRules.whitelist.some(whitelistItem => element.id.includes(whitelistItem) )) { return true; } } return false; } // 识别广告元素 function identifyAds() { const ads = []; // 只查询可能的广告容器元素 const possibleAdElements = document.querySelectorAll('div, section, article'); possibleAdElements.forEach(element => { // 跳过白名单元素 if (isInWhitelist(element)) { return; } // 检查CSS类名 - 只匹配完整的广告类名 const hasAdClassName = adRules.classNames.some(className => element.classList.contains(className) ); // 检查ID const hasAdId = adRules.ids.some(id => element.id === id ); // 检查属性 - 只匹配明确的广告属性 const hasAdAttribute = adRules.attributes.some(rule => { return element.getAttribute(rule.name) === rule.value; }); // 检查链接 let hasAdLink = false; if (element.tagName === 'A') { const href = element.getAttribute('href'); if (href) { hasAdLink = adRules.linkPatterns.some(pattern => pattern.test(href) ); } } // 检查子元素中的链接 if (!hasAdLink) { const links = element.querySelectorAll('a'); links.forEach(link => { const href = link.getAttribute('href'); if (href) { if (adRules.linkPatterns.some(pattern => pattern.test(href))) { hasAdLink = true; } } }); } // 只匹配明确的广告特征 const isAd = (hasAdClassName || hasAdId || hasAdAttribute) && hasAdLink; // 如果是明确的广告,则添加到列表 if (isAd) { ads.push(element); } }); return ads; } // 创建广告覆盖层 function createAdOverlay(adElement) { // 检查是否已经有覆盖层 if (adElement.querySelector('.ad-overlay')) { return; } // 设置广告元素为相对定位,以便覆盖层可以绝对定位 if (getComputedStyle(adElement).position === 'static') { adElement.style.position = 'relative'; } // 创建覆盖层元素 const overlay = document.createElement('div'); overlay.className = 'ad-overlay'; // 设置覆盖层样式 Object.assign(overlay.style, { position: 'absolute', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(128, 128, 128, 0.7)', display: 'flex', justifyContent: 'center', alignItems: 'center', fontSize: '24px', fontWeight: 'bold', color: '#fff', textAlign: 'center', zIndex: '9999', cursor: 'not-allowed', pointerEvents: 'auto' }); // 添加"广告"文字 overlay.textContent = '广告'; // 阻止点击事件 overlay.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); }); // 添加覆盖层到广告元素 adElement.appendChild(overlay); } // 处理广告元素 function processAds() { const ads = identifyAds(); ads.forEach(ad => { createAdOverlay(ad); }); } // 初始化MutationObserver监测动态广告 function initMutationObserver() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { // 检查新增的节点 mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // 检查节点本身是否是广告 if (!isInWhitelist(node)) { const hasAdClassName = adRules.classNames.some(className => node.classList?.contains(className) ); const hasAdId = adRules.ids.some(id => node.id === id ); const hasAdAttribute = adRules.attributes.some(rule => { return node.getAttribute(rule.name) === rule.value; }); // 检查节点中的链接 let hasAdLink = false; if (node.tagName === 'A') { const href = node.getAttribute('href'); if (href) { hasAdLink = adRules.linkPatterns.some(pattern => pattern.test(href) ); } } if (!hasAdLink) { const links = node.querySelectorAll('a'); links.forEach(link => { const href = link.getAttribute('href'); if (href) { if (adRules.linkPatterns.some(pattern => pattern.test(href))) { hasAdLink = true; } } }); } // 只匹配明确的广告特征 const isAd = (hasAdClassName || hasAdId || hasAdAttribute) && hasAdLink; // 如果是广告,添加覆盖层 if (isAd) { createAdOverlay(node); } } // 检查节点的子元素是否有广告 const childAds = []; const possibleAdElements = node.querySelectorAll('div, section, article'); possibleAdElements.forEach(element => { if (isInWhitelist(element)) { return; } // 检查CSS类名 const hasAdClassName = adRules.classNames.some(className => element.classList.contains(className) ); // 检查ID const hasAdId = adRules.ids.some(id => element.id === id ); // 检查属性 const hasAdAttribute = adRules.attributes.some(rule => { return element.getAttribute(rule.name) === rule.value; }); // 检查链接 let hasAdLink = false; if (element.tagName === 'A') { const href = element.getAttribute('href'); if (href) { hasAdLink = adRules.linkPatterns.some(pattern => pattern.test(href) ); } } // 检查子元素中的链接 if (!hasAdLink) { const links = element.querySelectorAll('a'); links.forEach(link => { const href = link.getAttribute('href'); if (href) { if (adRules.linkPatterns.some(pattern => pattern.test(href))) { hasAdLink = true; } } }); } // 只匹配明确的广告特征 const isAd = (hasAdClassName || hasAdId || hasAdAttribute) && hasAdLink; // 如果是广告,添加到列表 if (isAd) { childAds.push(element); } }); childAds.forEach(ad => { createAdOverlay(ad); }); } }); }); }); // 开始监测 observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false }); } // 初始化右侧推荐动态监测 function initSidebarObserver() { try { // 添加节流,避免频繁触发 let observerTimeout; let lastExecutionTime = 0; const MIN_EXECUTION_INTERVAL = 1000; // 最小执行间隔,1秒 const observer = new MutationObserver((mutations) => { // 检查是否有新添加的节点(主要关注这个) const hasNewNodes = mutations.some(mutation => mutation.addedNodes.length > 0 ); // 只有当有新节点添加时才考虑触发 if (hasNewNodes) { // 检查是否在最小执行间隔内 const currentTime = Date.now(); if (currentTime - lastExecutionTime < MIN_EXECUTION_INTERVAL) { return; } // 节流处理,1秒内只执行一次 clearTimeout(observerTimeout); observerTimeout = setTimeout(() => { const config = getConfig(); if (config.hideRightSidebar) { console.log('DOM变化触发,处理右侧推荐'); hideRightSidebar(); lastExecutionTime = Date.now(); } }, 1000); } }); // 优化监测配置,减少监测范围 observer.observe(document.body, { childList: true, // 只监测子节点变化 subtree: true, // 监测所有子树 attributes: false, // 关闭属性监测 characterData: false, // 关闭内容监测 }); console.log('右侧推荐动态监测初始化成功'); } catch (e) { console.error('初始化右侧推荐监测失败:', e); } } // 绑定滚动事件处理懒加载 function bindScrollEvent() { try { let scrollTimeout; let lastScrollY = window.scrollY; let lastExecutionTime = 0; const MIN_EXECUTION_INTERVAL = 2000; // 最小执行间隔,2秒 const MIN_SCROLL_DISTANCE = 200; // 最小滚动距离,200px window.addEventListener('scroll', () => { // 增加防抖时间到1000ms clearTimeout(scrollTimeout); // 计算滚动距离,只在滚动一定距离后触发 const currentScrollY = window.scrollY; const scrollDistance = Math.abs(currentScrollY - lastScrollY); const currentTime = Date.now(); // 只有当滚动距离大于200px且超过最小执行间隔时才触发 if (scrollDistance > MIN_SCROLL_DISTANCE && currentTime - lastExecutionTime > MIN_EXECUTION_INTERVAL) { scrollTimeout = setTimeout(() => { // 检查当前配置是否需要隐藏右侧推荐 const config = getConfig(); if (config.hideRightSidebar) { console.log('滚动事件触发,检查右侧推荐'); hideRightSidebar(); lastExecutionTime = currentTime; } // 更新上次滚动位置 lastScrollY = currentScrollY; }, 1000); // 1000ms防抖,减少执行次数 } }); console.log('滚动事件绑定成功'); } catch (e) { console.error('绑定滚动事件失败:', e); } } // 添加定时器定期检查右侧推荐 function addPeriodicCheck() { try { // 添加执行标志,避免重复执行 let isChecking = false; // 增加检查间隔到15秒 setInterval(() => { const config = getConfig(); if (config.hideRightSidebar && !isChecking) { isChecking = true; console.log('定期检查右侧推荐'); // 执行检查 hideRightSidebar(); // 检查完成后释放标志 setTimeout(() => { isChecking = false; }, 1000); } }, 15000); // 15秒间隔,减少执行次数 console.log('定期检查定时器设置成功'); } catch (e) { console.error('设置定期检查失败:', e); } } // 初始化脚本 function init() { try { console.log('开始初始化脚本'); // 创建配置菜单 createConfigMenu(); // 初始化动态监测 initMutationObserver(); // 初始化右侧推荐动态监测 initSidebarObserver(); // 绑定滚动事件处理懒加载 bindScrollEvent(); // 添加定期检查 addPeriodicCheck(); // 初始处理页面上的广告 processAds(); // 应用配置 - 减少初始执行次数,避免影响页面加载 const config = getConfig(); if (config.hideRightSidebar) { console.log('配置需要隐藏右侧推荐,计划执行'); // 只执行一次,5秒后(确保所有内容完全加载) setTimeout(hideRightSidebar, 5000); } console.log('B站广告覆盖脚本初始化成功'); } catch (e) { console.error('脚本初始化失败:', e); } } // 页面加载完成后初始化 function setupScript() { try { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { // 延迟执行,确保页面完全加载 setTimeout(init, 500); } } catch (e) { console.error('脚本设置失败:', e); } } // 启动脚本 setupScript(); })();