// ==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.0 // @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/* // @icon https://i0.hdslb.com/bfs/static/jinkela/long/images/512.png // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @downloadURL none // ==/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 durlTransformer = i => (i.url = i.url.replace(/https:\/\/.*?\//, Replacement), i); const playInfoTransformer = playInfo => { if (playInfo.code !== (void 0) && playInfo.code !== 0) { log('Failed to get playInfo, message:', playInfo.message) return playInfo } 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 } 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 playInfo } } else { // video pages' video_info = playInfo.data } try { video_info.dash.video.forEach(urlTransformer) video_info.dash.audio.forEach(urlTransformer) } catch (err) { log('ERR:', err) } return playInfo } // Network Request Interceptor const interceptXhrResponse = (() => { const interceptors = [] const interceptXhrResponse = (handler) => interceptors.push(handler) // when response === undefined, 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 = 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 const OriginalFetch = fetch unsafeWindow.fetch = (input, init) => (!handleInterceptedResponse(void 0, 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 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 }) 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 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') || url.startsWith('https://api.bilibili.com/x/player/playurl') || url.startsWith('https://api.bilibili.com/pgc/player/web/playurl') ) { if (response === void 0) return true // the url is handleable 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 && 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 (internalPlayInfo) log('__playinfo__ is already set', v) internalPlayInfo = disabled ? v : playInfoTransformer(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(`