// ==UserScript== // @name B站PCDN拦截器 // @namespace lllbbbzzz // @version 7.1.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(); } })();