// ==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
// ==/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', '盲盒数据收集进度', `<p>已收集盲盒数:<span id='collectedCount'>0</span></p>`);
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);
})();