【哔哩哔哩】屏蔽视频PCDN地址

从官方CDN加载视频

// ==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);
/******/ 	
/******/ })()
;