// ==UserScript== // @name B站净化器 // @namespace http://tampermonkey.net/ // @version 1.3.2 // @description B站净化器 是一款Tampermonkey脚本,旨在为您提供更清爽、无广告的B站浏览体验。它能自动过滤广告、推广内容,并允许您根据播放量、视频时长、UP主、标题关键词和分区等多种条件自定义隐藏视频,让您的B站首页和视频页面只显示您真正感兴趣的内容。轻松配置,即刻享受纯净B站! // @author Kiyuiro // @match https://*.bilibili.com/* // @icon https://www.bilibili.com/favicon.ico // @grant none // @license Apache-2.0 // @downloadURL none // ==/UserScript== (function () { 'use strict'; function findElement(selector, timeout = 5000, interval = 50) { return new Promise((resolve) => { const start = Date.now(); function check() { const element = document.querySelector(selector); if (element) { resolve(element); } else if (Date.now() - start >= timeout) { resolve(undefined); } else { setTimeout(check, interval); } } check(); }); } function findElements(selector, timeout = 5000, interval = 50) { return new Promise((resolve) => { const start = Date.now(); function check() { const elements = document.querySelectorAll(selector) || []; if (elements.length > 0) { resolve(elements); } else if (Date.now() - start >= timeout) { resolve([]); } else { setTimeout(check, interval); } } check(); }); } class FetchHook { constructor() { this.originalFetch = window.fetch; // 保存原始的 fetch 函数 this.preFns = []; // 前置处理函数数组 this.postFns = []; // 后置处理函数数组 this.hook = this.hook.bind(this); this.unhook = this.unhook.bind(this); this.addPreFn = this.addPreFn.bind(this); this.addPostFn = this.addPostFn.bind(this); } // 劫持 window.fetch hook() { if (window.fetch !== this.originalFetch) { console.warn('FetchHook: fetch 已经被劫持,跳过再次劫持。'); return; } window.fetch = async (input, init) => { let processedInput = input; let processedInit = init; // 执行前置处理函数 for (const fn of this.preFns) { const result = fn(processedInput, processedInit); if (result instanceof Promise) { [processedInput, processedInit] = await result; } else if (Array.isArray(result) && result.length === 2) { [processedInput, processedInit] = result; } else { processedInput = result; // 兼容只返回 input 的情况 } if (processedInput === null || processedInput === undefined) { // console.log('FetchHook: 请求被前置函数中断。', input); return Promise.resolve(new Response(null, { status: 204, statusText: 'No Content (Hooked)' })); } } // 调用原生 fetch 发送请求 let response; try { response = await this.originalFetch.call(window, processedInput, processedInit); } catch (error) { console.error('FetchHook: 原生 fetch 请求失败。', error); throw error; } // 执行后置处理函数 let processedResponse = response; for (const fn of this.postFns) { const result = fn(processedResponse, processedInput, processedInit); if (result instanceof Promise) { processedResponse = await result; } else { processedResponse = result; } } return processedResponse; }; // console.log('FetchHook: 成功劫持 window.fetch。'); } // 恢复 window.fetch 到原始状态 unhook() { if (window.fetch === this.originalFetch) { console.warn('FetchHook: fetch 未被劫持或已恢复,跳过。'); return; } window.fetch = this.originalFetch; // console.log('FetchHook: 恢复 window.fetch 到原始状态。'); } /** * 添加一个请求发送前的处理函数 * @param {Function} fn - 处理函数,接收 (input, init) 参数,应返回 [newInput, newInit] 或 Promise<[newInput, newInit]> * 如果只返回 newInput,则 init 保持不变。 * 如果返回 null 或 undefined,则中断请求。 */ addPreFn(fn) { if (typeof fn === 'function') { this.preFns.push(fn); } else { console.warn('FetchHook: addPreFn 接收的参数不是函数。'); } } /** * 添加一个请求响应后的处理函数 * @param {Function} fn - 处理函数,接收 (response, input, init) 参数,应返回新的 Response 对象或 Promise */ addPostFn(fn) { if (typeof fn === 'function') { this.postFns.push(fn); } else { console.warn('FetchHook: addPostFn 接收的参数不是函数。'); } } /** * 移除一个前置处理函数 * @param {Function} fn */ removePreFn(fn) { this.preFns = this.preFns.filter(existingFn => existingFn !== fn); } /** * 移除一个后置处理函数 * @param {Function} fn */ removePostFn(fn) { this.postFns = this.postFns.filter(existingFn => existingFn !== fn); } } function parseDuration(duration) { const parts = duration.split(':').map(Number); let hours = 0, minutes = 0, seconds = 0; if (parts.length === 2) { // mm:ss 格式 [minutes, seconds] = parts; } else if (parts.length === 3) { // hh:mm:ss 格式 [hours, minutes, seconds] = parts; } else { throw new Error('Invalid duration format'); } return hours * 3600 + minutes * 60 + seconds; } let config; const path = window.location.href; const paths = path.split('/').filter(it => it.length > 0); const fetchHook = new FetchHook(); fetchHook.hook(); // 在脚本加载时立即劫持 fetch // 广告tips async function adblockClear() { const adblock = await findElement('.adblock-tips'); if (adblock) { adblock.remove(); } } // 扩展加载 function increaseLoad() { const preloadFn = (input, init) => { if (typeof input === 'string' && input.includes('api.bilibili.com') && input.includes('feed/rcmd') && init?.method?.toUpperCase() === "GET") { input = input.replace("&ps=12&", "&ps=24&"); // console.log(`FetchHook: 修改了B站推荐请求的 ps 参数为 ${24}。`); } return input; }; fetchHook.addPreFn(preloadFn); } // 视频过滤 function videoFilter() { config = JSON.parse(localStorage.getItem('BiliFilterConfig')) || []; // console.log(config); // 广告过滤 const filterByAd = (it) => { if (!config.ad) return false; const el = it.querySelector('.bili-video-card__stats--text') return !!(el && el.innerHTML === '广告'); } // 播放量过滤 const filterByPlayCount = (it) => { if (!config.video?.playCount) return false; const el = it.querySelector('.bili-video-card__stats--text') if (!el || el.innerHTML === '广告') return false; let count; const countText = el.innerHTML; if (countText.includes('万')) { count = parseFloat(countText.replace('万', '')) * 10000; } else if (countText.includes('亿')) { count = parseFloat(countText.replace('亿', '')) * 100000000; } else { count = parseInt(countText); } return count < config.video.playCount; } // 时长过滤 const filterByDuration = (it) => { if (!config.video?.playCount) return false; const el = it.querySelector('.bili-video-card__stats__duration') if (!el) return false; const configDuration = parseDuration(config.video.duration); const duration = parseDuration(el.innerHTML); return duration < configDuration; } // 推广过滤 const filterByProm = (it) => { if (!config.prom) return false; const el = it.querySelector('.vui_icon'); return !!el; } // UP主过滤 const filterByUp = (it) => { if (!config.upList || config.upList.length === 0) return false; const el = it.querySelector('.bili-video-card__info--author'); if (!el) return false; return config.upList.some(up => el.title.includes(up)); } // 标题过滤 const filterByTitle = (it) => { if (!config.titleList || config.titleList.length === 0) return false; const el = it.querySelector('.bili-video-card__info--tit') if (!el) return false; return config.titleList.find(title => el.title.includes(title)); } // 分区过滤 const filterByPart = (it) => { if (!it.className.includes('floor-single-card')) return false; if (!config.partList || config.partList.length === 0) return false; if (config.partList[0] === 'ALL') return true; const el = it.querySelector('.floor-title'); if (!el) return false; return config.partList.some(part => el.title.includes(part)); } // 空元素清除 const filterByNull = (it) => { return it.innerHTML.length < 1; } // 过滤 const filters = [ filterByAd, filterByPlayCount, filterByDuration, filterByProm, filterByUp, filterByTitle, filterByPart, filterByNull ]; const process = (items) => { items.forEach(it => { if (filters.some(filter => filter(it))) { // console.log(it) it.remove(); } }) } // 初始化 MutationObserver const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType !== 1) return; let items; if (node.matches('.feed-card, .bili-feed-card, .floor-single-card')) { items = [node]; } else { items = Array.from(node.querySelectorAll('.feed-card, .bili-feed-card, .floor-single-card')); } process(items) }); } }); }); observer.observe(document.body, {childList: true, subtree: true}); findElements('.feed-card, .bili-feed-card, .floor-single-card').then(items => { process(items) }) } // 视频页 async function videoPage() { if (paths.indexOf('video') === -1) { return } // 删除投币展示框 setInterval(async () => { const toubi = await findElement('.bili-danmaku-x-guide-all'); if (toubi) { toubi.remove(); } }, 100) // 删除视频下广告 setInterval(async () => { const ads = await findElements('.ad-report, #slide_ad, .activity-m-v1, .video-page-game-card-small'); if (ads) { ads.forEach(ad => { ad.innerHTML = ""; }); } }, 100); // 多 p 视频列表扩展 const list = await findElement('.video-pod__body'); const video = await findElement('.bpx-player-video-area') if (list) { if (video) { setInterval(() => { list.style.maxHeight = video.offsetHeight - 165 + 'px' }, 10) } else { list.style.maxHeight = '400px'; } } } // 添加配置窗口 function addConfigOverlay() { // 过滤配置 // 定义配置窗口的 HTML、CSS 和 JavaScript const configHtml = ` `; const configCss = ` .config-window-container { background-color: #ffffff; padding: 30px; border-radius: 12px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); width: 100%; max-width: 500px; box-sizing: border-box; max-height: 80vh; /* 设置最大高度为视口高度的 80% */ overflow-y: auto; /* 启用垂直滚动 */ } .config-window-container h2, .config-window-container h3 { text-align: center; color: #1f2937; margin-bottom: 25px; font-weight: 700; } .config-section { margin-bottom: 20px; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px; background-color: #f9fafb; } .config-section label { display: block; margin-bottom: 10px; font-weight: 600; color: #374151; font-size: 0.95rem; } .config-section textarea, .config-section input[type="text"] { width: 100%; padding: 10px; margin-bottom: 15px; border: 1px solid #d1d5db; border-radius: 6px; box-sizing: border-box; min-height: 40px; resize: vertical; font-size: 0.9rem; color: #4b5563; } .config-section textarea { min-height: 80px; } .subsection { margin-top: 20px; padding-top: 15px; border-top: 1px dashed #d1d5db; } /* 开关样式 */ .config-window-container .switch { position: relative; display: inline-block; width: 50px; height: 20px; vertical-align: middle; } .config-window-container .switch input { opacity: 0; width: 0; height: 0; } .config-window-container .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #d1d5db; transition: .4s; border-radius: 28px; } .config-window-container .slider:before { position: absolute; content: ""; height: 16px; width: 24px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 28px; } .config-window-container input:checked + .slider { background-color: #2563eb; } .config-window-container input:focus + .slider { box-shadow: 0 0 1px #2563eb; } .config-window-container input:checked + .slider:before { transform: translateX(22px); } .switch-label { display: inline-block; vertical-align: middle; color: #374151; font-weight: 600; margin-right: 15px; } /* 隐藏的元素 */ .config-window-container .hidden { display: none !important; /* 使用 !important 确保覆盖 Tailwind 的 display */ } `; // 注入 Tailwind CSS CDN const tailwindScript = document.createElement('script'); tailwindScript.src = 'https://cdn.tailwindcss.com'; document.head.appendChild(tailwindScript); // 注入自定义 CSS const styleElement = document.createElement('style'); styleElement.textContent = configCss; document.head.appendChild(styleElement); // 注入配置窗口 HTML document.body.insertAdjacentHTML('beforeend', configHtml); // 配置窗口的 DOM 元素 const biliFilterConfigOverlay = document.getElementById('BiliFilterConfigOverlay'); // 配置窗口的 JavaScript 逻辑 // 确保在 DOM 元素可用后再执行 tailwindScript.onload = () => { // 等待 Tailwind 加载完成 const saveConfigButton = document.getElementById('saveConfig'); const closeConfigButton = document.getElementById('closeConfig'); // 收集所有配置数据并返回一个对象 const collectConfigData = () => { return { ad: document.getElementById('adToggle').checked, prom: document.getElementById('promToggle').checked, upList: document.getElementById('upList').value.split('\n').filter(item => item.trim() !== ''), titleList: document.getElementById('titleList').value.split('\n').filter(item => item.trim() !== ''), partList: document.getElementById('partList').value.split('\n').filter(item => item.trim() !== ''), video: { playCount: parseInt(document.getElementById('videoPlayCount').value.trim()), duration: document.getElementById('videoDuration').value.trim() }, }; }; // 加载配置数据到表单 const loadConfigData = (data) => { if (!data) { // 如果没有数据,则初始化为空对象 data = { ad: false, prom: false, upList: [], titleList: [], partList: [], video: {playCount: '', duration: ''}, }; } document.getElementById('adToggle').checked = data.ad; document.getElementById('promToggle').checked = data.prom; document.getElementById('upList').value = data.upList ? data.upList.join('\n') : ''; document.getElementById('titleList').value = data.titleList ? data.titleList.join('\n') : ''; document.getElementById('partList').value = data.partList ? data.partList.join('\n') : ''; if (data.video) { document.getElementById('videoPlayCount').value = data.video.playCount || ''; document.getElementById('videoDuration').value = data.video.duration || ''; } }; // 保存配置按钮点击事件 saveConfigButton.addEventListener('click', () => { config = collectConfigData(); console.log('保存的配置数据:', config); // 将配置数据保存到 localStorage localStorage.setItem('BiliFilterConfig', JSON.stringify(config)); const messageBox = document.createElement('div'); messageBox.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; messageBox.innerHTML = `

