// ==UserScript==
// @name DCjanus BiliBili Tweaks
// @name:zh-CN DCjanus B 站增强
// @namespace https://github.com/dcjanus/userscripts
// @version 20260426
// @description useful tweaks for bilibili.com
// @author kookxiang, DCjanus
// @match https://*.bilibili.com/*
// @icon https://raw.githubusercontent.com/DCjanus/userscripts/master/assets/bilibili-avatar.svg
// @run-at document-body
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM_notification
// ==/UserScript==
// 去掉叔叔去世时的全站黑白效果
GM_addStyle(
'html, body { -webkit-filter: none !important; filter: none !important; }',
);
// 屏蔽屏蔽提示
GM_addStyle(
'.adblock-tips, .feed-card:has(.bili-video-card>div:empty) { display: none !important; }',
);
// 没用的 URL 参数
const uselessUrlParams = [
'buvid',
'is_story_h5',
'launch_id',
'live_from',
'mid',
'session_id',
'timestamp',
'trackid',
'up_id',
'vd_source',
/^share/,
/^spm/,
];
function getRequestUrl(input) {
if (typeof input === 'string') return input;
if (input instanceof URL || input instanceof unsafeWindow.URL) {
return input.toString();
}
if (input?.url) return input.url;
return undefined;
}
function replaceRequestUrl(input, url) {
if (typeof input === 'string') return url;
if (input instanceof URL || input instanceof unsafeWindow.URL) {
return new unsafeWindow.URL(url);
}
const RequestCtor = unsafeWindow.Request || globalThis.Request;
if (RequestCtor && input instanceof RequestCtor) {
return new RequestCtor(url, input);
}
return url;
}
function defineReadonlyGlobal(name, value) {
try {
Object.defineProperty(unsafeWindow, name, {
get() {
return value;
},
set() {},
enumerable: false,
configurable: false,
});
} catch (e) {
try {
unsafeWindow[name] = value;
} catch (e) {}
}
}
// Block WebRTC,CNM 陈睿你就缺这点棺材钱?
try {
class _RTCPeerConnection {
addEventListener() {}
createDataChannel() {}
}
class _RTCDataChannel {}
Object.defineProperty(unsafeWindow, 'RTCPeerConnection', {
value: _RTCPeerConnection,
enumerable: false,
writable: false,
});
Object.defineProperty(unsafeWindow, 'RTCDataChannel', {
value: _RTCDataChannel,
enumerable: false,
writable: false,
});
Object.defineProperty(unsafeWindow, 'webkitRTCPeerConnection', {
value: _RTCPeerConnection,
enumerable: false,
writable: false,
});
Object.defineProperty(unsafeWindow, 'webkitRTCDataChannel', {
value: _RTCDataChannel,
enumerable: false,
writable: false,
});
} catch (e) {}
// 移除鸿蒙字体,系统自带它不香吗?
Array.from(
document.querySelectorAll('link[href*=\\/jinkela\\/long\\/font\\/]'),
).forEach((x) => x.remove());
GM_addStyle('html, body { font-family: initial !important; }');
// 首页优化
if (location.host === 'www.bilibili.com') {
GM_addStyle(
'.feed2 .feed-card:has(a[href*="cm.bilibili.com"]), .feed2 .feed-card:has(.bili-video-card:empty) { display: none } .feed2 .container > * { margin-top: 0 !important }',
);
}
// 动态页面优化
if (location.host === 't.bilibili.com') {
GM_addStyle(
'html[wide] #app { display: flex; } html[wide] .bili-dyn-home--member { box-sizing: border-box;padding: 0 10px;width: 100%;flex: 1; } html[wide] .bili-dyn-content { width: initial; } html[wide] main { margin: 0 8px;flex: 1;overflow: hidden;width: initial; } #wide-mode-switch { margin-left: 0;margin-right: 20px; } #wide-mode-switch.floating { position: fixed; right: 24px; bottom: 24px; z-index: 10000; padding: 8px 12px; border-radius: 6px; color: #fff; background: #00aeec; box-shadow: 0 2px 10px rgba(0, 0, 0, .18); } .bili-dyn-list__item:has(.bili-dyn-card-goods), .bili-dyn-list__item:has(.bili-rich-text-module.goods) { display: none !important }',
);
if (!localStorage.WIDE_OPT_OUT) {
document.documentElement.setAttribute('wide', 'wide');
}
function injectWideModeSwitch() {
if (document.querySelector('#wide-mode-switch')) return true;
const tabContainer =
document.querySelector('.bili-dyn-list-tabs__list') ||
document.querySelector('.bili-dyn-content') ||
document.querySelector('main');
if (!tabContainer) return false;
const switchButton = document.createElement('a');
switchButton.id = 'wide-mode-switch';
switchButton.className = 'bili-dyn-list-tabs__item';
switchButton.textContent = '宽屏模式';
switchButton.addEventListener('click', function (e) {
e.preventDefault();
if (localStorage.WIDE_OPT_OUT) {
localStorage.removeItem('WIDE_OPT_OUT');
document.documentElement.setAttribute('wide', 'wide');
} else {
localStorage.setItem('WIDE_OPT_OUT', '1');
document.documentElement.removeAttribute('wide');
}
});
if (tabContainer.matches('.bili-dyn-list-tabs__list')) {
const placeHolder = document.createElement('div');
placeHolder.style.flex = 1;
tabContainer.appendChild(placeHolder);
} else {
switchButton.classList.add('floating');
}
tabContainer.appendChild(switchButton);
return true;
}
window.addEventListener('load', function () {
if (injectWideModeSwitch()) return;
const observer = new MutationObserver(() => {
if (injectWideModeSwitch()) observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
// 去广告
GM_addStyle(
'.ad-report, a[href*="cm.bilibili.com"] { display: none !important; }',
);
if (unsafeWindow.__INITIAL_STATE__?.adData) {
for (const key in unsafeWindow.__INITIAL_STATE__.adData) {
if (!Array.isArray(unsafeWindow.__INITIAL_STATE__.adData[key]))
continue;
for (const item of unsafeWindow.__INITIAL_STATE__.adData[key]) {
item.name = 'B 站未来有可能会倒闭,但绝不会变质';
item.pic = 'https://static.hdslb.com/images/transparent.gif';
item.url = 'https://space.bilibili.com/208259';
}
}
}
// 去充电列表(叔叔的跳过按钮越做越小了,就尼玛离谱)
if (unsafeWindow.__INITIAL_STATE__?.elecFullInfo) {
unsafeWindow.__INITIAL_STATE__.elecFullInfo.list = [];
}
// 修复文章区复制
if (
location.href.startsWith('https://www.bilibili.com/read/cv') ||
location.href.startsWith('https://www.bilibili.com/opus/')
) {
if (unsafeWindow.original) unsafeWindow.original.reprint = '1';
function unlockArticleCopy() {
document
.querySelectorAll('.article-holder, .opus-module-content')
.forEach((holder) => {
holder.classList.remove('unable-reprint');
});
}
document.addEventListener(
'copy',
(e) => e.stopImmediatePropagation(),
true,
);
unlockArticleCopy();
window.addEventListener('load', unlockArticleCopy);
new MutationObserver(unlockArticleCopy).observe(document.body, {
childList: true,
subtree: true,
});
}
// 去 P2P CDN
Object.defineProperty(unsafeWindow, 'PCDNLoader', {
value: class {},
enumerable: false,
writable: false,
});
Object.defineProperty(unsafeWindow, 'BPP2PSDK', {
value: class {
on() {}
},
enumerable: false,
writable: false,
});
Object.defineProperty(unsafeWindow, 'SeederSDK', {
value: class {},
enumerable: false,
writable: false,
});
if (
location.href.startsWith('https://www.bilibili.com/video/') ||
location.href.startsWith('https://www.bilibili.com/bangumi/play/')
) {
let cdnDomain;
function replaceP2PUrl(url) {
cdnDomain ||= document.head.innerHTML.match(
/up[\w-]+\.bilivideo\.com/,
)?.[0];
try {
const urlObj = new URL(url);
const hostName = urlObj.hostname;
if (urlObj.hostname.endsWith('.mcdn.bilivideo.cn')) {
urlObj.host = cdnDomain || 'upos-sz-mirrorcoso1.bilivideo.com';
urlObj.port = 443;
console.warn(`更换视频源: ${hostName} -> ${urlObj.host}`);
return urlObj.toString();
} else if (urlObj.hostname.endsWith('.szbdyd.com')) {
urlObj.host = urlObj.searchParams.get('xy_usource');
urlObj.port = 443;
console.warn(`更换视频源: ${hostName} -> ${urlObj.host}`);
return urlObj.toString();
}
return url;
} catch (e) {
return url;
}
}
function replaceP2PUrlDeep(obj) {
if (!obj || typeof obj !== 'object') return;
for (const key in obj) {
if (typeof obj[key] === 'string') {
obj[key] = replaceP2PUrl(obj[key]);
} else if (
Array.isArray(obj[key]) ||
typeof obj[key] === 'object'
) {
replaceP2PUrlDeep(obj[key]);
}
}
}
replaceP2PUrlDeep(unsafeWindow.__playinfo__);
(function (HTMLMediaElementPrototypeSrcDescriptor) {
Object.defineProperty(unsafeWindow.HTMLMediaElement.prototype, 'src', {
...HTMLMediaElementPrototypeSrcDescriptor,
set: function (value) {
HTMLMediaElementPrototypeSrcDescriptor.set.call(
this,
replaceP2PUrl(value),
);
},
});
})(
Object.getOwnPropertyDescriptor(
unsafeWindow.HTMLMediaElement.prototype,
'src',
),
);
(function (open) {
unsafeWindow.XMLHttpRequest.prototype.open = function () {
try {
arguments[1] = replaceP2PUrl(arguments[1]);
} finally {
return open.apply(this, arguments);
}
};
})(unsafeWindow.XMLHttpRequest.prototype.open);
}
// 真·原画直播
if (location.href.startsWith('https://live.bilibili.com/')) {
const LIVE_PLAY_INFO_PATH = '/xlive/web-room/v2/index/getRoomPlayInfo';
unsafeWindow.disableLiveP2P = true;
unsafeWindow.forceHighestQuality =
localStorage.getItem('forceHighestQuality') === 'true';
let recentErrors = 0;
setInterval(() => {
recentErrors = Math.floor(recentErrors / 2);
}, 10000);
function isLivePlayInfoUrl(url) {
try {
return new URL(url, location.href).pathname === LIVE_PLAY_INFO_PATH;
} catch (e) {
return false;
}
}
function preferHighestLiveQuality(input) {
const url = getRequestUrl(input);
if (!unsafeWindow.forceHighestQuality || !url) return input;
try {
const urlObj = new URL(url, location.href);
if (urlObj.pathname !== LIVE_PLAY_INFO_PATH) return input;
urlObj.searchParams.set('qn', '30000');
return replaceRequestUrl(input, urlObj.toString());
} catch (e) {
return input;
}
}
function rewriteLiveMediaUrl(url) {
const mcdnRegexp = /[xy0-9]+\.mcdn\.bilivideo\.cn:\d+/;
const smtcdnsRegexp = /[\w.]+\.smtcdns.net\/([\w-]+\.bilivideo.com\/)/;
const qualityRegexp = /(live-bvc\/\d+\/live_\d+_\d+)_\w+/;
if (mcdnRegexp.test(url) && unsafeWindow.disableLiveP2P) {
return { blocked: true, url };
}
if (smtcdnsRegexp.test(url) && unsafeWindow.disableLiveP2P) {
return { blocked: false, url: url.replace(smtcdnsRegexp, '$1') };
}
if (qualityRegexp.test(url) && unsafeWindow.forceHighestQuality) {
return {
blocked: false,
url: url
.replace(qualityRegexp, '$1')
.replace(/(\d+)_(mini|pro)hevc/g, '$1'),
};
}
return { blocked: false, url };
}
function disableLiveP2PInPayload(payload) {
const seen = new WeakSet();
function walk(obj) {
if (!obj || typeof obj !== 'object' || seen.has(obj)) return;
seen.add(obj);
if (obj.p2p_data && typeof obj.p2p_data === 'object') {
obj.p2p_data.p2p_type = 0;
}
if (Object.prototype.hasOwnProperty.call(obj, 'need_p2p')) {
obj.need_p2p = 0;
}
for (const key in obj) walk(obj[key]);
}
walk(payload);
return payload;
}
function livePlayInfoResponse(response, payload) {
const headers = new unsafeWindow.Headers(response.headers);
headers.delete('content-length');
headers.delete('content-encoding');
headers.set('content-type', 'application/json; charset=utf-8');
return new unsafeWindow.Response(JSON.stringify(payload), {
status: response.status,
statusText: response.statusText,
headers,
});
}
const oldFetch = unsafeWindow.fetch;
unsafeWindow.fetch = function () {
const args = Array.from(arguments);
try {
args[0] = preferHighestLiveQuality(args[0]);
const url = getRequestUrl(args[0]);
if (url) {
const rewritten = rewriteLiveMediaUrl(url);
if (rewritten.blocked) {
return Promise.reject(
new TypeError('Blocked live P2P URL'),
);
}
if (rewritten.url !== url) {
args[0] = replaceRequestUrl(args[0], rewritten.url);
}
}
const requestUrl = getRequestUrl(args[0]) || '';
return oldFetch.apply(this, args).then(async (response) => {
const responseUrl = response.url || requestUrl;
if (/\.(m3u8|m4s)(?:[?#]|$)/.test(responseUrl)) {
if ([403, 404].includes(response.status)) recentErrors++;
}
if (recentErrors >= 5 && unsafeWindow.forceHighestQuality) {
recentErrors = 0;
unsafeWindow.forceHighestQuality = false;
GM_notification({
title: '最高清晰度可能不可用',
text: '已为您自动切换至播放器上选择的清晰度.',
timeout: 3000,
silent: true,
});
}
if (!isLivePlayInfoUrl(requestUrl)) return response;
try {
const payload = await response.clone().json();
return livePlayInfoResponse(
response,
disableLiveP2PInPayload(payload),
);
} catch (e) {
return response;
}
});
} catch (e) {}
return oldFetch.apply(this, args);
};
// 干掉些直播间没用的东西
GM_addStyle(
'#welcome-area-bottom-vm, .web-player-icon-roomStatus { display: none !important; }',
);
}
// 视频裁切
if (location.href.startsWith('https://www.bilibili.com/video/')) {
GM_addStyle(
'body[video-fit] #bilibili-player video { object-fit: cover; } .bpx-player-ctrl-setting-fit-mode { display: flex;width: 100%;height: 32px;line-height: 32px; } .bpx-player-ctrl-setting-box .bui-panel-wrap, .bpx-player-ctrl-setting-box .bui-panel-item { min-height: 172px !important; }',
);
let timer;
function toggleMode(enabled) {
if (enabled) {
document.body.setAttribute('video-fit', '');
} else {
document.body.removeAttribute('video-fit');
}
}
function injectButton() {
if (!document.querySelector('.bpx-player-ctrl-setting-menu-left')) {
return;
}
clearInterval(timer);
const parent = document.querySelector(
'.bpx-player-ctrl-setting-menu-left',
);
const item = document.createElement('div');
item.className = 'bpx-player-ctrl-setting-fit-mode bui bui-switch';
item.innerHTML =
'';
parent.insertBefore(
item,
document.querySelector('.bpx-player-ctrl-setting-more'),
);
document
.querySelector('.bpx-player-ctrl-setting-fit-mode input')
.addEventListener('change', (e) => toggleMode(e.target.checked));
document.querySelector(
'.bpx-player-ctrl-setting-box .bui-panel-item',
).style.height = '';
}
timer = setInterval(injectButton, 200);
}
// 去除地址栏多余参数
unsafeWindow.history.replaceState(
undefined,
undefined,
removeTracking(location.href),
);
const pushState = unsafeWindow.history.pushState;
unsafeWindow.history.pushState = function (state, unused, url) {
return pushState.apply(this, [state, unused, removeTracking(url)]);
};
const replaceState = unsafeWindow.history.replaceState;
unsafeWindow.history.replaceState = function (state, unused, url) {
return replaceState.apply(this, [state, unused, removeTracking(url)]);
};
function removeTracking(url) {
if (!url) return url;
try {
const urlObj = new URL(url, location.href);
if (!urlObj.search) return url;
const searchParams = urlObj.searchParams;
const keys = Array.from(searchParams.keys());
for (const key of keys) {
uselessUrlParams.forEach((item) => {
if (typeof item === 'string') {
if (item === key) searchParams.delete(key);
} else if (item instanceof RegExp) {
if (item.test(key)) searchParams.delete(key);
}
});
}
urlObj.search = searchParams.toString();
return urlObj.toString();
} catch (e) {
console.error(e);
return url;
}
}
// 去掉 B 站的傻逼上报
!(function () {
const oldFetch = unsafeWindow.fetch;
unsafeWindow.fetch = function (input) {
const url = getRequestUrl(input);
if (typeof url === 'string' && url.match(/(?:cm|data)\.bilibili\.com/))
return new Promise(function () {});
return oldFetch.apply(this, arguments);
};
const oldOpen = unsafeWindow.XMLHttpRequest.prototype.open;
unsafeWindow.XMLHttpRequest.prototype.open = function (method, url) {
const requestUrl = getRequestUrl(url);
if (
typeof requestUrl === 'string' &&
requestUrl.match(/(?:cm|data)\.bilibili\.com/)
) {
this.send = function () {};
}
return oldOpen.apply(this, arguments);
};
try {
Object.defineProperty(unsafeWindow.navigator, 'sendBeacon', {
value: () => true,
enumerable: false,
writable: false,
configurable: false,
});
} catch (e) {
unsafeWindow.navigator.sendBeacon = () => true;
}
const fakeMReporterInstance = new Proxy(function () {}, {
get(target, prop) {
debugLog(`MReporterInstance.${prop} called with`, arguments);
return () => {};
},
});
defineReadonlyGlobal('MReporterInstance', fakeMReporterInstance);
const fakeMReporter = new Proxy(function () {}, {
construct() {
return fakeMReporterInstance;
},
get(target, prop) {
debugLog(`MReporter.${prop} called with`, arguments);
return () => {};
},
});
defineReadonlyGlobal('MReporter', fakeMReporter);
const sentryHub = class {
bindClient() {}
};
const fakeSentry = {
SDK_NAME: 'sentry.javascript.browser',
SDK_VERSION: '0.0.0',
BrowserClient: class {},
Hub: sentryHub,
Integrations: {
Vue: class {},
GlobalHandlers: class {},
InboundFilters: class {},
},
init() {},
configureScope() {},
getCurrentHub: () => new sentryHub(),
setContext() {},
setExtra() {},
setExtras() {},
setTag() {},
setTags() {},
setUser() {},
wrap() {},
};
if (
!unsafeWindow.Sentry ||
unsafeWindow.Sentry.SDK_VERSION !== fakeSentry.SDK_VERSION
) {
if (unsafeWindow.Sentry) {
delete unsafeWindow.Sentry;
}
defineReadonlyGlobal('Sentry', fakeSentry);
}
const fakeReporterPbInstance = new Proxy(function () {}, {
get(target, prop) {
debugLog(`ReporterPbInstance.${prop} called with`, arguments);
return () => {};
},
});
defineReadonlyGlobal('ReporterPbInstance', fakeReporterPbInstance);
const fakeReporterPb = new Proxy(function () {}, {
construct() {
return fakeReporterPbInstance;
},
get(target, prop) {
debugLog(`ReporterPb.${prop} called with`, arguments);
return () => {};
},
});
defineReadonlyGlobal('ReporterPb', fakeReporterPb);
Object.defineProperty(unsafeWindow, '__biliUserFp__', {
get() {
return {
init() {},
queryUserLog() {
return [];
},
};
},
set() {},
});
Object.defineProperty(unsafeWindow, '__USER_FP_CONFIG__', {
get() {
return undefined;
},
set() {},
});
Object.defineProperty(unsafeWindow, '__MIRROR_CONFIG__', {
get() {
return undefined;
},
set() {},
});
})();
function debugLog() {
if (unsafeWindow.__MBGA_DEBUG__) console.log.apply(this, arguments);
}