Greasy Fork

Greasy Fork is available in English.

哔哩哔哩 CDN 优选和画质固定

哔哩哔哩 CDN 优选,逻辑参考了 https://github.com/guozhigq/pilipala,大部分代码由 ai 完成。固定 cookie 中存储的默认画质,不允许哔哩哔哩修改。

// ==UserScript==
// @name         哔哩哔哩 CDN 优选和画质固定
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  哔哩哔哩 CDN 优选,逻辑参考了 https://github.com/guozhigq/pilipala,大部分代码由 ai 完成。固定 cookie 中存储的默认画质,不允许哔哩哔哩修改。
// @author       Moranjianghe
// @match        *://*.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// @connect      proxy-tf-all-ws.bilivideo.com
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    /**
     * B站视频CDN优化与MCDN代理类
     */
    class BilibiliCDNOptimizer {
        constructor() {
            // CDN 节点列表,按优先级排序
            this.cdnList = {
                'ali': 'upos-sz-mirrorali.bilivideo.com',      // 阿里云 (推荐)
                'cos': 'upos-sz-mirrorcos.bilivideo.com',      // 腾讯云
                'hw': 'upos-sz-mirrorhw.bilivideo.com',        // 华为云
                'ws': 'upos-sz-mirrorws.bilivideo.com',        // 网宿
                'bda2': 'upos-sz-mirrorbda2.bilivideo.com',    // 百度云
            };

            // 视频画质选项
            this.qualityOptions = {
                '6': '240P 极速',
                '16': '360P 流畅',
                '32': '480P 清晰',
                '64': '720P 高清',
                '74': '720P60 高帧率',
                '80': '1080P 高清',
                '100': '智能修复',
                '112': '1080P+ 高码率',
                '116': '1080P60 高帧率',
                '120': '4K 超清',
                '125': 'HDR 真彩色',
                '126': '杜比视界',
                '127': '8K 超高清',
            };

            // 使用与cdn.js相同的MCDN检测方法
            this.upgcxcodeRegex = /(https?:\/\/)(.*?)(\/upgcxcode\/)/;

            // 初始化设置
            this.enableCDNOptimize = GM_getValue('enableCDNOptimize', true);
            this.enableMCDNProxy = GM_getValue('enableMCDNProxy', true);
            this.preferredCDN = GM_getValue('preferredCDN', 'ali');
            this.debugMode = GM_getValue('debugMode', false);

            // 画质固定功能
            this.enableQualityFix = GM_getValue('enableQualityFix', false);
            this.fixedQuality = GM_getValue('fixedQuality', '127'); // 默认8K超高清

            // 备用URL存储
            this.backupUrls = new Map();

            // 初始化后台拦截
            this.initInterceptors();

            // 注册菜单命令
            this.registerMenuCommands();

            // 设置画质cookie
            if (this.enableQualityFix) {
                this.setQualityCookie();
            }

            this.log("B站视频CDN优化与MCDN代理已初始化");
        }

        /**
         * 日志记录函数
         * @param {string} message - 日志消息
         */
        log(message) {
            if (this.debugMode) {
                console.log(`[CDN] ${message}`);  // 与cdn.js保持一致的日志前缀
            }
        }

        /**
         * 注册用户脚本菜单命令
         */
        registerMenuCommands() {
            // CDN优化开关
            GM_registerMenuCommand(`${this.enableCDNOptimize ? '✅' : '❌'} CDN优化`, () => {
                this.enableCDNOptimize = !this.enableCDNOptimize;
                GM_setValue('enableCDNOptimize', this.enableCDNOptimize);
                this.log(`CDN优化已${this.enableCDNOptimize ? '启用' : '禁用'}`);
                location.reload();
            });

            // MCDN代理开关
            GM_registerMenuCommand(`${this.enableMCDNProxy ? '✅' : '❌'} MCDN代理`, () => {
                this.enableMCDNProxy = !this.enableMCDNProxy;
                GM_setValue('enableMCDNProxy', this.enableMCDNProxy);
                this.log(`MCDN代理已${this.enableMCDNProxy ? '启用' : '禁用'}`);
                location.reload();
            });

            // CDN选择菜单
            GM_registerMenuCommand(`🔄 当前CDN: ${this.preferredCDN}`, () => {
                const cdnKeys = Object.keys(this.cdnList);
                const currentIndex = cdnKeys.indexOf(this.preferredCDN);
                const nextIndex = (currentIndex + 1) % cdnKeys.length;
                this.preferredCDN = cdnKeys[nextIndex];
                GM_setValue('preferredCDN', this.preferredCDN);
                this.log(`已切换CDN为: ${this.preferredCDN}`);
                alert(`已切换CDN为: ${this.preferredCDN} (${this.cdnList[this.preferredCDN]})`);
            });

            // 画质固定开关
            GM_registerMenuCommand(`${this.enableQualityFix ? '✅' : '❌'} 画质固定`, () => {
                this.enableQualityFix = !this.enableQualityFix;
                GM_setValue('enableQualityFix', this.enableQualityFix);
                this.log(`画质固定已${this.enableQualityFix ? '启用' : '禁用'}`);
                if (this.enableQualityFix) {
                    this.setQualityCookie();
                }
                location.reload();
            });

            // 画质选择菜单
            GM_registerMenuCommand(`🎞️ 固定画质: ${this.qualityOptions[this.fixedQuality]}`, () => {
                this.selectQuality();
            });

            // 调试模式开关
            GM_registerMenuCommand(`${this.debugMode ? '✅' : '❌'} 调试模式`, () => {
                this.debugMode = !this.debugMode;
                GM_setValue('debugMode', this.debugMode);
                this.log(`调试模式已${this.debugMode ? '启用' : '禁用'}`);
            });
        }

        /**
         * 初始化网络请求拦截器
         */
        initInterceptors() {
            // 拦截XMLHttpRequest
            this.interceptXHR();

            // 拦截Fetch
            this.interceptFetch();

            // 监听DOM变化处理视频元素
            this.observeDOM();

            // 拦截媒体源扩展(MSE)
            this.interceptMediaSource();
        }

        /**
         * 拦截XMLHttpRequest请求
         */
        interceptXHR() {
            const self = this;
            const originalXHROpen = XMLHttpRequest.prototype.open;
            const originalXHRSend = XMLHttpRequest.prototype.send;

            XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
                // 保存原始URL以便在send中使用
                this._originalUrl = url;

                const optimizedUrl = self.optimizeUrl(url);
                if (optimizedUrl && optimizedUrl !== url) {
                    self.log(`XHR请求已优化: ${url} -> ${optimizedUrl}`);
                    originalXHROpen.call(this, method, optimizedUrl, async !== false, user, password);
                } else {
                    originalXHROpen.call(this, method, url, async !== false, user, password);
                }
            };

            // 拦截响应以提取备用URL
            XMLHttpRequest.prototype.send = function(body) {
                const xhr = this;
                const originalUrl = xhr._originalUrl;

                // 检查是否是视频信息API
                if (originalUrl && (
                    originalUrl.includes('/x/player/playurl') ||
                    originalUrl.includes('/x/player/wbi/playurl') ||
                    originalUrl.includes('/pgc/player/web/playurl')
                )) {
                    // 添加响应监听器
                    xhr.addEventListener('load', function() {
                        try {
                            if (xhr.responseType === 'json' || xhr.responseType === '') {
                                const response = xhr.responseType === 'json' ? xhr.response : JSON.parse(xhr.responseText);
                                self.extractBackupUrls(response);
                            }
                        } catch (e) {
                            self.log(`提取备用URL错误: ${e.message}`);
                        }
                    });
                }

                originalXHRSend.call(this, body);
            };
        }

        /**
         * 拦截Fetch请求
         */
        interceptFetch() {
            const self = this;
            const originalFetch = window.fetch;

            window.fetch = function(resource, init) {
                let url = '';
                let originalRequest = null;

                if (typeof resource === 'string') {
                    url = resource;
                    const optimizedUrl = self.optimizeUrl(url);
                    if (optimizedUrl && optimizedUrl !== url) {
                        self.log(`Fetch请求已优化: ${url} -> ${optimizedUrl}`);
                        resource = optimizedUrl;
                    }
                } else if (resource instanceof Request) {
                    url = resource.url;
                    originalRequest = resource.clone();
                    const optimizedUrl = self.optimizeUrl(url);
                    if (optimizedUrl && optimizedUrl !== url) {
                        self.log(`Fetch Request已优化: ${url} -> ${optimizedUrl}`);
                        resource = new Request(optimizedUrl, resource);
                    }
                }

                // 判断是否是视频信息API
                const isVideoApi = url && (
                    url.includes('/x/player/playurl') ||
                    url.includes('/x/player/wbi/playurl') ||
                    url.includes('/pgc/player/web/playurl')
                );

                // 执行原始fetch请求
                return originalFetch.call(window, resource, init).then(response => {
                    // 如果是视频API,提取备用URL
                    if (isVideoApi) {
                        response.clone().json().then(data => {
                            self.extractBackupUrls(data);
                        }).catch(err => {
                            self.log(`提取备用URL错误: ${err.message}`);
                        });
                    }
                    return response;
                });
            };
        }

        /**
         * 拦截MediaSource
         */
        interceptMediaSource() {
            if (window.MediaSource) {
                const self = this;
                const originalAddSourceBuffer = MediaSource.prototype.addSourceBuffer;

                MediaSource.prototype.addSourceBuffer = function(mimeType) {
                    self.log(`MediaSource添加缓冲区: ${mimeType}`);
                    const sourceBuffer = originalAddSourceBuffer.call(this, mimeType);
                    return sourceBuffer;
                };
            }
        }

        /**
         * 从API响应中提取备用URL
         * @param {Object} data - API响应数据
         */
        extractBackupUrls(data) {
            try {
                // 处理不同API格式的响应
                if (data && data.data) {
                    const responseData = data.data;

                    // 处理备用URL
                    if (responseData.durl && Array.isArray(responseData.durl)) {
                        responseData.durl.forEach((item, index) => {
                            if (item.url) {
                                // 保存原始URL和备用URL的映射关系
                                if (item.backup_url && Array.isArray(item.backup_url)) {
                                    item.backup_url.forEach(backupUrl => {
                                        if (backupUrl && backupUrl.includes('http')) {
                                            this.backupUrls.set(item.url, backupUrl);
                                            this.log(`提取到备用URL: ${backupUrl} (对应 ${item.url})`);
                                        }
                                    });
                                }
                            }
                        });
                    }

                    // 处理DASH格式
                    if (responseData.dash) {
                        // 处理视频流
                        if (responseData.dash.video && Array.isArray(responseData.dash.video)) {
                            responseData.dash.video.forEach(video => {
                                if (video.base_url && video.backup_url && Array.isArray(video.backup_url)) {
                                    video.backup_url.forEach(backupUrl => {
                                        if (backupUrl && backupUrl.includes('http')) {
                                            this.backupUrls.set(video.base_url, backupUrl);
                                            this.log(`提取到视频备用URL: ${backupUrl} (对应 ${video.base_url})`);
                                        }
                                    });
                                }
                            });
                        }

                        // 处理音频流
                        if (responseData.dash.audio && Array.isArray(responseData.dash.audio)) {
                            responseData.dash.audio.forEach(audio => {
                                if (audio.base_url && audio.backup_url && Array.isArray(audio.backup_url)) {
                                    audio.backup_url.forEach(backupUrl => {
                                        if (backupUrl && backupUrl.includes('http')) {
                                            this.backupUrls.set(audio.base_url, backupUrl);
                                            this.log(`提取到音频备用URL: ${backupUrl} (对应 ${audio.base_url})`);
                                        }
                                    });
                                }
                            });
                        }
                    }
                }
            } catch (e) {
                this.log(`解析备用URL错误: ${e.message}`);
            }
        }

        /**
         * 观察DOM变化以处理视频元素
         */
        observeDOM() {
            const self = this;

            // 页面加载完成后开始观察DOM变化
            window.addEventListener('DOMContentLoaded', () => {
                self.processVideoElements();

                const observer = new MutationObserver(() => {
                    self.processVideoElements();
                });

                observer.observe(document.body, { childList: true, subtree: true });
                self.log('DOM观察器已激活');
            });
        }

        /**
         * 处理页面中的视频元素
         */
        processVideoElements() {
            const self = this;
            const videoElements = document.querySelectorAll('video');

            videoElements.forEach(video => {
                // 处理video.src
                if (video.src) {
                    const backupUrl = self.getBackupUrl(video.src);
                    const targetUrl = backupUrl || video.src;
                    const optimizedUrl = self.optimizeVideoUrl(targetUrl, backupUrl);

                    if (optimizedUrl && optimizedUrl !== video.src) {
                        self.log(`视频元素src已优化: ${video.src} -> ${optimizedUrl}`);
                        video.src = optimizedUrl;
                    }
                }

                // 处理source元素
                const sourceElements = video.querySelectorAll('source');
                sourceElements.forEach(source => {
                    if (source.src) {
                        const backupUrl = self.getBackupUrl(source.src);
                        const targetUrl = backupUrl || source.src;
                        const optimizedUrl = self.optimizeVideoUrl(targetUrl, backupUrl);

                        if (optimizedUrl && optimizedUrl !== source.src) {
                            self.log(`视频source元素已优化: ${source.src} -> ${optimizedUrl}`);
                            source.src = optimizedUrl;
                        }
                    }
                });
            });
        }

        /**
         * 获取URL对应的备用URL
         * @param {string} url - 原始URL
         * @returns {string|null} 备用URL或null
         */
        getBackupUrl(url) {
            // 先检查完全匹配
            if (this.backupUrls.has(url)) {
                return this.backupUrls.get(url);
            }

            // 检查部分匹配 (处理URL参数可能不同的情况)
            for (const [originalUrl, backupUrl] of this.backupUrls.entries()) {
                // 提取URL的基本部分(不含参数)
                const baseOriginalUrl = originalUrl.split('?')[0];
                const baseInputUrl = url.split('?')[0];

                if (baseInputUrl === baseOriginalUrl) {
                    this.log(`找到部分匹配的备用URL: ${backupUrl} (对应 ${url})`);
                    return backupUrl;
                }
            }

            return null;
        }

        /**
         * 检测是否是MCDN URL(使用与cdn.js相同的检测逻辑)
         * @param {string} url - 要检查的URL
         * @returns {boolean} 是否是MCDN URL
         */
        isMCDNUrl(url) {
            return url && (
                url.includes('.mcdn.bilivideo') ||
                url.includes('.mcdn.bilivideo.cn') ||
                url.includes('.mcdn.bilivideo.com')
            );
        }

        /**
         * 优化视频URL,综合处理备用URL、MCDN代理和CDN优选
         * @param {string} originalUrl - 原始URL
         * @param {string} backupUrl - 备用URL
         * @returns {string} 优化后的URL
         */
        optimizeVideoUrl(originalUrl, backupUrl = '') {
            this.log(`原始URL: ${originalUrl}`);
            this.log(`备用URL: ${backupUrl}`);

            // 检查CDN优化是否启用
            const enableCdn = this.enableCDNOptimize;
            if (!enableCdn && !this.enableMCDNProxy) {
                this.log('CDN优化和MCDN代理都已禁用,使用原始URL');
                return originalUrl;
            }

            // 优先使用backupUrl,通常是upgcxcode地址,播放更稳定
            let videoUrl = '';
            if (backupUrl && backupUrl.includes('http')) {
                videoUrl = backupUrl;
                this.log('使用备用URL');
            } else {
                videoUrl = originalUrl;
                this.log('使用原始URL');
            }

            // 处理mcdn域名的特殊情况 - 使用与cdn.js相同的检测逻辑
            if (this.enableMCDNProxy && this.isMCDNUrl(videoUrl)) {
                this.log(`检测到mcdn域名: ${videoUrl}`);
                const proxyUrl = this.proxyMCDN(videoUrl);
                this.log(`使用代理: ${proxyUrl}`);
                return proxyUrl;
            }

            // 处理upgcxcode路径,替换为优选CDN
            if (enableCdn && this.upgcxcodeRegex.test(videoUrl)) {
                this.log(`检测到upgcxcode路径,替换CDN`);

                // 从GM_getValue获取用户选择的CDN(与原始CDN.js行为保持一致)
                const preferredCdn = this.preferredCDN;
                // 获取对应的CDN主机名
                const cdn = this.cdnList[preferredCdn] || this.cdnList['ali'];

                // 使用正则表达式替换域名部分
                const replacedUrl = videoUrl.replace(this.upgcxcodeRegex, `https://${cdn}/upgcxcode/`);

                this.log(`替换CDN: ${preferredCdn} -> ${cdn}`);
                return replacedUrl;
            }

            this.log('无需优化,返回原始URL');
            return videoUrl;
        }

        /**
         * 优化普通URL,用于XHR/Fetch拦截
         * @param {string} url - 原始URL
         * @returns {string} 优化后的URL
         */
        optimizeUrl(url) {
            if (!url) return url;

            try {
                // 获取可能的备用URL
                const backupUrl = this.getBackupUrl(url);

                // 使用完整的优化逻辑
                return this.optimizeVideoUrl(url, backupUrl);
            } catch (error) {
                this.log(`URL优化错误: ${error.message}`);
                return url;
            }
        }

        /**
         * 使用代理服务器代理MCDN请求
         * @param {string} url - MCDN URL
         * @returns {string} 代理后的URL
         */
        proxyMCDN(url) {
            const proxyUrl = `https://proxy-tf-all-ws.bilivideo.com/?url=${encodeURIComponent(url)}`;
            this.log(`MCDN代理URL: ${proxyUrl}`);
            return proxyUrl;
        }

        /**
         * 替换CDN为优选节点
         * @param {string} url - 包含upgcxcode的URL
         * @returns {string} 替换CDN后的URL
         */
        replaceCDN(url) {
            const cdn = this.cdnList[this.preferredCDN] || this.cdnList['ali'];
            const replacedUrl = url.replace(this.upgcxcodeRegex, `https://${cdn}/upgcxcode/`);
            this.log(`替换CDN: ${this.preferredCDN} (${cdn})`);
            return replacedUrl;
        }

        /**
         * 弹出画质选择对话框
         */
        selectQuality() {
            const qualityEntries = Object.entries(this.qualityOptions);
            let optionsText = "请选择要固定的视频画质:\n(可以输入索引或质量值)\n(如果设置为自己的会员等级无法观看的画质或者不支持的画质,会自动向下兼容)\n\n";

            qualityEntries.forEach(([value, label], index) => {
                optionsText += `${index + 1}. ${label} (${value})\n`;
            });

            const userInput = prompt(optionsText, this.fixedQuality);

            if (userInput !== null) {
                // 检查用户输入是否是数字索引或质量值
                const numInput = parseInt(userInput);

                if (!isNaN(numInput)) {
                    // 检查是否是索引值
                    if (numInput >= 1 && numInput <= qualityEntries.length) {
                        // 用户输入了选项的序号
                        this.fixedQuality = qualityEntries[numInput - 1][0];
                    } else if (Object.keys(this.qualityOptions).includes(userInput)) {
                        // 用户直接输入了质量值
                        this.fixedQuality = userInput;
                    } else {
                        alert("无效的画质选择,请重新选择");
                        return;
                    }

                    GM_setValue('fixedQuality', this.fixedQuality);
                    this.log(`已设置固定画质为: ${this.qualityOptions[this.fixedQuality]}`);
                    alert(`已设置固定画质为: ${this.qualityOptions[this.fixedQuality]}`);

                    if (this.enableQualityFix) {
                        this.setQualityCookie();
                        location.reload();
                    }
                } else {
                    alert("请输入有效的数字");
                }
            }
        }

        /**
         * 设置画质cookie
         */
        setQualityCookie() {
            const cookieName = "CURRENT_QUALITY";
            const cookieValue = this.fixedQuality;
            const expiryDate = new Date();
            expiryDate.setFullYear(expiryDate.getFullYear() + 1); // 设置一年有效期

            document.cookie = `${cookieName}=${cookieValue}; domain=.bilibili.com; path=/; expires=${expiryDate.toUTCString()}`;
            this.log(`已设置画质Cookie: ${cookieName}=${cookieValue}`);
        }

        /**
         * 获取当前画质设置信息
         * @returns {Object} 画质设置信息
         */
        getQualityFixInfo() {
            return {
                enabled: this.enableQualityFix,
                quality: this.fixedQuality,
                qualityName: this.qualityOptions[this.fixedQuality] || '未知画质'
            };
        }

        /**
         * 获取当前CDN信息
         * @returns {Object} CDN信息
         */
        getCDNInfo() {
            return {
                key: this.preferredCDN,
                host: this.cdnList[this.preferredCDN],
                enabled: this.enableCDNOptimize
            };
        }

        /**
         * 获取MCDN代理信息
         * @returns {Object} MCDN代理信息
         */
        getMCDNProxyInfo() {
            return {
                enabled: this.enableMCDNProxy,
                proxy: 'proxy-tf-all-ws.bilivideo.com'
            };
        }

        /**
         * 获取备用URL统计信息
         * @returns {Object} 备用URL统计
         */
        getBackupUrlStats() {
            return {
                count: this.backupUrls.size,
                urls: Array.from(this.backupUrls.entries()).slice(0, 5) // 仅返回前5个示例
            };
        }
    }

    // 创建并初始化B站CDN优化器
    const bilibiliCDNOptimizer = new BilibiliCDNOptimizer();

    // 将优化器实例暴露到全局,方便调试
    window.bilibiliCDNOptimizer = bilibiliCDNOptimizer;
})();