// ==UserScript== // @name B站PCDN拦截器 // @namespace lllbbbzzz // @version 8.3.0 // @description 拦截B站PCDN视频源,主动替换为正规CDN。防止你成为PCDN节点 // @author lllbbbzzz // @icon https://bsyimg.luoca.net/imgtc/20260321/3e2aea17a0f2fd3b44e0cf5effccc060.webp // @match *://*.bilibili.com/* // @match *://*.bilibili.tv/* // @match *://*.bilivideo.cn/* // @run-at document-start // @grant GM_registerMenuCommand // @license MIT // ==/UserScript== (function () { 'use strict'; const KEY = 'pcdn_killer_v8'; let on = localStorage.getItem(KEY) !== '0'; let cnt = 0; let toastTimer = null; // --- 检测规则 --- const RE_PCDN_HOST = /\.mcdn\.bilivideo\.cn|szbdyd\.com|cos\.bilibili\.com\/.*pcdn/i; const RE_PCDN_PATH = /xy\d+x\d+x\d+x\d+xy|\/pcdn\/|\/mcdn\//i; const RE_PRIVATE_IP = /^https?:\/\/(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.)/; const RE_SIGNAL = /p2p|pcdn|mcdn|webrtc|signal|stun|turn/i; const RE_ADS = /\/x\/web-interface\/wbi\/index|googleads|doubleclick|adsense|googlesyndication|adsrvr|adnxs|tracking\/pixel|beacon\/report/i; function isPCDN(u) { return u && (RE_PCDN_HOST.test(u) || RE_PCDN_PATH.test(u) || RE_PRIVATE_IP.test(u)); } function isSeg(u) { return u && (/\/seg\//i.test(u) || /\d+-\d+\.(m4s|ts)(\?|$)/i.test(u)); } function isSig(u) { return u && RE_SIGNAL.test(u); } function isAd(u) { return u && RE_ADS.test(u); } // --- Toast 通知 --- function showToast() { if (toastTimer) clearTimeout(toastTimer); let el = document.getElementById('pcdn-toast'); if (!el) { el = document.createElement('div'); el.id = 'pcdn-toast'; el.style.cssText = 'position:fixed;bottom:16px;right:16px;padding:6px 14px;border-radius:8px;font:600 12px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#fff;z-index:2147483647;pointer-events:none;transition:opacity .4s;box-shadow:0 2px 8px rgba(0,0,0,.3)'; (document.body || document.documentElement).appendChild(el); } const bg = on ? '#00a843' : '#c62828'; const text = on ? 'PCDN拦截 ON' + (cnt > 0 ? ' · ' + cnt : '') : 'PCDN拦截 OFF'; el.style.cssText = el.style.cssText.replace(/background:[^;]+/, '') + ';background:' + bg; el.textContent = text; el.style.opacity = '1'; toastTimer = setTimeout(function () { el.style.opacity = '0'; }, 2500); } // --- WebRTC --- (function () { if (!window.RTCPeerConnection) return; const Orig = window.RTCPeerConnection; window.RTCPeerConnection = function (cfg, cns) { if (on && cfg && cfg.iceServers) { const urls = []; cfg.iceServers.forEach(function (s) { if (s.urls) urls.push(...(Array.isArray(s.urls) ? s.urls : [s.urls])); }); if (urls.some(function (u) { return isPCDN(u) || isSig(u); })) { cnt++; return new Orig({ iceServers: [{ urls: 'stun:0.0.0.0' }] }); } } return new Orig(cfg, cns); }; window.RTCPeerConnection.prototype = Orig.prototype; for (const k in Orig) { if (typeof Orig[k] === 'function') window.RTCPeerConnection[k] = Orig[k]; } })(); // --- WebSocket --- (function () { const Orig = window.WebSocket; window.WebSocket = function (url, proto) { if (on && isSig(url)) { cnt++; const ws = proto !== undefined ? new Orig(url, proto) : new Orig(url); setTimeout(function () { try { ws.close(); } catch (e) {} }, 0); return ws; } return proto !== undefined ? new Orig(url, proto) : new Orig(url); }; window.WebSocket.prototype = Orig.prototype; })(); // --- ServiceWorker --- (function () { if (!navigator.serviceWorker) return; const orig = navigator.serviceWorker.register; navigator.serviceWorker.register = function (url) { if (on && /p2p|pcdn|mcdn|sw-proxy|cdn-worker/i.test(url || '')) { cnt++; return Promise.reject(new DOMException('blocked', 'AbortError')); } return orig.apply(this, arguments); }; })(); // --- SharedWorker --- (function () { if (!window.SharedWorker) return; const Orig = window.SharedWorker; window.SharedWorker = function (url, opt) { if (on && /p2p|pcdn|mcdn/i.test(url || '')) { cnt++; return { port: { start: function () {} } }; } return new Orig(url, opt); }; window.SharedWorker.prototype = Orig.prototype; })(); // --- Worker --- (function () { const Orig = window.Worker; window.Worker = function (url, opt) { if (on && /p2p|pcdn|mcdn/i.test(url || '')) { cnt++; return { postMessage: function () {}, terminate: function () {}, addEventListener: function () {} }; } return new Orig(url, opt); }; window.Worker.prototype = Orig.prototype; })(); // --- sendBeacon --- (function () { if (!navigator.sendBeacon) return; const orig = navigator.sendBeacon; navigator.sendBeacon = function (url) { if (on && isSig(url)) { cnt++; return true; } return orig.apply(this, arguments); }; })(); // --- EventSource --- (function () { if (!window.EventSource) return; const Orig = window.EventSource; window.EventSource = function (url, opt) { if (on && isSig(url)) { cnt++; return { close: function () {}, addEventListener: function () {}, readyState: 2 }; } return new Orig(url, opt); }; window.EventSource.prototype = Orig.prototype; })(); // --- CDN 替换 --- function replaceCDN(json) { if (!on || !json || !json.dash) return json; const good = []; ['video', 'audio'].forEach(function (t) { if (!json.dash[t]) return; json.dash[t].forEach(function (s) { if (s.base_url && !isPCDN(s.base_url)) good.push(s.base_url); (s.backup_url || []).forEach(function (u) { if (!isPCDN(u)) good.push(u); }); }); }); if (!good.length) return json; const hosts = []; good.forEach(function (u) { try { const h = new URL(u, location.origin).host; if (hosts.indexOf(h) < 0) hosts.push(h); } catch (e) {} }); if (!hosts.length) return json; function genUrl(base, host) { try { return new URL(base).href.replace(/^[a-z]+:\/\/[^/]+/, 'https://' + host); } catch (e) { return 'https://' + host + (base || ''); } } let dirty = false; ['video', 'audio'].forEach(function (t) { if (!json.dash[t]) return; json.dash[t].forEach(function (s) { const bk = []; (s.backup_url || []).forEach(function (u) { if (!isPCDN(u)) bk.push(u); else dirty = true; }); if (isPCDN(s.base_url)) { s.base_url = bk.length ? bk[0] : genUrl(s.base_url, hosts[0]); dirty = true; } while (bk.length < 3 && hosts.length) { bk.push(genUrl(s.base_url, hosts[bk.length % hosts.length])); } s.backup_url = bk; }); }); if (dirty) cnt++; return json; } // --- fetch hook --- const _fetch = window.fetch; window.fetch = function (input) { const url = typeof input === 'string' ? input : (input && input.url) || ''; if (on && isPCDN(url) && isSeg(url)) { cnt++; return Promise.reject(new DOMException('blocked', 'AbortError')); } if (on && isAd(url)) { cnt++; return Promise.reject(new DOMException('blocked', 'AbortError')); } if (on && isSig(url)) { cnt++; return Promise.reject(new DOMException('blocked', 'AbortError')); } return _fetch.apply(this, arguments).then(function (res) { if (!on || !/\/playurl/i.test(url)) return res; return res.clone().text().then(function (text) { try { const j = JSON.parse(text); const r = replaceCDN(j); if (r === j) return res; return new Response(JSON.stringify(r), { status: res.status, statusText: res.statusText, headers: res.headers }); } catch (e) { return res; } }); }); }; // --- XHR hook --- const _open = XMLHttpRequest.prototype.open; const _send = XMLHttpRequest.prototype.send; const _rtd = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'responseText'); XMLHttpRequest.prototype.open = function (m, u) { this._u = u; this._play = /\/playurl/i.test(u); return _open.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { const self = this; if (on && isPCDN(this._u) && isSeg(this._u)) { cnt++; try { this.abort(); } catch (e) {} return; } if (on && isAd(this._u)) { cnt++; try { this.abort(); } catch (e) {} return; } if (on && this._play) { this.addEventListener('readystatechange', function () { if (self.readyState !== 4 || self.status !== 200) return; try { const t = _rtd ? _rtd.get.call(self) : self.responseText; const j = JSON.parse(t); const r = replaceCDN(j); if (r === j) return; const out = JSON.stringify(r); if (_rtd) Object.defineProperty(self, 'responseText', { value: out, writable: true, configurable: true }); Object.defineProperty(self, 'response', { value: out, writable: true, configurable: true }); } catch (e) {} }); } return _send.apply(this, arguments); }; // --- DOM 优化 --- const deferCSS = document.createElement('style'); deferCSS.textContent = '#comment,.bili-comment-container,.review-list,.bpx-player-ending,.video-card-list,.rec-list,.floor-single-card{content-visibility:auto;contain-intrinsic-size:1px 600px}'; (document.head || document.documentElement).appendChild(deferCSS); // --- 初始化 --- GM_registerMenuCommand('PCDN拦截: ' + (on ? '已开启' : '已关闭') + ' (点击切换)', function () { on = !on; if (!on) cnt = 0; localStorage.setItem(KEY, on ? '1' : '0'); showToast(); }); if (document.body) { showToast(); } else { document.addEventListener('DOMContentLoaded', showToast); } })();