// ==UserScript==
// @name 【哔哩哔哩】屏蔽视频PCDN地址
// @version 0.3.5
// @description 从官方CDN加载视频
// @icon https://static.hdslb.com/images/favicon.ico
// @match https://www.bilibili.com/video/*
// @match https://www.bilibili.com/list/*
// @match https://www.bilibili.com/bangumi/play/*
// @match https://www.bilibili.com/blackboard/live/live-activity-player.html*
// @match https://live.bilibili.com/*
// @grant unsafeWindow
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-start
// @namespace https://github.com/AkagiYui/UserScript
// @supportURL https://github.com/AkagiYui/UserScript/issues
// @homepage https://github.com/AkagiYui
// @author AkagiYui
// @license MIT
// ==/UserScript==
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ 507:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const menu_1 = __webpack_require__(997);
const logger_1 = __webpack_require__(686);
const video_1 = __importDefault(__webpack_require__(683));
const live_1 = __importDefault(__webpack_require__(682));
const { debug, useLogger: subLogger } = (0, logger_1.useLogger)("bilibili-ban-pcdn");
const { getConfig } = (0, menu_1.useBooleanMenu)({
blockPlayError: {
title: "屏蔽“播放遇到问题?”提示",
defaultValue: false,
},
blockBCacheCDN: {
title: "屏蔽视频地区CDN",
defaultValue: false,
},
blockLivePCDN: {
title: "屏蔽直播PCDN",
defaultValue: false,
},
keepOneUrl: {
title: "保留至少一条播放链接",
defaultValue: true,
},
});
const matchUrls = {
live: ["https://www.bilibili.com/blackboard/live/live-activity-player.html", "https://live.bilibili.com/"],
video: ["https://www.bilibili.com/video/", "https://www.bilibili.com/list/"],
bangumi: ["https://www.bilibili.com/bangumi/play/"],
};
const getUrlType = (url) => {
for (const [type, patterns] of Object.entries(matchUrls)) {
for (const pattern of patterns) {
if (url.includes(pattern)) {
return type;
}
}
}
return null;
};
const pageWindow = unsafeWindow;
// 屏蔽“播放遇到问题?”提示
if (getConfig("blockPlayError")) {
const originalDefineProperty = pageWindow.Object.defineProperty;
pageWindow.Object.defineProperty = function (target, propertyKey, descriptor) {
if (propertyKey === "videoHasBuffered") {
originalDefineProperty(target, "showLoadTimeoutFeedback", {
get: () => () => {
debug("屏蔽“播放遇到问题?”提示");
},
set: () => {
pageWindow.Object.defineProperty = originalDefineProperty;
},
});
}
return originalDefineProperty(target, propertyKey, descriptor);
};
}
if (getUrlType(location.href) === "video" || getUrlType(location.href) === "bangumi") {
(0, video_1.default)(subLogger, getConfig);
}
else if (getUrlType(location.href) === "live") {
(0, live_1.default)(subLogger, getConfig);
}
/***/ }),
/***/ 682:
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports["default"] = (useLogger, getConfig) => {
const { log, debug } = useLogger("live");
const pageWindow = unsafeWindow;
// 屏蔽直播P2P视频流信息
if (getConfig("blockLivePCDN")) {
function processPlayurlInfo(playurlInfo) {
if (!playurlInfo)
return;
playurlInfo.p2p_data.m_p2p = false;
playurlInfo.p2p_data.m_servers = null;
playurlInfo.stream.forEach((stream) => {
stream.format.forEach((format) => {
format.codec.forEach((codec) => {
codec.url_info = codec.url_info.filter((urlInfo) => {
const keep = !urlInfo.host.includes("mcdn.bilivideo");
debug("保留链接", keep, urlInfo.host);
return keep;
});
});
});
});
}
// 替换SSR属性__NEPTUNE_IS_MY_WAIFU__
let __NEPTUNE_IS_MY_WAIFU__ = pageWindow.__NEPTUNE_IS_MY_WAIFU__;
Object.defineProperty(pageWindow, "__NEPTUNE_IS_MY_WAIFU__", {
get: () => __NEPTUNE_IS_MY_WAIFU__,
set: (value) => {
if (value.roomInitRes) {
log("直播房间信息", "处理前", JSON.parse(JSON.stringify(value.roomInitRes)));
processPlayurlInfo(value.roomInitRes.data.playurl_info?.playurl);
log("直播房间信息", "处理后", JSON.parse(JSON.stringify(value.roomInitRes)));
}
__NEPTUNE_IS_MY_WAIFU__ = value;
},
});
let oldFetch = pageWindow.fetch;
function hookFetch(url, init) {
if (typeof url === "string") {
if (url.includes("api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo")) {
log("请求直播列表");
return new Promise((resolve, reject) => {
oldFetch.apply(this, arguments).then((response) => {
const oldJson = response.json;
response.json = function () {
return new Promise((resolve, reject) => {
oldJson.apply(this, arguments).then((result) => {
log("直播列表", "fetch", "处理前", JSON.parse(JSON.stringify(result)));
processPlayurlInfo(result.data.playurl_info?.playurl);
log("直播列表", "fetch", "处理后", JSON.parse(JSON.stringify(result)));
resolve(result);
});
});
};
resolve(response);
});
});
}
}
return oldFetch.apply(this, arguments);
}
// 对window.fetch挂载成我们的劫持函数hookFetch
pageWindow.fetch = hookFetch;
const originalXHR = pageWindow.XMLHttpRequest;
const xhrOpen = originalXHR.prototype.open;
originalXHR.prototype.open = function (_, url) {
if (url.includes("api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo")) {
log("请求直播列表");
const getter = Object.getOwnPropertyDescriptor(originalXHR.prototype, "responseText").get;
Object.defineProperty(this, "responseText", {
get: () => {
const response = getter.call(this);
const responseJson = JSON.parse(response);
log("直播列表", "xhr", "处理前", JSON.parse(JSON.stringify(responseJson)));
processPlayurlInfo(responseJson.data.playurl_info?.playurl);
log("直播列表", "xhr", "处理后", JSON.parse(JSON.stringify(responseJson)));
return JSON.stringify(responseJson);
},
});
}
return xhrOpen.apply(this, arguments);
};
}
// 未来可能考虑屏蔽出方向的P2P
};
/***/ }),
/***/ 683:
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({ value: true }));
const PCDN_REGEX_PATTERN = /mcdn.bilivideo.(com|cn)/;
const BCACHE_REGEX_PATTERN = /(cn-.*\.bilivideo\.(com|cn))/;
exports["default"] = (useLogger, getConfig) => {
const { log, debug } = useLogger("video");
const pageWindow = unsafeWindow;
// 挑出有用的链接
const removeSomeUrls = (allUrls) => {
const keepOneUrl = getConfig("keepOneUrl");
const blockBCacheCDN = getConfig("blockBCacheCDN");
const filterUrls = (urls, pattern) => {
return urls.filter((url) => {
const keep = !pattern.test(url);
debug("保留链接", keep, url);
return keep;
});
};
const applyFilter = (urls, pattern, filterName) => {
debug(`过滤${filterName}链接`);
const filteredUrls = filterUrls(urls, pattern);
if (filteredUrls.length === 0) {
debug(`仅包含${filterName}链接,${keepOneUrl ? "保留所有播放链接" : "无可用链接"}`);
return keepOneUrl ? urls : [];
}
return filteredUrls;
};
let restUrls = applyFilter(allUrls, PCDN_REGEX_PATTERN, "PCDN");
if (blockBCacheCDN) {
restUrls = applyFilter(restUrls, BCACHE_REGEX_PATTERN, "自建地区CDN");
}
return { baseUrl: restUrls[0], backupUrls: restUrls.slice(1) };
};
// 处理资源数据
const cleanPlayInfo = (playInfo) => {
log("处理前", JSON.parse(JSON.stringify(playInfo)));
if (playInfo.data) {
log("非番剧视频");
cleanNonBangumiVideo(playInfo.data);
}
else if (playInfo.result) {
log("番剧视频");
cleanBangumiVideo(playInfo.result);
}
log("处理后", JSON.parse(JSON.stringify(playInfo)));
};
const cleanNonBangumiVideo = (data) => {
if (data.dash) {
cleanDash(data.dash);
}
if (data.durl) {
log("试看视频");
cleanDurl(data.durl);
}
};
const cleanBangumiVideo = (result) => {
if (!result.video_info) {
log("番剧播放列表不存在,可能是没有大会员或未承包");
return;
}
const videoInfo = result.video_info;
if (videoInfo.dash) {
cleanDash(videoInfo.dash);
}
else if (videoInfo.durl || videoInfo.durls) {
log("试看番剧");
if (videoInfo.durl) {
cleanDurl(videoInfo.durl);
}
if (videoInfo.durls) {
videoInfo.durls.forEach((durlGroup) => cleanDurl(durlGroup.durl));
}
}
else {
log("番剧播放列表不存在,可能是没有大会员或未承包");
}
};
const cleanDash = (dash) => {
const cleanMedia = (media) => {
const { baseUrl, backupUrls } = removeSomeUrls([media.baseUrl, ...media.backupUrl]);
media.baseUrl = media.base_url = baseUrl;
media.backupUrl = media.backup_url = backupUrls;
};
dash.video.forEach(cleanMedia);
dash.audio?.forEach(cleanMedia); // 部分视频没有音频流
dash.dolby?.audio && dash.dolby.audio.forEach(cleanMedia); // 杜比
dash.flac?.audio && cleanMedia(dash.flac.audio); // Hi-Res
};
const cleanDurl = (durls) => {
durls.forEach((durl) => {
const { baseUrl, backupUrls } = removeSomeUrls([durl.url, ...durl.backup_url]);
durl.url = baseUrl;
durl.backup_url = backupUrls;
});
};
// 播放器初始化参数
let __playinfo__ = pageWindow.__playinfo__;
Object.defineProperty(pageWindow, "__playinfo__", {
get: () => __playinfo__,
set: (value) => {
log("初始化参数", value);
cleanPlayInfo(value);
__playinfo__ = value;
},
});
// 播放列表请求处理
const originalXHR = pageWindow.XMLHttpRequest;
const xhrOpen = originalXHR.prototype.open;
originalXHR.prototype.open = function (_, url) {
if (url.includes("api.bilibili.com/x/player/wbi/playurl")) {
// 包括单个视频的多个(画质数量*编码数量)的url
const avid = url.match(/avid=(\d+)/)?.[1]; // 提取出url中的avid参数
log("请求视频列表", `av${avid}`);
const getter = Object.getOwnPropertyDescriptor(originalXHR.prototype, "responseText").get;
Object.defineProperty(this, "responseText", {
get: () => {
const response = getter.call(this);
const responseJson = JSON.parse(response);
cleanPlayInfo(responseJson);
return JSON.stringify(responseJson);
},
});
}
if (url.includes("api.bilibili.com/pgc/player/web/v2/playurl")) {
const season_id = url.match(/season_id=(\d+)/)?.[1]; // 提取出url中的season_id参数
const ep_id = url.match(/ep_id=(\d+)/); // 提取出url中的ep_id参数
log("请求番剧列表", `ss${season_id}`, ep_id ? `ep${ep_id[1]}` : "ep_id not found");
const getter = Object.getOwnPropertyDescriptor(originalXHR.prototype, "responseText").get;
Object.defineProperty(this, "responseText", {
get: () => {
const response = getter.call(this);
const responseJson = JSON.parse(response);
cleanPlayInfo(responseJson);
return JSON.stringify(responseJson);
},
});
}
return xhrOpen.apply(this, arguments);
};
};
/***/ }),
/***/ 686:
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.useLogger = void 0;
const createLoggerFunction = (consoleMethod, prefix, name) => consoleMethod.bind(console, prefix, name ? `[${name}]` : "");
/**
* 生成 Logger
* @param name 前缀
* @returns console.log
*/
const useLogger = (name) => {
const prefix = "AkagiYui";
return {
log: createLoggerFunction(console.log, prefix, name),
warn: createLoggerFunction(console.warn, prefix, name),
error: createLoggerFunction(console.error, prefix, name),
info: createLoggerFunction(console.info, prefix, name),
debug: createLoggerFunction(console.debug, prefix, name),
useLogger: (subName) => (0, exports.useLogger)(`${name ? name + ":" : ""}${subName}`),
};
};
exports.useLogger = useLogger;
/***/ }),
/***/ 997:
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.useBooleanMenu = void 0;
/**
* 布尔菜单配置
* @param configs 配置项
* @returns 配置获取函数
*/
const useBooleanMenu = (configs) => {
// 缓存
const cache = {};
// 获取配置
const getConfig = (key) => {
if (cache[key] !== undefined) {
return cache[key];
}
let value = GM_getValue(key, configs[key].defaultValue);
cache[key] = value;
return value;
};
// 配置注册
let menuIds = [];
const registerMenuCommand = () => {
menuIds.forEach((id) => {
GM_unregisterMenuCommand(id);
});
menuIds = [];
Object.entries(configs).forEach(([key, config]) => {
let commandName = getConfig(key) ? "✅" : "❌";
commandName += ` ${config.title}`;
let id = GM_registerMenuCommand(commandName, () => {
let newValue = !getConfig(key);
let valueToSet = config.callback ? config.callback(newValue) : newValue;
GM_setValue(key, valueToSet);
cache[key] = valueToSet;
registerMenuCommand();
});
menuIds.push(id);
});
};
registerMenuCommand();
return { getConfig };
};
exports.useBooleanMenu = useBooleanMenu;
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module is referenced by other modules so it can't be inlined
/******/ var __webpack_exports__ = __webpack_require__(507);
/******/
/******/ })()
;