Greasy Fork

Greasy Fork is available in English.

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

从官方CDN加载视频

当前为 2024-09-16 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         【哔哩哔哩】屏蔽视频PCDN地址
// @version      0.3.3
// @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,
    },
});
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) {
            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 needRemove = urlInfo.host.includes("mcdn.bilivideo");
                            debug("处理中", urlInfo.host, needRemove);
                            return !needRemove;
                        });
                    });
                });
            });
        }
        // 替换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("直播房间信息", "fetch", "处理前", JSON.parse(JSON.stringify(value.roomInitRes)));
                    processPlayurlInfo(value.roomInitRes.data.playurl_info.playurl);
                    log("直播房间信息", "fetch", "处理后", 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("直播列表", "处理前", JSON.parse(JSON.stringify(result)));
                                        processPlayurlInfo(result.data.playurl_info.playurl);
                                        log("直播列表", "处理后", 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 }));
exports["default"] = (useLogger, getConfig) => {
    const { log, debug } = useLogger("video");
    const pageWindow = unsafeWindow;
    // 挑出有用的链接
    const removeSomeUrls = (allUrls) => {
        const whichToRemove = [/mcdn.bilivideo.(com|cn)/];
        if (getConfig("blockBCacheCDN")) {
            whichToRemove.push(/(cn-.*\.bilivideo\.(com|cn))/);
        }
        const urls = allUrls.filter((url) => {
            const needRemove = whichToRemove.some((re) => re.test(url));
            debug("链接", url, needRemove);
            return !needRemove;
        });
        const baseUrl = urls[0];
        const backupUrls = urls.slice(1);
        return { baseUrl, backupUrls };
    };
    // 处理资源数据
    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);
/******/ 	
/******/ })()
;