您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
拯救B站直播换牌子的用户体验
当前为
// ==UserScript== // @name blivemedal // @namespace http://tampermonkey.net/ // @version 0.9 // @description 拯救B站直播换牌子的用户体验 // @author xfgryujk // @include /https?:\/\/live\.bilibili\.com\/?\??.*/ // @include /https?:\/\/live\.bilibili\.com\/\d+\??.*/ // @include /https?:\/\/live\.bilibili\.com\/(blanc\/)?\d+\??.*/ // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/vuex.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js // @grant none // ==/UserScript== (function () { function main() { initLib() initCss() waitForLoaded(() => { Vue.use(Vuex) initUi() }) } function initLib() { let scriptElement = document.createElement('script') scriptElement.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js' document.head.appendChild(scriptElement) let linkElement = document.createElement('link') linkElement.rel = 'stylesheet' linkElement.href = 'https://cdn.jsdelivr.net/npm/[email protected]/lib/theme-chalk/index.css' document.head.appendChild(linkElement) scriptElement = document.createElement('script') scriptElement.src = 'https://cdn.jsdelivr.net/npm/[email protected]/lib/index.js' document.head.appendChild(scriptElement) } function initCss() { let css = ` /* 屏蔽原来的牌子按钮 */ .medal-section { display: none !important; } /* 屏蔽选牌子对话框,防止刷新时闪烁 */ .dialog-ctnr.medal { display: none !important; } ` let styleElement = document.createElement('style') styleElement.innerText = css document.head.appendChild(styleElement) } function waitForLoaded(callback, timeout = 10 * 1000) { let startTime = new Date() function poll() { if (isLoaded()) { callback() return } if (new Date() - startTime > timeout) { return } setTimeout(poll, 1000) } poll() } function isLoaded() { if (window.ELEMENT === undefined) { return false } if (document.querySelector('#control-panel-ctnr-box') === null) { return false } return true } let store = null function getStore() { if (store === null) { store = new Vuex.Store({ state: { config: loadConfig(), medals: [], curMedal: null }, mutations: { setMedals(state, medals) { state.medals = medals }, setCurMedal(state, curMedal) { state.curMedal = curMedal }, setConfigItems(state, config) { for (let name in config) { state.config[name] = config[name] } saveConfig(state.config) } }, actions: { async updateMedals({ commit }) { commit('setMedals', getMedalsAsync()) }, async updateCurMedal({ commit }) { commit('setCurMedal', await getCurMedal()) } } }) } return store } function loadConfig() { let config try { config = JSON.parse(window.localStorage.blivemedalConfig || '{}') } catch { config = {} } if (config.autoWearMedal === undefined) { config.autoWearMedal = false } if (config.autoWearDefaultMedal === undefined) { config.autoWearDefaultMedal = false } if (config.defaultMedalId === undefined) { config.defaultMedalId = '' } return config } function saveConfig(config) { window.localStorage.blivemedalConfig = JSON.stringify(config) } function initUi() { let panelElement = document.querySelector('#control-panel-ctnr-box') let myMedalButtonElement = document.createElement('div') panelElement.appendChild(myMedalButtonElement) new Vue({ el: myMedalButtonElement, store: getStore(), components: { MedalDialog }, template: ` <div> <el-button type="primary" style="font-size: 12px; min-width: 80px; height: 24px; padding: 6px 12px;" @click="showMedalDialog" > {{ curMedal === null ? '勋章' : curMedal.medal_name }} </el-button> <medal-dialog ref="medalDialog"></medal-dialog> </div> `, computed: { ...Vuex.mapState({ config: state => state.config, curMedal: state => state.curMedal }) }, async created() { await this.tryAutoWearMedal() this.updateCurMedal() }, methods: { ...Vuex.mapActions([ 'updateCurMedal' ]), async tryAutoWearMedal() { if (!this.config.autoWearMedal) { return } try { let medalInfo = window.__NEPTUNE_IS_MY_WAIFU__.roomInfoRes.data.anchor_info.medal_info if (medalInfo !== null) { await wearMedal(medalInfo.medal_id) return } } catch { } try { if (this.config.autoWearDefaultMedal && this.config.defaultMedalId !== '') { await sleep(1000) await wearMedal(this.config.defaultMedalId) } } catch { } }, showMedalDialog() { this.$refs.medalDialog.showDialog() } } }) } let MedalDialog = { name: 'MedalDialog', template: ` <el-dialog :visible.sync="dialogVisible" title="我的粉丝勋章" top="60px" width="850px" :modal="false" append-to-body> <div style="line-height: 40px"> <el-checkbox label="进入直播间时自动佩戴勋章" :value="config.autoWearMedal" @change="value => setConfigItems({ autoWearMedal: value })" ></el-checkbox> <el-checkbox v-show="config.autoWearMedal" label="没有对应勋章时佩戴" :value="config.autoWearDefaultMedal" @change="value => setConfigItems({ autoWearDefaultMedal: value })" ></el-checkbox> <el-select v-show="config.autoWearMedal" style="margin-left: 16px; width: 240px" filterable :value="config.defaultMedalId" @change="value => setConfigItems({ defaultMedalId: value })" > <el-option v-for="item in sortedMedals" :key="item.medal_id" :label="item.target_name + ' / ' + item.medal_name" :value="item.medal_id" > <span>{{ item.target_name }}</span> <span style="float: right; color: #8492a6; font-size: 13px">{{ item.medal_name }}</span> </el-option> </el-select> </div> <div> <el-button icon="el-icon-refresh" @click="refreshMedals">刷新勋章</el-button> <el-input type="primary" v-model="query" placeholder="搜索" clearable style="margin-left: 70px; width: 180px"></el-input> </div> <el-table :data="medalsTableData" stripe height="80vh"> <el-table-column label="勋章" prop="medal_name" width="100" sortable :sort-method="(a, b) => a.medal_name.localeCompare(b.medal_name)" > <template slot-scope="scope"> <el-tag :type="scope.row.is_lighted ? '' : 'info'">{{ scope.row.medal_name }}</el-tag> </template> </el-table-column> <el-table-column label="等级" prop="level" width="80" sortable></el-table-column> <el-table-column label="主播昵称" prop="target_name" width="200" sortable :sort-method="(a, b) => a.target_name.localeCompare(b.target_name)" > <template slot-scope="scope"> <el-link type="primary" :underline="false" target="_blank" :href="'https://live.bilibili.com/' + scope.row.roomid"> {{ scope.row.target_name }} </el-link> </template> </el-table-column> <el-table-column label="亲密度/原力值" prop="intimacy" width="140" sortable> <template slot-scope="scope"> {{ scope.row.intimacy }} / {{ scope.row.next_intimacy }} </template> </el-table-column> <el-table-column label="本日亲密度/原力值" prop="today_intimacy" width="160" sortable> <template slot-scope="scope"> {{ scope.row.today_feed }} / {{ scope.row.day_limit }} </template> </el-table-column> <el-table-column label="操作" width="120"> <template slot-scope="scope"> <el-button v-if="curMedal !== null && scope.row.medal_id === curMedal.medal_id" type="info" size="mini" @click="takeOffMedal" >取消佩戴</el-button> <el-button v-else type="primary" size="mini" @click="wearMedal(scope.row)">佩戴</el-button> </template> </el-table-column> </el-table> </el-dialog> `, data() { return { dialogVisible: false, query: '' } }, computed: { ...Vuex.mapState({ config: state => state.config, medals: state => state.medals, curMedal: state => state.curMedal }), medalsTableData() { if (this.query === '') { return this.sortedMedals } let query = this.query.toLowerCase() let res = [] for (let medal of this.sortedMedals) { if (medal.medal_name.toLowerCase().indexOf(query) !== -1 || medal.target_name.toLowerCase().indexOf(query) !== -1 ) { res.push(medal) } } return res }, sortedMedals() { let curRoomId try { curRoomId = window.__NEPTUNE_IS_MY_WAIFU__.roomInfoRes.data.room_info.room_id } catch { curRoomId = 0 } let curMedal = [] let curRoomMedal = [] let medals = [] for (let medal of this.medals) { if (this.curMedal !== null && medal.medal_id === this.curMedal.medal_id) { curMedal.push(medal) } else if (medal.roomid === curRoomId) { curRoomMedal.push(medal) } else { medals.push(medal) } } // 剩下的按上次佩戴时间降序排序 medals.sort((a, b) => b.last_wear_time - a.last_wear_time) return [...curMedal, ...curRoomMedal, ...medals] } }, methods: { ...Vuex.mapMutations([ 'setConfigItems' ]), ...Vuex.mapActions({ doUpdateMedals: 'updateMedals', doUpdateCurMedal: 'updateCurMedal' }), showDialog() { // 只自动加载一次 if (this.medals.length === 0) { this.updateMedals() } this.updateCurMedal() this.dialogVisible = true }, refreshMedals() { this.updateMedals() this.updateCurMedal() refreshBilibiliCurMedalCache() }, async updateMedals() { try { await this.doUpdateMedals() } catch (e) { this.$message.error(e) } }, async updateCurMedal() { try { await this.doUpdateCurMedal() } catch (e) { this.$message.error(e) } }, async wearMedal(medal) { try { await wearMedal(medal.medal_id) } catch (e) { this.$message.error(e) return } this.updateCurMedal() }, async takeOffMedal() { try { await takeOffMedal() } catch (e) { this.$message.error(e) return } this.updateCurMedal() } } } let apiClient = axios.create({ baseURL: 'https://api.live.bilibili.com', withCredentials: true }) function getMedalsAsync() { let res = [] async function doGetMedalsAsync() { // 获取第一页和总页数 let rsp try { rsp = await getPage(1) } catch (e) { console.error('获取勋章列表第 1 页失败:', e) return } for (let medal of rsp.items) { res.push(medal) } // 并发获取剩下的页 if (rsp.page_info.total_page <= 1) { return } let pageQueue = [] for (let page = 2; page <= rsp.page_info.total_page; page++) { pageQueue.push(page) } const WORKER_NUM = 8 let workerPromises = [] for (let i = 0; i < WORKER_NUM; i++) { workerPromises.push(worker(pageQueue)) } await Promise.all(workerPromises) } async function worker(pageQueue) { while (true) { let page = pageQueue.shift() if (page === undefined) { break } let rsp try { rsp = await getPage(page) } catch (e) { console.error(`获取勋章列表第 ${page} 页失败:`, e) continue } for (let medal of rsp.items) { res.push(medal) } } } async function getPage(page) { let rsp = (await apiClient.get('/xlive/app-ucenter/v1/user/GetMyMedals', { params: { page_size: 10, page: page } })).data if (rsp.code !== 0) { throw rsp.message } return rsp.data } doGetMedalsAsync() return res } async function getCurMedal() { let rsp = (await apiClient.get('/live_user/v1/UserInfo/get_weared_medal')).data if (rsp.code !== 0) { throw rsp.message } let curMedal = rsp.data if (curMedal.medal_id === undefined) { // 没佩戴牌子 curMedal = null } return curMedal } async function wearMedal(medalId) { let csrfToken = getCsrfToken() let data = new FormData() data.append('medal_id', medalId) data.append('csrf_token', csrfToken) data.append('csrf', csrfToken) let rsp = (await apiClient.post('/xlive/web-room/v1/fansMedal/wear', data)).data if (rsp.code !== 0) { throw rsp.message } refreshBilibiliCurMedalCache() } async function takeOffMedal() { let csrfToken = getCsrfToken() let data = new FormData() data.append('csrf_token', csrfToken) data.append('csrf', csrfToken) let rsp = (await apiClient.post('/xlive/web-room/v1/fansMedal/take_off', data)).data if (rsp.code !== 0) { throw rsp.message } refreshBilibiliCurMedalCache() } function getCsrfToken() { let match = document.cookie.match(/\bbili_jct=(.+?)(?:;|$)/) if (match === null) { return '' } return match[1] } function refreshBilibiliCurMedalCache() { let originalMedalButton = document.querySelector('.medal-section .fans-medal-item') if (originalMedalButton === null) { return } originalMedalButton.click() setTimeout(() => originalMedalButton.click(), 0) } async function sleep(time) { return new Promise(resolve => window.setTimeout(resolve, time)) } main() })();