// ==UserScript== // @name Bilibili 盲盒统计 // @namespace Schwi // @version 0.3 // @description 调用 API 来收集自己的 Bilibili 盲盒概率,公示概率真的准确吗?(受API限制,获取的记录大约只有最近2个自然月) // @author Schwi // @match *://*.bilibili.com/* // @connect api.live.bilibili.com // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @noframes // @supportURL https://github.com/cyb233/script // @icon https://www.bilibili.com/favicon.ico // @license GPL-3.0 // @downloadURL none // ==/UserScript== (function () { 'use strict'; const api = { getBlindBox: (nextId = 0, month = '', pageSize = 100) => `https://api.live.bilibili.com/xlive/fuxi-interface/gift/blindGiftStream?nextId=${nextId}&month=${month}&pageSize=${pageSize}`, getBlindBoxByIds: (ids = [], nextId = 0, month = '', size = 100) => `https://api.live.bilibili.com/xlive/fuxi-interface/BlindBoxController/getRecordsByIds?_ts_rpc_args_=[${ids},${nextId},"${month}",${size}]` } // https://api.live.bilibili.com/gift/v3/live/gift_config /* const resp = await fetch('https://api.live.bilibili.com/gift/v3/live/gift_config').then(resp => resp.json()) const gifts = resp.data gifts.find(gift => gift.id === 1)?.name gifts.find(gift => gift.name === '')?.id */ // 盲盒信息,percentage为官方公示概率(不包含活动倍率) const giftInfo = { 32649: { id: 32649, name: '星月盲盒', price: 50, gifts: [ { id: 32698, name: '小蛋糕', price: 15, percentage: 20, subGifts: {} }, { id: 32694, name: '星与月', price: 25, percentage: 24.3, subGifts: {} }, { id: 32075, name: '情书', price: 52, percentage: 23.15, subGifts: {} }, { id: 34188, name: '少女祈祷', price: 66, percentage: 20, subGifts: {} }, { id: 32695, name: '冲鸭', price: 99, percentage: 10.3, subGifts: {} }, { id: 32700, name: '星河入梦', price: 199, percentage: 2, subGifts: {} }, { id: 32692, name: '落樱缤纷', price: 600, percentage: 0.25, subGifts: {} } ] }, 32251: { id: 32251, name: '心动盲盒', price: 150, gifts: [ { id: 32125, name: '电影票', price: 20, percentage: 6, subGifts: { 34614: { id: 34614, name: '梦幻气球' }, 34620: { id: 34620, name: '冰晶雪花' }, 34626: { id: 34626, name: '盛典礼花' } } }, { id: 32126, name: '棉花糖', price: 90, percentage: 44.5, subGifts: { 34615: { id: 34615, name: '星星糖' }, 34621: { id: 34621, name: '水晶星星' }, 34627: { id: 34627, name: '星际徽章' } } }, { id: 32128, name: '爱心抱枕', price: 160, percentage: 45.56, subGifts: { 34616: { id: 34616, name: '梦境玫瑰' }, 34622: { id: 34622, name: '冰晶之球' }, 34628: { id: 34628, name: '荣耀皇冠' } } }, { id: 32281, name: '绮彩权杖', price: 400, percentage: 3.7, subGifts: { 34629: { id: 34629, name: '光辉之星' } } }, { id: 34082, name: '时空之站', price: 1000, percentage: 0.12, subGifts: { } }, { id: 34894, name: '蛇形护符', price: 2000, percentage: 0.08, subGifts: { } }, { id: 32132, name: '浪漫城堡', price: 22330, percentage: 0.04, subGifts: { } } ] }, 34052: { id: 34052, name: '奇遇盲盒', price: 330, gifts: [ { id: 34059, name: '魔力球', price: 50, percentage: 5, subGifts: {} }, { id: 34058, name: '精灵兔', price: 100, percentage: 41.67, subGifts: {} }, { id: 34057, name: '许愿神灯', price: 400, percentage: 49, subGifts: {} }, { id: 34530, name: '梦幻花车', price: 1000, percentage: 4, subGifts: {} }, { id: 34055, name: '奇遇巴士', price: 2000, percentage: 0.13, subGifts: {} }, { id: 34054, name: '星愿飞船', price: 8000, percentage: 0.1, subGifts: {} }, { id: 32683, name: '奇幻古堡', price: 28880, percentage: 0.1, subGifts: {} } ] }, 32368: { id: 32368, name: '闪耀盲盒', price: 500, gifts: [ { id: 32360, name: '璀璨钻石', price: 200, percentage: 9.96, subGifts: {} }, { id: 32359, name: '旅行日记', price: 300, percentage: 36, subGifts: {} }, { id: 34000, name: '机械幻想', price: 510, percentage: 50.1, subGifts: {} }, { id: 34082, name: '时空之站', price: 1000, percentage: 3.4, subGifts: {} }, { id: 34894, name: '蛇形护符', price: 2000, percentage: 0.28, subGifts: {} }, { id: 34895, name: '金蛇献福', price: 5000, percentage: 0.16, subGifts: {} }, { id: 32356, name: '幻影飞船', price: 30000, percentage: 0.1, subGifts: {} } ] }, 32369: { id: 32369, name: '至尊盲盒', price: 1000, gifts: [ { id: 32360, name: '璀璨钻石', price: 200, percentage: 0.1, subGifts: {} }, { id: 32281, name: '绮彩权杖', price: 400, percentage: 22.75, subGifts: {} }, { id: 32363, name: '许愿精灵', price: 888, percentage: 35, subGifts: {} }, { id: 33999, name: '星际启航', price: 1010, percentage: 40.14, subGifts: {} }, { id: 34894, name: '蛇形护符', price: 2000, percentage: 1.45, subGifts: {} }, { id: 34895, name: '金蛇献福', price: 5000, percentage: 0.32, subGifts: {} }, { id: 32361, name: '奇幻之城', price: 32000, percentage: 0.24, subGifts: {} } ] } }; const boxOrder = ['星月盲盒', '心动盲盒', '奇遇盲盒', '闪耀盲盒', '至尊盲盒'] // API 请求函数 async function apiRequest(url, retry = 3) { for (let attempt = 1; attempt <= retry; attempt++) { try { const response = await GM.xmlHttpRequest({ method: 'GET', url: url, }); const data = JSON.parse(response.responseText); return data; } catch (e) { if (attempt === retry) { throw e; } } } } // 去重合并记录并存储 function saveGiftList(newGifts) { const storedGifts = GM_getValue('allGiftList', []); const mergedGifts = [...storedGifts, ...newGifts].reduce((acc, gift) => { if (!acc.some(existingGift => existingGift.id === gift.id)) { acc.push(gift); } return acc; }, []); GM_setValue('allGiftList', mergedGifts); return mergedGifts; } // 工具函数:创建 dialog function createDialog(id, title, content) { let dialog = document.createElement('div'); dialog.id = id; dialog.style.position = 'fixed'; dialog.style.top = '5%'; dialog.style.left = '5%'; dialog.style.width = '90%'; dialog.style.height = '90%'; dialog.style.backgroundColor = '#fff'; dialog.style.border = '1px solid #ccc'; dialog.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; dialog.style.zIndex = '9999'; dialog.style.display = 'none'; dialog.style.overflow = 'hidden'; let header = document.createElement('div'); header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; header.style.padding = '10px'; header.style.borderBottom = '1px solid #ccc'; header.style.backgroundColor = '#f9f9f9'; let titleElement = document.createElement('span'); titleElement.textContent = title; header.appendChild(titleElement); let closeButton = document.createElement('button'); closeButton.textContent = '关闭'; closeButton.style.backgroundColor = '#ff4d4f'; closeButton.style.color = '#fff'; closeButton.style.border = 'none'; closeButton.style.borderRadius = '5px'; closeButton.style.cursor = 'pointer'; closeButton.style.padding = '5px 10px'; closeButton.style.transition = 'background-color 0.3s'; closeButton.onmouseover = () => { closeButton.style.backgroundColor = '#d93637'; } closeButton.onmouseout = () => { closeButton.style.backgroundColor = '#ff4d4f'; } closeButton.onclick = () => dialog.remove(); header.appendChild(closeButton); dialog.appendChild(header); let contentArea = document.createElement('div'); contentArea.innerHTML = content; contentArea.style.padding = '10px'; contentArea.style.overflowY = 'auto'; // 允许垂直滚动 contentArea.style.height = 'calc(100% - 40px)'; // 减去 header 的高度 dialog.appendChild(contentArea); document.body.appendChild(dialog); return { dialog: dialog, header: header, titleElement: titleElement, closeButton: closeButton, contentArea: contentArea }; } // 循环请求盲盒数据 async function fetchAllBlindBoxes() { let nextId = 0; let month = ''; let isMore = 1; const allGiftList = []; // 创建进度弹窗 let { dialog: progressDialog, contentArea: progressContentArea } = createDialog('progressDialog', '盲盒数据收集进度', `

