// ==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();
})();