Greasy Fork

Greasy Fork is available in English.

Bilibili Video CDN Switcher

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

当前为 2024-07-11 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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.5
// @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网址,设置为null可以禁用此配置 (Enter your custom CDN URL in quotes here. Setting this to null will disable this configuration)
var CustomCDN = ''
// 例如将上一行修改为如下,可以将CDN强制设置为 'upos-sz-mirrorali.bilivideo.com' (e.g. Modify the previous line as follows to force CDN to be set to 'upos-sz-mirrorali.bilivideo.com')
// var CustomCDN = 'upos-sz-mirrorali.bilivideo.com'


const PluginName = 'BiliCDNSwitcher'
const log = (str, ...args) => console.log(`[${PluginName}]: ${str}`, ...args);
const Language = (() => {
    const lang = (navigator.language || navigator.browserLanguage || (navigator.languages || ["en"])[0]).substring(0, 2)
    return (lang === 'zh' || lang === 'ja') ? lang : 'en'
})()

let disabled = !!GM_getValue('disabled')
const Replacement = (() => {
    const toURL = ((url) => { if (url.indexOf('://') === -1) url = 'https://' + url; return url.endsWith('/') ? url : `${url}/` })
    const stored = GM_getValue('CustomCDN')
    CustomCDN = CustomCDN === 'null' ? null : CustomCDN
    let domain = ''
    if (CustomCDN && CustomCDN !== '') {
        domain = CustomCDN
        // Prevent custom CDNs from being disabled by update scripts
        if (CustomCDN !== stored) {
            GM_setValue('CustomCDN', domain)
            log('CustomCDN was saved to GM storage')
        }
    } else if (stored && CustomCDN !== null) {
        domain = stored
    } else {
        if (CustomCDN === null && stored !== null) {
            GM_setValue('CustomCDN', null)
            log('CustomCDN was deleted from GM storage')
        }
        domain = {
            'zh': 'cn-jxnc-cmcc-bcache-06.bilivideo.com',
            'en': 'upos-sz-mirroraliov.bilivideo.com',
            'ja': 'upos-sz-mirroralib.bilivideo.com'
        }[Language]
    }
    log(`CDN=${domain}`)
    return toURL(domain)
})()
const SettingsBarTitle = {
    'zh': '拦截修改视频CDN',
    'en': 'CDN Switcher',
    'ja': 'CDNスイッチャー'
}[Language]

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'
        if (!playInfo.result.video_info) {
            log('Failed to get video_info, limit_play_reason:', playInfo.result.play_check?.limit_play_reason)
            return
        }
        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
}

(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')
        });
})();