已收集盲盒数:0

`); progressDialog.style.display = 'block'; while (isMore) { try { const response = await apiRequest(api.getBlindBox(nextId, month)); if (response.code === 0 && response.data) { const { list, params } = response.data; list.forEach(gift => { gift.id = parseInt(gift.id, 10); gift.originalGiftId = parseInt(gift.originalGiftId, 10); gift.giftId = parseInt(gift.giftId, 10); gift.giftNum = parseInt(gift.giftNum, 10); delete gift.giftImg; }) allGiftList.push(...list); console.log('当前盲盒数据:', list, params); nextId = params.nextId; month = params.month; isMore = params.isMore; // 更新进度弹窗 progressContentArea.querySelector('#collectedCount').textContent = allGiftList.length; } else { console.error('API 返回错误:', response.message); break; } } catch (error) { console.error('请求失败:', error); break; } } // 关闭进度弹窗 progressDialog.remove(); // 去重并存储 const mergedGiftList = saveGiftList(allGiftList); console.log('合并后的盲盒数据:', mergedGiftList); // {originalGiftId: {giftId: giftName}} 格式化,仅保存giftInfo中gifts及subGifts中不存在的礼物 const giftMap = {}; mergedGiftList.forEach(gift => { const { originalGiftId, originalGiftName, giftId, giftName } = gift; if (!giftMap[originalGiftId]) { giftMap[originalGiftId] = { name: originalGiftName }; } const giftInfoEntry = giftInfo[originalGiftId]?.gifts.find(g => g.id === giftId || Object.values(g.subGifts).some(gift => gift.id === giftId)); if (!giftInfoEntry) { giftMap[originalGiftId][giftId] = giftName; } }); console.log('礼物 ID 映射(按 originalGiftId 分组):', giftMap); // 根据 originalGiftId 分组统计 giftId 数量 const groupedGiftStats = {}; mergedGiftList.forEach(gift => { const { originalGiftId, originalGiftName, giftId, giftName, giftNum } = gift; if (!groupedGiftStats[originalGiftId]) { groupedGiftStats[originalGiftId] = { originalGiftName, totalCount: 0, gifts: {} }; } // 检查 giftId 是否属于 subGifts let mainGiftId = giftId; const giftInfoEntry = giftInfo[originalGiftId]?.gifts.find(g => g.id === giftId || Object.values(g.subGifts).some(gift => gift.id === giftId)); if (giftInfoEntry) { mainGiftId = giftInfoEntry.id; } if (!groupedGiftStats[originalGiftId].gifts[mainGiftId]) { groupedGiftStats[originalGiftId].gifts[mainGiftId] = { giftName: giftInfoEntry?.name || giftName, count: 0, percentage: 0 }; } groupedGiftStats[originalGiftId].totalCount += giftNum; groupedGiftStats[originalGiftId].gifts[mainGiftId].count += giftNum; }); // 计算每个 giftId 的百分比概率 Object.values(groupedGiftStats).forEach(group => { Object.values(group.gifts).forEach(gift => { gift.percentage = ((gift.count / group.totalCount) * 100).toFixed(2) + '%'; }); }); console.log('按 originalGiftId 分组的盲盒统计:', groupedGiftStats); // 显示结果弹窗 showResultsDialog(groupedGiftStats); } // 显示结果 dialog function showResultsDialog(groupedGiftStats) { const { dialog, titleElement, closeButton, contentArea } = createDialog('resultsDialog', '盲盒统计结果', ''); // 获取排序后的 originalGiftId 数组 const sortedOriginalGiftIds = Object.entries(groupedGiftStats) .sort(([originalGiftIdA, groupA], [originalGiftIdB, groupB]) => { const nameA = groupA.originalGiftName; const nameB = groupB.originalGiftName; const indexA = boxOrder.indexOf(nameA); const indexB = boxOrder.indexOf(nameB); if (indexA === -1 && indexB === -1) return 0; if (indexA === -1) return 1; if (indexB === -1) return -1; return indexA - indexB; }) .map(([originalGiftId]) => originalGiftId); // 循环创建每个盲盒的表格 sortedOriginalGiftIds.forEach(originalGiftId => { const group = groupedGiftStats[originalGiftId]; // 创建标题 let title = document.createElement('h2'); title.textContent = `${group.originalGiftName} (总抽数: ${group.totalCount})`; title.style.marginTop = '20px'; contentArea.appendChild(title); // 创建表格 let table = document.createElement('table'); table.style.width = '100%'; table.style.borderCollapse = 'collapse'; table.style.margin = '10px 0'; // 创建表头 let thead = table.createTHead(); let headerRow = thead.insertRow(); let headers = ['礼物名称', '数量', '你的概率', '公示概率']; headers.forEach(headerText => { let th = document.createElement('th'); th.textContent = headerText; th.style.padding = '8px'; th.style.border = '1px solid #ddd'; th.style.textAlign = 'left'; headerRow.appendChild(th); }); // 创建表体 let tbody = table.createTBody(); // 获取排序后的 gifts 数组 const sortedGifts = Object.entries(group.gifts).sort(([giftIdA, giftA], [giftIdB, giftB]) => { const giftInfoA = giftInfo[originalGiftId]?.gifts.find(g => g.id === parseInt(giftIdA)); const giftInfoB = giftInfo[originalGiftId]?.gifts.find(g => g.id === parseInt(giftIdB)); if (!giftInfoA && !giftInfoB) return 0; if (!giftInfoA) return 1; if (!giftInfoB) return -1; const indexA = giftInfo[originalGiftId].gifts.indexOf(giftInfoA); const indexB = giftInfo[originalGiftId].gifts.indexOf(giftInfoB); return indexA - indexB; }); sortedGifts.forEach(([giftId, gift]) => { let row = tbody.insertRow(); let cell1 = row.insertCell(); let cell2 = row.insertCell(); let cell3 = row.insertCell(); let cell4 = row.insertCell(); let giftLink = document.createElement('a'); giftLink.href = `https://shuvi.moe/sync-bilibili-gifts/#${giftId}`; giftLink.textContent = gift.giftName; giftLink.target = '_blank'; // 在新标签页中打开 cell1.appendChild(giftLink); cell2.textContent = gift.count; cell3.textContent = gift.percentage; // 获取公示概率 const officialPercentage = giftInfo[originalGiftId]?.gifts.find(g => g.id === parseInt(giftId))?.percentage; cell4.textContent = officialPercentage ? officialPercentage + '%' : 'N/A'; [cell1, cell2, cell3, cell4].forEach(cell => { cell.style.padding = '8px'; cell.style.border = '1px solid #ddd'; cell.style.textAlign = 'left'; }); }); // 将表格添加到弹窗内容区域 contentArea.appendChild(table); }); dialog.style.display = 'block'; } // 注册菜单项 GM_registerMenuCommand("检查盲盒数据", fetchAllBlindBoxes); })();