Greasy Fork

Greasy Fork is available in English.

Bilibili 盲盒统计

调用 API 来收集自己的 Bilibili 盲盒概率,公示概率真的准确吗?(受API限制,获取的记录大约只有最近2个自然月)

当前为 2025-04-29 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bilibili 盲盒统计
// @namespace    Schwi
// @version      0.2
// @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: {} }
      ]
    }
  };

  // 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;
  }

  // 循环请求盲盒数据
  async function fetchAllBlindBoxes() {
    let nextId = 0;
    let month = '';
    let isMore = 1;

    const allGiftList = [];
    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;
        } else {
          console.error('API 返回错误:', response.message);
          break;
        }
      } catch (error) {
        console.error('请求失败:', error);
        break;
      }
    }

    // 去重并存储
    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);
  }

  // 注册菜单项
  GM_registerMenuCommand("检查盲盒数据", fetchAllBlindBoxes);

})();