配置已保存

`; document.body.appendChild(messageBox); document.getElementById('messageBoxClose').addEventListener('click', () => { document.body.removeChild(messageBox); biliFilterConfigOverlay.classList.add('hidden'); // 隐藏配置窗口 }); }); // 关闭配置窗口按钮点击事件 closeConfigButton.addEventListener('click', () => { biliFilterConfigOverlay.classList.add('hidden'); // 隐藏配置窗口 }); // 注入配置按钮,在头像下面 findElement('.links-item', 999999).then(links => { if (links) { const configButton = document.createElement('div'); configButton.className = 'single-link-item'; configButton.insertAdjacentHTML('beforeend', ` `); links.appendChild(configButton); configButton.addEventListener('click', () => { loadConfigData(JSON.parse(localStorage.getItem('BiliFilterConfig'))); biliFilterConfigOverlay.classList.remove('hidden'); // 显示配置窗口 }); } }) }; } // 附加样式 function additionalStyles() { const style = document.createElement('style'); style.textContent = ` .security_content { box-sizing: content-box !important; } .load-more-anchor { position: absolute; bottom: 1000px; opacity: 0; } `; document.head.appendChild(style); } function _main() { adblockClear(); increaseLoad(); videoFilter(); videoPage(); addConfigOverlay(); additionalStyles(); } _main(); })();