您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
修改哔哩哔哩的视频播放源 - 仅使用内置CDN列表 (修复版)
// ==UserScript== // @name Local Custom CDN of Bilibili (LCCB) - 修改哔哩哔哩的视频播放源 (本地版) // @namespace LCCB // @license MIT // @version 0.1.4 // @description 修改哔哩哔哩的视频播放源 - 仅使用内置CDN列表 (修复版) // @author 鼠鼠今天吃嘉然 & Bush2021 // @run-at document-start // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/bangumi/play/* // @match https://www.bilibili.com/festival/* // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // ==/UserScript== // 日志输出函数 const PluginName = 'LCCB' const log = console.log.bind(console, `[${PluginName}]:`) const defaultCdnNode = '使用默认源' const cdnNodeStored = 'LCCB_CDN_NODE' // 修改存储键名,避免冲突 // CDN 列表 const initCdnList = [ // 海外 'upos-sz-mirroraliov.bilivideo.com', 'upos-sz-mirrorawsov.bilivideo.com', 'upos-sz-mirrorcosov.bilivideo.com', 'upos-sz-mirrorhwov.bilivideo.com', 'upos-sz-mirrorcf01ov.bilivideo.com', // 国内 'upos-sz-mirrorali.bilivideo.com', 'upos-sz-mirroralib.bilivideo.com', 'upos-sz-mirroralio1.bilivideo.com', 'upos-sz-mirrorbd.bilivideo.com', 'upos-sz-mirrorcos.bilivideo.com', 'upos-sz-mirrorcosb.bilivideo.com', 'upos-sz-mirrorcoso1.bilivideo.com', 'upos-sz-mirrorhw.bilivideo.com', 'upos-sz-mirror08c.bilivideo.com', 'upos-sz-mirror08h.bilivideo.com', 'upos-sz-estgcos.bilivideo.com', ] const cdnList = [defaultCdnNode, ...initCdnList] const getCurCdnNode = () => { return GM_getValue(cdnNodeStored, defaultCdnNode) } const isCcbEnabled = () => { return getCurCdnNode() !== defaultCdnNode } const getReplacement = () => { const domain = getCurCdnNode() if (domain === defaultCdnNode) return null let url = domain if (url.indexOf('://') === -1) { url = 'https://' + url } return url.endsWith('/') ? url : `${url}/` } const playInfoTransformer = playInfo => { if (!isCcbEnabled()) return const replacement = getReplacement() if (!replacement) return log(`播放源已修改为: ${getCurCdnNode()}`) 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 !== undefined && playInfo.code !== 0) { log('Failed to get playInfo, message:', playInfo.message) return } let video_info if (playInfo.result) { video_info = playInfo.result.dash === undefined ? playInfo.result.video_info : playInfo.result if (!video_info?.dash) { if (playInfo.result.durl && playInfo.result.durls) { video_info = playInfo.result } else { log('Failed to get video_info, limit_play_reason:', playInfo.result.play_check?.limit_play_reason) } video_info?.durl?.forEach(durlTransformer) video_info?.durls?.forEach(durl => { durl.durl?.forEach(durlTransformer) }) return } } else { video_info = playInfo.data } try { if (video_info.dash) { video_info.dash.video.forEach(urlTransformer) video_info.dash.audio.forEach(urlTransformer) } else if (video_info.durl) { video_info.durl.forEach(durlTransformer) } else if (video_info.video_info) { video_info.video_info.dash.video.forEach(urlTransformer) video_info.video_info.dash.audio.forEach(urlTransformer) } } catch (err) { log('ERR:', err) } } // Network Request Interceptor const interceptNetResponse = (theWindow => { const interceptors = [] const interceptNetResponse = (handler) => interceptors.push(handler) 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 = (selectors) => new Promise(resolve => { const findElement = () => { const selArray = Array.isArray(selectors) ? selectors : [selectors]; for (const s of selArray) { const ele = document.querySelector(s); if (ele) return ele; } return null; }; let ele = findElement(); if (ele) return resolve(ele); const observer = new MutationObserver(mutations => { let ele = findElement(); if (ele) { observer.disconnect(); resolve(ele); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); log('waitForElm, MutationObserver started for selectors:', selectors); }) 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'; // 初始化时显示当前CDN设置 log(`当前CDN设置: ${getCurCdnNode()}`) // Hook Bilibili PlayUrl Api interceptNetResponse((response, url) => { // 每次请求时重新检查CDN设置 if (!isCcbEnabled()) 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/x/player/online') || url.startsWith('https://api.bilibili.com/x/player/wbi') || url.startsWith('https://api.bilibili.com/pgc/player/web/playurl') || url.startsWith('https://api.bilibili.com/pugv/player/web/playurl') ) { if (response === null) return true log('(Intercepted) playurl api response.') const responseText = response const playInfo = JSON.parse(responseText) playInfoTransformer(playInfo) return JSON.stringify(playInfo) } }); // 响应式 window.__playinfo__ if (unsafeWindow.__playinfo__) { playInfoTransformer(unsafeWindow.__playinfo__) } else { let internalPlayInfo = unsafeWindow.__playinfo__ Object.defineProperty(unsafeWindow, '__playinfo__', { get: () => internalPlayInfo, set: v => { if (isCcbEnabled()) playInfoTransformer(v); internalPlayInfo = v } }) } // 持续监听播放器状态变化,防止B站动态切换CDN let lastCheckTime = 0 const monitorPlayback = () => { const now = Date.now() if (now - lastCheckTime > 5000) { lastCheckTime = now if (isCcbEnabled() && unsafeWindow.__playinfo__) { playInfoTransformer(unsafeWindow.__playinfo__) } } } setInterval(monitorPlayback, 1000) // 添加组件 if (location.href.startsWith('https://www.bilibili.com/video/') || location.href.startsWith('https://www.bilibili.com/bangumi/play/') || location.href.startsWith('https://www.bilibili.com/festival/')) { 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-left', '#bilibili-player > div > 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-left' ]) .then(settingsBar => { // CDN 选择下拉列表 const cdnSelector = fromHTML(` <div class="bpx-player-ctrl-setting-checkbox" style="margin-left: 10px; display: flex; align-items: center;"> <span style="color: white; font-size: 12px; margin-right: 5px;">CDN:</span> <select class="lccb-cdn-selector" style="background: #2b2b2b; color: white; border: 1px solid #444; padding: 2px 5px; border-radius: 4px; width: 150px; height: 22px; font-size: 12px;"> ${cdnList.map(cdn => `<option value="${cdn}"${cdn === getCurCdnNode() ? ' selected' : ''}>${cdn === defaultCdnNode ? cdn : cdn.split('.')[0]}</option>`).join('')} </select> </div> `) const selectNode = cdnSelector.querySelector('select') selectNode.addEventListener('change', (e) => { const selectedCDN = e.target.value GM_setValue(cdnNodeStored, selectedCDN) log(`CDN已切换为: ${selectedCDN}`) // 延迟刷新,给用户一点反馈时间 setTimeout(() => { location.reload() }, 100) }) settingsBar.appendChild(cdnSelector) log('CDN selector added, 当前选择:', getCurCdnNode()) }); } })();