// ==UserScript==
// @name B站PCDN拦截器
// @namespace lllbbbzzz
// @version 7.1
// @description 拦截B站PCDN视频源,主动替换为正规CDN,减少上传带宽占用
// @author lllbbbzzz
// @match *://*.bilibili.com/*
// @match *://*.bilibili.tv/*
// @match *://*.bilivideo.cn/*
// @run-at document-start
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function () {
'use strict';
var KEY = 'pcdn_killer_v7';
var IND = 'pcdn-ind';
var enabled = localStorage.getItem(KEY) !== '0';
var count = 0;
/* ════════ 检测 ════════ */
function isPCDN(u) {
return u && (/\.mcdn\.bilivideo\.cn/i.test(u) || /xy\d+x\d+x\d+x\d+xy/i.test(u));
}
function isSegment(u) {
return u && (/\/seg\//i.test(u) || /\d+-\d+\.(m4s|ts)(\?|$)/i.test(u));
}
/* ════════ 主动替换CDN ════════ */
function replaceCDN(json) {
if (!enabled || !json || !json.dash) return json;
var legitPaths = [];
['video', 'audio'].forEach(function (t) {
if (!json.dash[t]) return;
json.dash[t].forEach(function (s) {
if (s.base_url && !isPCDN(s.base_url)) {
legitPaths.push(s.base_url);
}
if (s.backup_url) {
s.backup_url.forEach(function (u) {
if (!isPCDN(u)) legitPaths.push(u);
});
}
});
});
if (legitPaths.length === 0) return json;
var legitHosts = [];
legitPaths.forEach(function (u) {
try {
var h = new URL(u, location.origin).host;
if (legitHosts.indexOf(h) === -1) legitHosts.push(h);
} catch (_) { /* ignore */ }
});
if (legitHosts.length === 0) return json;
var dirty = false;
['video', 'audio'].forEach(function (t) {
if (!json.dash[t]) return;
json.dash[t].forEach(function (s) {
var newBackups = [];
if (s.backup_url) {
s.backup_url.forEach(function (u) {
if (!isPCDN(u)) {
newBackups.push(u);
} else {
dirty = true;
}
});
}
if (isPCDN(s.base_url)) {
if (newBackups.length > 0) {
s.base_url = newBackups[0];
} else {
var path = s.base_url.replace(/^https?:\/\/[^/]+/, '');
s.base_url = 'https://' + legitHosts[0] + path;
}
dirty = true;
}
while (newBackups.length < 2 && legitHosts.length > 0) {
var host = legitHosts[newBackups.length % legitHosts.length];
var ref = s.base_url.replace(/^https?:\/\/[^/]+/, '');
newBackups.push('https://' + host + ref);
}
s.backup_url = newBackups;
});
});
if (dirty) {
count++;
render();
console.log('%c[PCDN Killer] CDN源已替换为正规线路', 'color:#00c853;font-weight:bold');
}
return json;
}
/* ════════ Hook fetch ════════ */
var _fetch = window.fetch;
window.fetch = function (input) {
var url = typeof input === 'string' ? input : (input && input.url) || '';
var isPlay = /\/playurl/i.test(url);
if (enabled && isPCDN(url) && isSegment(url)) {
count++;
render();
console.log('%c[PCDN Killer] 拦截分片', 'color:#ff9800', url.slice(0, 100));
return Promise.reject(new DOMException('blocked', 'AbortError'));
}
return _fetch.apply(this, arguments).then(function (res) {
if (!enabled || !isPlay) return res;
return res.clone().text().then(function (text) {
try {
var filtered = replaceCDN(JSON.parse(text));
return new Response(JSON.stringify(filtered), {
status: res.status,
statusText: res.statusText,
headers: res.headers
});
} catch (_) { return res; }
});
});
};
/* ════════ Hook XMLHttpRequest ════════ */
var _open = XMLHttpRequest.prototype.open;
var _send = XMLHttpRequest.prototype.send;
var _rtDesc = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText');
XMLHttpRequest.prototype.open = function (m, u) {
this._u = u;
this._isPlay = /\/playurl/i.test(u);
return _open.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
var self = this;
if (enabled && isPCDN(this._u) && isSegment(this._u)) {
count++;
render();
console.log('%c[PCDN Killer] XHR拦截', 'color:#ff9800', this._u.slice(0, 100));
try { this.abort(); } catch (_) { /* ignore */ }
return;
}
if (enabled && this._isPlay) {
this.addEventListener('readystatechange', function () {
if (self.readyState !== 4 || self.status !== 200) return;
try {
var text = _rtDesc.get.call(self);
var filtered = replaceCDN(JSON.parse(text));
var out = JSON.stringify(filtered);
Object.defineProperty(self, 'responseText', { value: out, writable: true, configurable: true });
Object.defineProperty(self, 'response', { value: out, writable: true, configurable: true });
} catch (_) { /* ignore */ }
});
}
return _send.apply(this, arguments);
};
/* ════════ 指示器 ════════ */
function pos() {
var sels = ['#viewbox_report h1', '.video-title-href', 'h1.video-title', '.video-title'];
for (var i = 0; i < sels.length; i++) {
var el = document.querySelector(sels[i]);
if (!el) continue;
var r = el.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
return { top: r.top + scrollY + (r.height - 28) / 2, left: r.right + 12 };
}
}
return { top: 70, left: 10 };
}
function build() {
var el = document.getElementById(IND) || document.createElement('div');
el.id = IND;
var p = pos();
var bg = enabled ? 'linear-gradient(135deg,#00c853,#00a843)' : 'linear-gradient(135deg,#e53935,#c62828)';
var label = enabled ? 'PCDN拦截' : 'PCDN·关';
if (enabled && count > 0) label += ' · ' + count;
el.style.cssText = 'position:absolute;top:' + p.top + 'px;left:' + p.left + 'px;display:inline-flex;align-items:center;gap:5px;padding:3px 12px 3px 9px;border-radius:14px;font-size:12px;font-weight:600;color:#fff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;cursor:pointer;user-select:none;line-height:1.6;background:' + bg + ';box-shadow:0 1px 6px rgba(0,0,0,.25);z-index:99999;transition:background .3s';
el.innerHTML = '' + label + '';
el.title = 'PCDN拦截: ' + (enabled ? '开启' : '关闭') + (enabled && count ? '\n本次拦截 ' + count + ' 次' : '') + '\n点击切换';
el.onclick = toggle;
return el;
}
function render() {
var old = document.getElementById(IND);
if (old) old.replaceWith(build());
}
function ensure() {
if (!document.body) return;
document.getElementById(IND) ? render() : document.body.appendChild(build());
}
/* ════════ 开关 ════════ */
function toggle() {
enabled = !enabled;
if (!enabled) count = 0;
localStorage.setItem(KEY, enabled ? '1' : '0');
render();
}
try { GM_registerMenuCommand('PCDN拦截切换', toggle); } catch (_) { /* ignore */ }
/* ════════ 定时守护 ════════ */
setInterval(function () { if (document.body) ensure(); }, 1000);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ensure);
} else {
ensure();
}
})();