Bilibili Video CDN Switcher

修改哔哩哔哩播放时的所用CDN 加快视频加载 番剧加速 视频加速

目前为 2024-07-10 提交的版本。查看 最新版本

// ==UserScript==
// @name         Bilibili Video CDN Switcher
// @name:zh-CN   Bilibili CDN切换
// @name:zh-TW   Bilibili CDN切換
// @name:ja      BilibiliビデオCDNスイッチャー
// @name:en      Bilibili Video CDN Switcher
// @namespace    mailto:[email protected]
// @copyright    Free For Personal Use
// @license      No License
// @version      0.0.2
// @description       修改哔哩哔哩播放时的所用CDN 加快视频加载 番剧加速 视频加速
// @description:zh-CN 修改哔哩哔哩播放时的所用CDN 加快视频加载 番剧加速 视频加速
// @description:en    Modify Bilibili's CDN during playback to speed up video loading, supporting Animes & Videos
// @description:zh-TW 修改 Bilibili 播放時的所用CDN 加快影片載入 番劇加速 影片加速
// @description:ja    ビリビリ動画(Bilibili)の動画再生時のCDNを変更して、動画読み込み速度の向上、アニメとビデオ読込高速化
// @author       [email protected]
// @run-at       document-start
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/bangumi/play/*
// @icon         https://i0.hdslb.com/bfs/static/jinkela/long/images/512.png
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// ==/UserScript==

// 在这里的引号内输入自定义的CDN网址
var CustomCDN = ''
// 例如将下一行修改为如下,可以将CDN强制设置为 'xy42x101x76x8xy.mcdn.bilivideo.cn:8082'
// var CustomCDN = 'xy42x101x76x8xy.mcdn.bilivideo.cn:8082'


const Language = (() => {
    const lang = (navigator.language || navigator.browserLanguage || (navigator.languages || ["en"])[0]).substring(0, 2)
    return (lang === 'zh' || lang === 'ja') ? lang : 'en'
})()

const Replacement = (() => {
    const toURL = ((url) => { if (url.indexOf('://') === -1) url = 'https://' + url; return url.endsWith('/') ? url : `${url}/` })
    return toURL(
        (CustomCDN !== undefined && CustomCDN !== '') ? CustomCDN : {
            'zh': 'cn-jxnc-cmcc-bcache-06.bilivideo.com',
            'en': 'upos-hz-mirrorakam.akamaized.net',
            'ja': 'upos-sz-mirroraliov.bilivideo.com'
        }[Language]
    )
})()
const SettingsBarTitle = {
    'zh': '拦截修改视频CDN',
    'en': 'CDN Switcher',
    'ja': 'CDNスイッチャー'
}[Language]

const PluginName = 'BiliCDNSwitcher'
let disabled = !!GM_getValue('disabled')

const urlTransformer = i => {
    const newUrl = i.base_url.replace(
        /https:\/\/.*?\//,
        Replacement
    ); i.baseUrl = newUrl; i.base_url = newUrl; return i
}
const playInfoTransformer = playInfo => {
    if (playInfo.result) { // bangumi pages'
        playInfo.result.video_info.dash.video.forEach(urlTransformer)
        playInfo.result.video_info.dash.audio.forEach(urlTransformer)
    } else { // video pages'
        playInfo.data.dash.video.forEach(urlTransformer)
        playInfo.data.dash.audio.forEach(urlTransformer)
    }
    return playInfo
}

// Network Request Interceptor
const interceptXhrResponse = (() => {
    const interceptors = []
    const interceptXhrResponse = (handler) => interceptors.push(handler)
    const handleInterceptedResponse = (response, url) => interceptors.reduce((modified, handler) => {
        const ret = handler(modified, url)
        return ret ? ret : modified
    }, response)
    const OriginalXMLHttpRequest = unsafeWindow.XMLHttpRequest

    class XMLHttpRequest extends OriginalXMLHttpRequest {
        get responseText() {
            if (this.readyState !== this.DONE) return super.responseText
            return handleInterceptedResponse(super.responseText, this.responseURL)
        }
        get response() {
            if (this.readyState !== this.DONE) return super.response
            return handleInterceptedResponse(super.response, this.responseURL)
        }
    }

    unsafeWindow.XMLHttpRequest = XMLHttpRequest
    return interceptXhrResponse
})()

const waitForElm = (selector) => new Promise(resolve => {
    let ele = document.querySelector(selector)
    if (ele) return resolve(ele)

    const observer = new MutationObserver(mutations => {
        let ele = document.querySelector(selector)
        if (ele) {
            observer.disconnect()
            resolve(ele)
        }
    })

    observer.observe(document.documentElement, {
        childList: true,
        subtree: true
    })
})

// Parse HTML string to DOM Element
function fromHTML(html, trim = false) {
    html = trim ? html.trim() : html
    if (!html) return null
    const template = document.createElement('template')
    template.innerHTML = html
    const result = template.content.children
    if (result.length === 1) return result[0]
    return result
}

const log = (str, ...args) => console.log(`[${PluginName}]: ${str}`, ...args);

(function () {
    'use strict';

    // Hook Bilibili PlayUrl Api
    interceptXhrResponse((response, url) => {
        if (disabled) return
        if (url.startsWith('https://api.bilibili.com/x/player/wbi/playurl') ||
            url.startsWith('https://api.bilibili.com/pgc/player/web/v2/playurl')
        ) {
            log('Intercepted playurl api response.')
            const responseText = response
            const playInfo = JSON.parse(responseText)
            const newPlayInfo = playInfoTransformer(playInfo)
            return JSON.stringify(newPlayInfo)
        }
    });

    // Modify unsafeWindow.__playinfo__
    if (disabled) {
        log('Plugin is Disabled')
        return
    }
    if (unsafeWindow.__playinfo__) {
        log('Directly modify the window.__playinfo__')
        playInfoTransformer(unsafeWindow.__playinfo__)
    } else {
        log('Hook the window.__playinfo__')
        let internalPlayInfo = undefined
        Object.defineProperty(unsafeWindow, '__playinfo__', {
            get: () => internalPlayInfo,
            set: v => {
                if (internalPlayInfo) throw Error('__playinfo__ is already set', v)
                internalPlayInfo = playInfoTransformer(v)
            }
        })
    }

    // Add setting checkbox
    waitForElm('#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-video-area > div.bpx-player-control-wrap > div.bpx-player-control-entity > div.bpx-player-control-bottom > div.bpx-player-control-bottom-right > div.bpx-player-ctrl-btn.bpx-player-ctrl-setting > div.bpx-player-ctrl-setting-box > div > div > div > div > div > div > div.bpx-player-ctrl-setting-others')
        .then(settingsBar => {
            settingsBar.appendChild(fromHTML(`<div class="bpx-player-ctrl-setting-others-title">${SettingsBarTitle}</div>`))
            const checkBoxWrapper = fromHTML(`<div class="bpx-player-ctrl-setting-checkbox bpx-player-ctrl-setting-blackgap bui bui-checkbox bui-dark"><div class="bui-area"><input class="bui-checkbox-input" type="checkbox" checked="" aria-label="自定义视频CDN">
    <label class="bui-checkbox-label">
        <span class="bui-checkbox-icon bui-checkbox-icon-default"><svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 32 32"><path d="M8 6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H8zm0-2h16c2.21 0 4 1.79 4 4v16c0 2.21-1.79 4-4 4H8c-2.21 0-4-1.79-4-4V8c0-2.21 1.79-4 4-4z"></path></svg></span>
        <span class="bui-checkbox-icon bui-checkbox-icon-selected"><svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 32 32"><path d="m13 18.25-1.8-1.8c-.6-.6-1.65-.6-2.25 0s-.6 1.5 0 2.25l2.85 2.85c.318.318.762.468 1.2.448.438.02.882-.13 1.2-.448l8.85-8.85c.6-.6.6-1.65 0-2.25s-1.65-.6-2.25 0l-7.8 7.8zM8 4h16c2.21 0 4 1.79 4 4v16c0 2.21-1.79 4-4 4H8c-2.21 0-4-1.79-4-4V8c0-2.21 1.79-4 4-4z"></path></svg></span>
        <span class="bui-checkbox-name">${SettingsBarTitle}</span>
    </label></div></div>`)
            const checkBox = checkBoxWrapper.getElementsByTagName('input')[0]
            checkBox.checked = !disabled

            checkBoxWrapper.onclick = () => {
                if (checkBox.checked) {
                    disabled = false
                    GM_setValue('disabled', false)
                    log(`已启用 ${SettingsBarTitle}`)
                } else {
                    disabled = true
                    GM_setValue('disabled', true)
                    log(`已禁用 ${SettingsBarTitle}`)
                }
            }

            settingsBar.appendChild(checkBoxWrapper)
            log('checkbox added')
        });
})();