// ==UserScript== // @name B站去广告 // @namespace bili-adblock-scriptcat // @version 2.10.0 // @description 首页:接口滤广告 + 清 SSR 广告/白卡/空壳 + 隐藏楼层;详情页:清横幅/右栏广告 + 一键下载最高清视频/音频流 + 移除官方悬浮栏(自带回顶按钮)。 // @author b站脚本 // @match *://www.bilibili.com/ // @match *://www.bilibili.com/index.html // @match *://www.bilibili.com/?* // @match *://www.bilibili.com/video/* // @run-at document-start // @noframes // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant unsafeWindow // @inject-into page // 视频流常走 PCDN(如 *.edge.mountaintoys.cn:4483、*.mcdn.bilivideo.cn),域名不固定, // 白名单列不全会被管理器拦截导致下载失败,这里声明全域名访问。 // @connect * // @compatible edge 脚本猫/篡改猴 // @compatible chrome 脚本猫/篡改猴 // @license MIT // ==/UserScript== (function () { 'use strict'; // =========================================================================== // 纯逻辑核心(无浏览器依赖)。 // tests/api-filter.test.mjs 直接 require 本文件,浏览器测试页走 // window.__BILI_ADBLOCK_CORE__ seam —— 判定规则只改这里,别再复制副本。 // =========================================================================== // 统一 URL 判定(hook 与 filter 必须一致,否则钩到了却不过滤) const RELEVANT_URL_RE = /\/x\/web-interface\/(?:index\/top\/(?:feed\/)?rcmd|wbi\/index\/top\/feed\/rcmd|popular(?:\/series)?|search\/type)/; function isRelevantUrl(url) { return RELEVANT_URL_RE.test(String(url || '')); } // 播放地址接口:只读观测(下载用),绝不改写响应 const PLAYURL_RE = /\/x\/player\/(?:wbi\/)?playurl/; function isAdItem(item) { if (!item || typeof item !== 'object') return false; if (item.is_ad === 1 || item.is_ad === true) return true; if (item.isAd === 1 || item.isAd === true) return true; if (typeof item.ad_cb === 'string' && item.ad_cb.length > 0) return true; if (item.creative_id && item.source_id && String(item.source_id) === '5614') return true; if (item.type === 'bili_ad' || item.item === 'bili_ad') return true; // is_ad_loc 单独出现不够狠,要再带上商业特征 if (item.is_ad_loc === 1 && (item.ad_cb || item.creative_id)) return true; return false; } function isFeedItem(it) { return !!( it && typeof it === 'object' && (it.bvid || it.aid || it.pic || it.cover || it.goto || it.is_ad !== undefined) ); } // 只动这些路径上的数组,避免深层改写把 Vue 弄炸 const FEED_ARRAY_PATHS = [ ['data', 'item'], ['data', 'list'], ['data', 'archives'], ['data', 'items'], ]; function getPath(obj, path) { let cur = obj; for (let i = 0; i < path.length; i++) { if (!cur || typeof cur !== 'object') return undefined; cur = cur[path[i]]; } return cur; } function filterFeedArrays(json) { if (!json || typeof json !== 'object') return json; for (const path of FEED_ARRAY_PATHS) { const parent = path.length > 1 ? getPath(json, path.slice(0, -1)) : json; const key = path[path.length - 1]; if (!parent || typeof parent !== 'object') continue; const arr = parent[key]; if (!Array.isArray(arr) || !arr.length) continue; if (!arr.some((it) => it && (isAdItem(it) || isFeedItem(it)))) continue; parent[key] = arr.filter((it) => it != null && !isAdItem(it)); } return json; } function filterJsonText(text) { try { const json = JSON.parse(text); const filtered = filterFeedArrays(json); if (!filtered || typeof filtered !== 'object') return text; return JSON.stringify(filtered); } catch (_) { return text; } } const CORE = { isRelevantUrl, PLAYURL_RE, isAdItem, isFeedItem, filterFeedArrays, filterJsonText }; // Node(tests/api-filter.test.mjs require 本文件时只导出纯逻辑,不跑浏览器代码) if (typeof document === 'undefined' && typeof module !== 'undefined' && module.exports) { module.exports = CORE; return; } // 浏览器测试页(tests/api-filter-browser.html)取同一份逻辑 if (typeof window !== 'undefined' && window.__BILI_ADBLOCK_CORE__) { Object.assign(window.__BILI_ADBLOCK_CORE__, CORE); return; } // =========================================================================== // 浏览器侧 // =========================================================================== function isHomePage() { try { const path = location.pathname || '/'; return path === '/' || path === '/index.html'; } catch (_) { return false; } } function isVideoPage() { try { return (location.pathname || '').startsWith('/video/'); } catch (_) { return false; } } // 非目标页整段不跑(@match 已限首页/视频页,SPA 离开后再判一次) if (!isHomePage() && !isVideoPage()) return; const options = { filterApi: true, cleanDom: true, hideNonVideo: true, download: true }; function filterPayload(url, text) { if (!options.filterApi) return text; if (!isHomePage()) return text; // 接口过滤只作用于首页信息流 if (!isRelevantUrl(url)) return text; if (!text || text.length > 2_000_000) return text; return filterJsonText(text); } // --------------------------------------------------------------------------- // 接口过滤 + playurl 观测 // XHR 在 open 时就把 responseText / response 换成带过滤的 getter —— 页面无论 // 在什么事件里、以什么顺序读取,拿到的都是过滤后的数据。playurl 只做只读 // 观测(供下载面板用),绝不改写。 // --------------------------------------------------------------------------- function hookXHR() { const XHR = unsafeWindow && unsafeWindow.XMLHttpRequest ? unsafeWindow.XMLHttpRequest : XMLHttpRequest; if (!XHR || XHR.__biliAdBlocked) return; const proto = XHR.prototype; const descText = Object.getOwnPropertyDescriptor(proto, 'responseText'); const descResp = Object.getOwnPropertyDescriptor(proto, 'response'); const canText = !!(descText && descText.get); const canResp = !!(descResp && descResp.get); const origOpen = proto.open; proto.open = function (method, url, ...rest) { this.__biliUrl = String(url || ''); this.__biliTap = PLAYURL_RE.test(this.__biliUrl) ? 'playurl' : isRelevantUrl(this.__biliUrl) ? 'filter' : null; if (this.__biliTap && !this.__biliPatched && (canText || canResp)) { this.__biliPatched = true; const xhr = this; const readText = () => { const raw = canText ? descText.get.call(xhr) : ''; if (xhr.readyState !== 4 || typeof raw !== 'string') return raw; if (xhr.__biliTap === 'playurl') { if (raw) observePlayurl(parseJson(raw)); return raw; // 只观测,不改写 } if (xhr.__biliOut === undefined) xhr.__biliOut = filterPayload(xhr.__biliUrl, raw); return xhr.__biliOut; }; if (canText) { Object.defineProperty(xhr, 'responseText', { configurable: true, get: readText }); } if (canResp) { Object.defineProperty(xhr, 'response', { configurable: true, get() { const raw = descResp.get.call(xhr); const t = xhr.responseType; if (t === '' || t === 'text') return canText ? readText() : raw; if (t === 'json' && raw && typeof raw === 'object') { if (xhr.__biliTap === 'playurl') { observePlayurl(raw); return raw; } try { return filterFeedArrays(raw); } catch (_) { return raw; } } return raw; }, }); } } return origOpen.call(this, method, url, ...rest); }; XHR.__biliAdBlocked = true; } function hookFetch() { const g = unsafeWindow || window; if (!g.fetch || g.fetch.__biliAdBlocked) return; const orig = g.fetch; g.fetch = function (input, init) { const url = typeof input === 'string' ? input : input instanceof URL ? input.href : (input && input.url) || ''; const tap = PLAYURL_RE.test(url) ? 'playurl' : isRelevantUrl(url) ? 'filter' : null; if (!tap) return orig.call(this, input, init); return orig.call(this, input, init).then((res) => { try { if (tap === 'playurl') { res .clone() .json() .then((j) => observePlayurl(j)) .catch(() => {}); return res; } return res.clone().text().then((text) => { const next = filterPayload(url, text); if (!next || next === text || !next.trim().startsWith('{')) return res; const headers = new Headers(res.headers); headers.delete('content-length'); // 长度已变 headers.delete('content-encoding'); // 原编码与新 body 不符 return new Response(next, { status: res.status, statusText: res.statusText, headers, }); }); } catch (_) { return res; } }); }; g.fetch.__biliAdBlocked = true; } // --------------------------------------------------------------------------- // 下载模块(详情页):playurl 观测 + __playinfo__ 兜底,一键最高清 // --------------------------------------------------------------------------- let lastPlayurl = null; // { data, at } function observePlayurl(json) { try { if (json && json.code === 0 && json.data && (json.data.dash || json.data.durl)) { lastPlayurl = { data: json.data, at: Date.now() }; } } catch (_) {} } function parseJson(text) { try { return JSON.parse(text); } catch (_) { return null; } } function getPlayData() { if (lastPlayurl && lastPlayurl.data) return lastPlayurl.data; try { const pi = unsafeWindow.__playinfo__; if (pi && pi.code === 0 && pi.data && (pi.data.dash || pi.data.durl)) return pi.data; } catch (_) {} return null; } const CODEC_RANK = { avc1: 0, avc3: 0, hev1: 1, hvc1: 1, av01: 2 }; function codecRank(codecs) { const k = String(codecs || '').slice(0, 4); return k in CODEC_RANK ? CODEC_RANK[k] : 9; } const QUALITY_FALLBACK = { 127: '8K 超清', 126: '杜比视界', 125: 'HDR 真彩', 120: '4K 超清', 116: '1080P60 高帧率', 112: '1080P 高码率', 80: '1080P 高清', 74: '720P60 高帧率', 64: '720P 高清', 32: '480P 清晰', 16: '360P 流畅', }; function safeName(s) { return String(s || 'bilibili') .replace(/[\\/:*?"<>|]+/g, '_') .replace(/\s+/g, ' ') .trim() .slice(0, 80); } function pageTitle() { try { return safeName(document.title.replace(/_哔哩哔哩_bilibili.*$/, '').replace(/_bilibili.*$/, '')); } catch (_) { return 'bilibili'; } } function saveBlob(blob, filename) { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.documentElement.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 4000); } // 依次尝试主/备用 CDN(B 站常把流发到随机 PCDN,单个域名可能被拦或超时) function gmDownload(urlCandidates, filename, onProgress, onDone, onError) { const urls = (Array.isArray(urlCandidates) ? urlCandidates : [urlCandidates]).filter(Boolean); let i = 0; const tryNext = () => { if (i >= urls.length) { onError && onError(new Error('all cdn failed')); return; } const url = urls[i++]; GM_xmlhttpRequest({ method: 'GET', url, headers: { Referer: 'https://www.bilibili.com/' }, responseType: 'arraybuffer', onprogress: (e) => { if (onProgress && e.total > 0) onProgress((e.loaded / e.total) * 100); }, onload: (res) => { if (res.status >= 200 && res.status < 300 && res.response) { try { saveBlob(new Blob([res.response]), filename); onDone && onDone(); } catch (e) { tryNext(); } } else { tryNext(); } }, onerror: tryNext, ontimeout: tryNext, }); }; tryNext(); } function dlInjectCss() { GM_addStyle(` #bili-dl-btn,#bili-top-btn,#bili-sponsor-btn{ position:fixed;right:20px;z-index:2147483000; display:flex;align-items:center;gap:7px; padding:10px 16px 12px;border-radius:14px; border:1px solid rgba(251,114,153,.35); background:linear-gradient(135deg,rgba(251,114,153,.18),rgba(0,174,236,.08)),rgba(18,19,22,.9); backdrop-filter:blur(10px); color:#f1f2f3;font-size:13px;font-weight:600;cursor:pointer; box-shadow:0 8px 24px rgba(0,0,0,.35); transition:transform .15s ease,opacity .25s ease,box-shadow .15s ease,border-color .15s ease; user-select:none;white-space:nowrap; } #bili-dl-btn{bottom:88px} #bili-top-btn{bottom:140px} #bili-sponsor-btn{bottom:192px} #bili-sponsor-btn .bili-dl-ico{color:#fb7299} #bili-top-btn.hide{opacity:0;pointer-events:none;transform:translateY(8px)} #bili-dl-btn:hover,#bili-top-btn:hover,#bili-sponsor-btn:hover{ transform:translateY(-1px); border-color:rgba(251,114,153,.75); box-shadow:0 10px 28px rgba(251,114,153,.28); } #bili-dl-btn:active,#bili-top-btn:active,#bili-sponsor-btn:active{transform:translateY(0)} #bili-dl-btn .bili-dl-ico,#bili-top-btn .bili-dl-ico,#bili-sponsor-btn .bili-dl-ico{width:15px;height:15px;color:#fb7299;flex-shrink:0} #bili-dl-btn.busy .bili-dl-ico{animation:bili-dl-pulse 1.1s ease infinite} #bili-dl-btn .bili-dl-bar{ position:absolute;left:12px;right:12px;bottom:5px;height:2px;border-radius:2px; background:rgba(255,255,255,.14);overflow:hidden;opacity:0;transition:opacity .2s; } #bili-dl-btn.busy .bili-dl-bar{opacity:1} #bili-dl-btn .bili-dl-bar i{ display:block;height:100%;width:0;border-radius:2px; background:linear-gradient(90deg,#fb7299,#fc8bab);transition:width .2s; } #bili-dl-btn.done{border-color:rgba(103,194,58,.6)} #bili-dl-btn.done .bili-dl-ico, #bili-dl-btn.done .bili-dl-txt{color:#67c23a} #bili-dl-btn.err{border-color:rgba(245,108,108,.6)} #bili-dl-btn.err .bili-dl-ico, #bili-dl-btn.err .bili-dl-txt{color:#f56c6c} @keyframes bili-dl-pulse{50%{opacity:.35;transform:translateY(2px)}} `); } // 一键下载:最高清视频流 + 最高音质音频流,两个文件分开保存,无选项无面板 let dlBusy = false; function onDlClick(btn) { if (dlBusy) return; const label = btn.querySelector('.bili-dl-txt'); const fill = btn.querySelector('.bili-dl-bar i'); const data = getPlayData(); const dash = data && data.dash; if (!dash || !Array.isArray(dash.video) || !dash.video.length) { label.textContent = '先播放几秒'; setTimeout(() => (label.textContent = '下载视频'), 2000); return; } const aq = data.accept_quality || []; const ad = data.accept_description || []; const qName = {}; aq.forEach((q, i) => (qName[q] = ad[i] || QUALITY_FALLBACK[q] || q + 'P')); // 最高清晰度;同档多编码时选兼容性最好的一条 let best = null; for (const v of dash.video) { if (!best || v.id > best.id || (v.id === best.id && codecRank(v.codecs) < codecRank(best.codecs))) { best = v; } } const streamUrls = (s) => [s.baseUrl || s.base_url].concat(s.backupUrl || s.backup_url || []).filter(Boolean); const auds = [] .concat(dash.flac && dash.flac.audio ? dash.flac.audio : []) .concat(dash.dolby && dash.dolby.audio ? dash.dolby.audio : []) .concat(dash.audio || []) .sort((a, b) => b.id - a.id); dlBusy = true; btn.classList.add('busy'); let progV = 0; let progA = auds.length ? 0 : 100; let vDone = false; let aDone = !auds.length; let err = false; const render = () => { const overall = (progV + progA) / 2; fill.style.width = Math.min(100, overall) + '%'; label.textContent = overall >= 100 ? '保存中…' : '下载中 ' + Math.floor(overall) + '%'; btn.title = '视频 ' + Math.floor(progV) + '% · 音频 ' + Math.floor(progA) + '%'; }; const maybeFinish = () => { if (!(vDone && aDone)) return; dlBusy = false; btn.classList.remove('busy'); btn.title = ''; if (err) { btn.classList.add('err'); label.textContent = '下载失败,点重试'; } else { btn.classList.add('done'); fill.style.width = '100%'; label.textContent = '✓ 已保存'; } setTimeout(() => { btn.classList.remove('done', 'err'); fill.style.width = '0'; label.textContent = '下载视频'; }, 3000); }; render(); gmDownload( streamUrls(best), safeName(pageTitle() + '_' + (qName[best.id] || best.id)) + '_video.m4s', (p) => { progV = p; render(); }, () => { vDone = true; render(); maybeFinish(); }, () => { vDone = true; err = true; render(); maybeFinish(); } ); if (auds.length) { const a0 = auds[0]; gmDownload( streamUrls(a0), safeName(pageTitle()) + '_audio.m4s', (p) => { progA = p; render(); }, () => { aDone = true; render(); maybeFinish(); }, () => { aDone = true; err = true; render(); maybeFinish(); } ); } } let dlBtnShown = false; function ensureDlButton() { if (document.getElementById('bili-dl-btn')) return; if (!document.body) return; const btn = document.createElement('div'); btn.id = 'bili-dl-btn'; btn.innerHTML = '' + '下载视频' + ''; btn.onclick = () => onDlClick(btn); document.body.appendChild(btn); dlBtnShown = true; } function removeDlButton() { if (!dlBtnShown) return; const b = document.getElementById('bili-dl-btn'); if (b) b.remove(); dlBtnShown = false; } // 自带「顶部」回顶按钮:替代被移除的官方悬浮栏,滚动超过一屏才出现 let topBtnShown = false; function updateTopBtnVisibility() { const b = document.getElementById('bili-top-btn'); if (!b) return; let y = 0; try { y = window.scrollY || document.documentElement.scrollTop || 0; } catch (_) {} b.classList.toggle('hide', y < 240); } function ensureTopButton() { if (document.getElementById('bili-top-btn')) return; if (!document.body) return; const btn = document.createElement('div'); btn.id = 'bili-top-btn'; btn.title = '返回顶部'; btn.innerHTML = '' + '顶部'; btn.onclick = () => { try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (_) { window.scrollTo(0, 0); } }; document.body.appendChild(btn); topBtnShown = true; updateTopBtnVisibility(); } function removeTopButton() { if (!topBtnShown) return; const b = document.getElementById('bili-top-btn'); if (b) b.remove(); topBtnShown = false; } // 爱发电赞助入口:首页/详情页常驻 const AFDIAN_URL = 'https://afdian.com/a/wenjings'; let sponsorBtnShown = false; function ensureSponsorButton() { if (document.getElementById('bili-sponsor-btn')) return; if (!document.body) return; const btn = document.createElement('div'); btn.id = 'bili-sponsor-btn'; btn.title = '去爱发电赞助作者'; btn.innerHTML = '' + '赞助'; btn.onclick = () => { try { window.open(AFDIAN_URL, '_blank', 'noopener'); } catch (_) { location.href = AFDIAN_URL; } }; document.body.appendChild(btn); sponsorBtnShown = true; } function removeSponsorButton() { if (!sponsorBtnShown) return; const b = document.getElementById('bili-sponsor-btn'); if (b) b.remove(); sponsorBtnShown = false; } // body 一就绪就出按钮,不等 2s 轮询;SPA 切页时由 MutationObserver 触发同样立即创建 // 下载按钮只在详情页;回顶/赞助按钮首页/详情页都要(首页原生悬浮栈已被移除) function ensureUiButtons() { if (!document.body) return; const home = isHomePage(); const video = isVideoPage(); if (!home && !video) return; if (video) ensureDlButton(); ensureTopButton(); ensureSponsorButton(); } // --------------------------------------------------------------------------- // DOM 清理 // 首页:SSR 商业卡 / 反拦截白卡 / 空壳 / 分区条 / 轮播 / 直播番剧楼层 // 详情页:左横幅(#slide_ad/.strip-ad)+ 右栏广告卡(.video-card-ad-small) // + 统一广告位(.ad-report)+ tianma 课程带货卡(依据实际页面结构) // --------------------------------------------------------------------------- function injectCss() { GM_addStyle(` [data-bili-adblock][data-adblock-removed]{ display:none!important; width:0!important;height:0!important; margin:0!important;padding:0!important; overflow:hidden!important; position:absolute!important;left:-9999px!important; background:transparent!important;pointer-events:none!important; } /* 统一广告位标记(首页/详情页通用) */ .ad-report{display:none!important} /* 详情页:左侧横幅广告 + 右栏广告卡 */ #slide_ad, .slide-ad-exp, .strip-ad, .left-banner, .video-card-ad-small, .video-card-ad-small-inner{ display:none!important; } /* 激进加载:加载锚点热区向上扩 2000px(其内容楼层卡已被隐藏,无视觉影响) */ .load-more-anchor{ height:2000px!important; margin-top:-2000px!important; } /* 骨架屏:数据到达前的灰色占位动画。V8 结构下骨架与真实内容是兄弟节点 (骨架在 .bili-video-card 内部,封面是兄弟渲染位),隐藏不影响懒加载封面 */ .bili-video-card__skeleton{ display:none!important; } /* 右下角官方悬浮栈总容器(小窗/稍后再看PIP、刷新内容、客服/新版反馈、更多、原生顶部)。 之前只藏子项漏了 PIP 入口和原生顶部,这里整锅端掉;回顶由脚本自带按钮提供 */ .palette-button-wrap, .flexible-roll-btn, .storage-box{ display:none!important; } .feed-card:empty, .bili-feed-card:empty{ display:none!important;width:0!important;height:0!important; margin:0!important;padding:0!important;overflow:hidden!important; } /* 分区条 / 顶部分区频道(不动 .bili-header__bar 主栏) */ .bili-header__channel, .left-fixed-channel, .channel-icons, .right-channel-container, .channel-items__left, .channel-items__right, .header-channel, .header-channel-fixed, .fixed-channel-shim, .header-channel-fixed-left, .header-channel-fixed-center, .header-channel-fixed-right{ display:none!important; height:0!important;margin:0!important;padding:0!important; overflow:hidden!important; } /* 顶部楼层区:直播/番剧/课堂等混排行 */ .floor-single-card{ display:none!important; } .recommended-swipe, .recommended-swipe *{ display:none!important; height:0!important;min-height:0!important;max-height:0!important; margin:0!important;padding:0!important;overflow:hidden!important; } .bili-feed4-layout, .bili-feed4-layout > .feed2, .recommended-container_floor-aside, .recommended-container_floor-aside > .container.is-version8{ padding-top:12px!important;margin-top:0!important;min-height:0!important; } button.primary-btn.roll-btn, .roll-btn{ display:none!important; } `); } function hideEl(el) { if (!el || !el.isConnected) return; try { if (el === document.body || el === document.documentElement || el.id === 'app') return; el.setAttribute('data-bili-adblock', '1'); el.dataset.adblockRemoved = '1'; setTimeout(() => { try { if (el.isConnected) el.remove(); } catch (_) {} }, 0); } catch (_) {} } // 详情页是 Vue 管理的 DOM:remove 节点会让 Vue 后续 patch 崩溃($scopedSlots undefined), // 这里只打标记靠 CSS 隐藏,不动 DOM 结构 function hideOnly(el) { if (!el || !el.isConnected) return; try { if (el === document.body || el === document.documentElement || el.id === 'app') return; el.setAttribute('data-bili-adblock', '1'); el.dataset.adblockRemoved = '1'; } catch (_) {} } // 反拦截白卡的类名是动态的:按增量补样式,而不是只处理第一次见到的 const appliedDetectClasses = new Set(); function hideAdblockDetect() { try { const classes = new Set(); for (const style of document.querySelectorAll('style')) { const t = style.textContent || ''; if (!t.includes('被AdGuard') && !t.includes('AdBlock类插件屏蔽')) continue; const re = /\.([a-zA-Z][\w-]{5,})/g; let m; while ((m = re.exec(t))) { const cls = m[1]; // user/reply/comment/ip 等前缀排除:反拦截样式里若混入正常 UI 类名(如 user-ip)不误伤 if (/^(n-|bpx-|bili-|v-|recommended|feed-card|container|user|reply|comment|ip([-_]|$))/.test(cls)) continue; if (cls.length > 40) continue; classes.add(cls); } } if (!classes.size) return; const delta = Array.from(classes).filter((c) => !appliedDetectClasses.has(c)); if (!delta.length) return; const css = delta.map((c) => '.' + c + '{display:none!important}').join('\n'); try { GM_addStyle(css); } catch (_) { const s = document.createElement('style'); s.textContent = css; document.documentElement.appendChild(s); } delta.forEach((c) => appliedDetectClasses.add(c)); classes.forEach((cls) => { document.querySelectorAll('.' + cls).forEach((el) => { hideEl(el.closest('.bili-video-card') || el.closest('.feed-card') || el); }); }); } catch (_) {} } // 每张卡只体检一次:滚动时 MutationObserver 每帧触发,别对几百张卡反复扫文案 const seenCards = new WeakSet(); const seenVideoCards = new WeakSet(); function inspectHomeCard(card) { if (card.dataset.adblockRemoved || seenCards.has(card)) return; // SSR / 渲染后的商业卡 if ( card.querySelector( 'a[href*="cm.bilibili.com"], a[href*="/cm/api/fees/"], a[data-target-url*="cm.bilibili.com"]' ) || card.querySelector('img[src*="/bfs/sycp/"], source[srcset*="/bfs/sycp/"]') ) { hideEl(card.closest('.feed-card') || card); return; } // 信息流里的直播 / 番剧卡:只看链接与角标,不按标题文案猜(标题含「直播中」的正常视频会误杀) if (options.hideNonVideo && card.matches('.feed-card, .feed-card .bili-video-card')) { const a = card.querySelector('a[href]'); const href = (a && (a.getAttribute('href') || a.getAttribute('data-target-url') || '')) || ''; const isLive = /live\.bilibili\.com/.test(href) || !!card.querySelector('.bili-video-card__info--living, .living, .live-badge'); const isBangumi = /\/bangumi\//.test(href) || /\/anime\//.test(href); if (isLive || isBangumi) { hideEl(card.closest('.feed-card') || card); return; } } seenCards.add(card); } // 详情页广告卡(依据 2026-09 实抓页面结构): // .ad-report 统一广告位 / #slide_ad 左横幅 / .video-card-ad-small 右栏卡 // a[href*="cm.bilibili.com"] 商业跳转 / /bfs/sycp/ 广告素材 / csource=Hp_tianma 课程带货 // 正常推荐卡是 .video-page-card-small,只在命中上述特征时才删。 function inspectVideoCard(el) { if (el.dataset.adblockRemoved || seenVideoCards.has(el)) return; const CARD_SEL = '.video-card-ad-small, .strip-ad, .left-banner, .video-page-card-small, .video-page-card, #slide_ad, .fixed-sidenav-storage'; const hit = el.querySelector('a[href*="cm.bilibili.com"], a[data-target-url*="cm.bilibili.com"]') || el.querySelector('img[src*="/bfs/sycp/"], source[srcset*="/bfs/sycp/"]') || el.querySelector('a[href*="csource=Hp_tianma"], a[data-target-url*="csource=Hp_tianma"]') || (el.matches('.ad-report, #slide_ad, .strip-ad, .left-banner, .video-card-ad-small, .fixed-sidenav-storage') ? el : null); if (hit) { hideOnly(el.closest(CARD_SEL) || el); return; } seenVideoCards.add(el); } // --------------------------------------------------------------------------- // 激进加载 // 1) IntersectionObserver 补丁:视口根且未显式设 rootMargin 的观察器统一提前 // 2000px 触发(无限滚动加载锚点因此提前预取下一批); // 2) CSS 把 .load-more-anchor 热区向上扩展 2000px(其内容楼层卡已被隐藏,无视觉影响); // 3) 深扫时把视口前方 4 屏内的懒加载封面改为立即加载。 // --------------------------------------------------------------------------- function patchIntersectionObserver() { try { const g = unsafeWindow || window; const OrigIO = g.IntersectionObserver; if (!OrigIO || OrigIO.__biliPatched) return; const wrapped = function (callback, options) { try { if (!options || (!options.root && !options.rootMargin)) { options = Object.assign({}, options, { rootMargin: '0px 0px 2000px 0px' }); } } catch (_) {} return new OrigIO(callback, options); }; wrapped.prototype = OrigIO.prototype; wrapped.__biliPatched = true; g.IntersectionObserver = wrapped; } catch (_) {} } function eagerNearbyCovers() { try { const vh = window.innerHeight || 800; document .querySelectorAll( '.recommended-container_floor-aside img[loading="lazy"], .bili-feed4-layout img[loading="lazy"]' ) .forEach((img) => { const top = img.getBoundingClientRect().top; if (top < vh * 4 && top > -vh * 2) img.loading = 'eager'; }); } catch (_) {} } // 广告卡内容是后填充的:卡片首次体检时可能还没有 cm 链接,WeakSet 会一直跳过 // (实测 WorkBuddy 广告卡因此漏删,且其标记嵌在正常卡片内部造成「一大一小」错位)。 // 深扫兜底:定时全局探测 cm 链接/广告素材,出现即整卡移除,不管 WeakSet。 function probeLateAds() { try { document .querySelectorAll('a[href*="cm.bilibili.com"], a[data-target-url*="cm.bilibili.com"]') .forEach((a) => { hideEl(a.closest('.feed-card, .bili-feed-card, .bili-video-card') || a); }); document.querySelectorAll('img[src*="/bfs/sycp/"], source[srcset*="/bfs/sycp/"]').forEach((el) => { hideEl(el.closest('.feed-card, .bili-feed-card, .bili-video-card') || el); }); } catch (_) {} } function cleanDomOnce(deep) { if (!options.cleanDom) return; const home = isHomePage(); const video = isVideoPage(); if (!home && !video) return; try { if (home) { // 反拦截白卡只出现在首页信息流;详情页不跑,避免误伤评论区等正常 UI hideAdblockDetect(); if (deep) { probeLateAds(); eagerNearbyCovers(); } document.querySelectorAll('.feed-card, .bili-video-card').forEach(inspectHomeCard); // 「换一换/刷新内容」:B 站改版后类名不再叫 roll-btn,按文案兜底隐藏 document .querySelectorAll( '.recommended-container_floor-aside button, .recommended-container_floor-aside a, .bili-feed4-layout button' ) .forEach((b) => { const t = (b.textContent || '').replace(/\s+/g, ''); if (t === '换一换' || t === '刷新内容') hideEl(b); }); // 真正空壳(无子元素) document.querySelectorAll('.feed-card, .bili-feed-card').forEach((el) => { if (el.dataset.adblockRemoved) return; if (el.children.length === 0 && !(el.textContent || '').trim()) { hideEl(el); } }); } if (video) { // 官方右侧悬浮栏(小窗/客服/顶部)整组移除,回顶由脚本自带按钮替代 document .querySelectorAll('.ad-report, #slide_ad, .strip-ad, .left-banner, .video-card-ad-small, .fixed-sidenav-storage') .forEach(inspectVideoCard); // 兜底:商业链接 / 广告素材出现在任何容器里 document .querySelectorAll('a[href*="cm.bilibili.com"], img[src*="/bfs/sycp/"], source[srcset*="/bfs/sycp/"], a[href*="csource=Hp_tianma"]') .forEach((el) => { const card = el.closest('.video-card-ad-small, .strip-ad, .left-banner, .video-page-card-small, .video-page-card') || el; inspectVideoCard(card); }); } } catch (_) {} } function startDomClean() { injectCss(); dlInjectCss(); let obs = null; let pending = false; // deep=true 时额外跑 probeLateAds(rAF 高频路径不跑,2s 轮询与定时扫描才跑) const scan = (deep) => { pending = false; if (!isHomePage() && !isVideoPage()) return; ensureUiButtons(); cleanDomOnce(!!deep); }; const onMut = () => { if (pending) return; pending = true; requestAnimationFrame(() => scan(false)); }; const ensureObs = () => { if (obs) return; obs = new MutationObserver(onMut); obs.observe(document.documentElement, { childList: true, subtree: true }); scan(true); }; // 定时器负责:SPA 离开目标页停扫省 CPU、回来恢复,并兼做兜底清理 setInterval(() => { const active = isHomePage() || isVideoPage(); if (active) { ensureObs(); scan(true); } else if (obs) { obs.disconnect(); obs = null; pending = false; } if (isVideoPage()) { ensureDlButton(); ensureTopButton(); ensureSponsorButton(); } else if (isHomePage()) { removeDlButton(); ensureTopButton(); ensureSponsorButton(); } else { removeDlButton(); removeTopButton(); removeSponsorButton(); } }, 2000); window.addEventListener('scroll', updateTopBtnVisibility, { passive: true }); [500, 1500, 3000, 6000].forEach((t) => setTimeout(() => scan(true), t)); if (isHomePage() || isVideoPage()) { ensureObs(); ensureUiButtons(); } document.addEventListener('DOMContentLoaded', ensureUiButtons, { once: true }); } console.info('[B站去广告] v2.10.0 已加载(首页过滤+清理+深扫+激进加载 | 详情页清理+一键下载+回顶+赞助)'); patchIntersectionObserver(); hookXHR(); hookFetch(); startDomClean(); })();