// ==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:1332019995@qq.com
// @copyright Free For Personal Use
// @license No License
// @version 0.1.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 1332019995@qq.com
// @run-at document-start
// @match https://www.bilibili.com/video/*
// @match https://www.bilibili.com/bangumi/play/*
// @match https://www.bilibili.com/blackboard/*
// @match https://live.bilibili.com/blanc/*
// @match https://www.bilibili.com/?*
// @match https://www.bilibili.com/
// @match https://www.bilibili.com/mooc/*
// @match https://www.bilibili.com/v/*
// @match https://www.bilibili.com/documentary/*
// @match https://www.bilibili.com/variety/*
// @match https://www.bilibili.com/tv/*
// @match https://www.bilibili.com/guochuang/*
// @match https://www.bilibili.com/movie/*
// @match https://www.bilibili.com/anime/*
// @match https://www.bilibili.com/match/*
// @match https://www.bilibili.com/cheese/*
// @match https://music.bilibili.com/pc/music-center/*
// @match https://search.bilibili.com/*
// @match https://m.bilibili.com/video/*
// @match https://m.bilibili.com/bangumi/play/*
// @match https://m.bilibili.com/?*
// @match https://m.bilibili.com/
// @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 = console.log.bind(console, `[${PluginName}]:`)
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 (CustomCDN === null && stored !== null) {
GM_setValue('CustomCDN', null)
log('CustomCDN was deleted from GM storage')
} else if (stored) {
domain = stored
}
// Default Servers
if (!domain) {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 playInfoTransformer = playInfo => {
const urlTransformer = i => {
const newUrl = i.base_url.replace(
/https:\/\/.*?\//,
Replacement
)
i.baseUrl = newUrl; i.base_url = newUrl
};
const durlTransformer = i => { i.url = i.url.replace(/https:\/\/.*?\//, Replacement) };
if (playInfo.code !== (void 0) && playInfo.code !== 0) {
log('Failed to get playInfo, message:', playInfo.message)
return
}
let video_info
if (playInfo.result) { // bangumi pages'
video_info = playInfo.result.dash === (void 0) ? playInfo.result.video_info : playInfo.result
if (!video_info?.dash) {
if (playInfo.result.durl && playInfo.result.durls) {
video_info = playInfo.result // documentary trail viewing, m.bilibili.com/bangumi/play/* trail or non-trail viewing
} else {
log('Failed to get video_info, limit_play_reason:', playInfo.result.play_check?.limit_play_reason)
}
// durl & durls are for trial viewing, and they usually exist when limit_play_reason=PAY
video_info?.durl?.forEach(durlTransformer)
video_info?.durls?.forEach(durl => { durl.durl?.forEach(durlTransformer) })
return
}
} else { // video pages'
video_info = playInfo.data
}
try {
video_info.dash.video.forEach(urlTransformer)
video_info.dash.audio.forEach(urlTransformer)
} catch (err) {
if (video_info.durl) { // 充电专属视频、m.bilibili.com/video/*
log('accept_description:', video_info.accept_description?.join(', '))
video_info.durl.forEach(durlTransformer)
} else {
log('ERR:', err)
}
}
return
}
// Network Request Interceptor
const interceptNetResponse = (theWindow => {
const interceptors = []
const interceptNetResponse = (handler) => interceptors.push(handler)
// when response === null && url is String, it's checking if the url is handleable
const handleInterceptedResponse = (response, url) => interceptors.reduce((modified, handler) => {
const ret = handler(modified, url)
return ret ? ret : modified
}, response)
const OriginalXMLHttpRequest = theWindow.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)
}
}
theWindow.XMLHttpRequest = XMLHttpRequest
const OriginalFetch = fetch
theWindow.fetch = (input, init) => (!handleInterceptedResponse(null, input) ? OriginalFetch(input, init) :
OriginalFetch(input, init).then(response =>
new Promise((resolve) => response.text()
.then(text => resolve(new Response(handleInterceptedResponse(text, input), {
status: response.status,
statusText: response.statusText,
headers: response.headers
})))
)
)
);
return interceptNetResponse
})(unsafeWindow)
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
})
log('waitForElm, MutationObserver started.')
})
// Parse HTML string to DOM Element
function fromHTML(html) {
if (!html) throw Error('html cannot be null or undefined', html)
const template = document.createElement('template')
template.innerHTML = html
const result = template.content.children
return result.length === 1 ? result[0] : result
}
(function () {
'use strict';
if (disabled) log('Plugin is Disabled');
// Hook Bilibili PlayUrl Api
interceptNetResponse((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') ||
url.startsWith('https://api.bilibili.com/x/player/playurl') ||
url.startsWith('https://api.bilibili.com/pgc/player/web/playurl') ||
url.startsWith('https://api.bilibili.com/pugv/player/web/playurl') // at /cheese/
) {
if (response === null) return true // the url is handleable
log('(Intercepted) playurl api response.')
const responseText = response
const playInfo = JSON.parse(responseText)
playInfoTransformer(playInfo)
return JSON.stringify(playInfo)
}
});
// Modify Pages playinfo
if (location.host === 'm.bilibili.com') {
const optionsTransformer = (opts) => (opts.readyVideoUrl = opts.readyVideoUrl?.replace(/https:\/\/.*?\//, Replacement))
if (!disabled && unsafeWindow.options) { // Modify unsafeWindow.options
log('Directly modify the window.options')
optionsTransformer(unsafeWindow.options)
} else {
let internalOptions = unsafeWindow.options
Object.defineProperty(unsafeWindow, 'options', {
get: () => internalOptions,
set: v => {
if (!disabled) optionsTransformer(v);
internalOptions = v
}
})
}
} else {
if (!disabled && unsafeWindow.__playinfo__) { // Modify unsafeWindow.__playinfo__
log('Directly modify the window.__playinfo__')
playInfoTransformer(unsafeWindow.__playinfo__)
} else {
let internalPlayInfo = unsafeWindow.__playinfo__
Object.defineProperty(unsafeWindow, '__playinfo__', {
get: () => internalPlayInfo,
set: v => {
if (!disabled) playInfoTransformer(v);
internalPlayInfo = v
}
})
}
}
// Add setting checkbox
if (location.href.startsWith('https://www.bilibili.com/video/') || location.href.startsWith('https://www.bilibili.com/bangumi/play/')) {
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, MutationObserver disconnected.')
});
}
})();