Greasy Fork is available in English.

Bilibili 收藏集奖励筛查脚本

调用 API 来收集自己的 Bilibili 收藏集,并筛选未领取的奖励。注意,一套收藏集中至少存在一张卡牌才能本项目的接口被检测到!

安裝腳本?
作者推薦腳本

您可能也會喜歡 Bilibili 动态筛选

安裝腳本
// ==UserScript==
// @name         Bilibili 收藏集奖励筛查脚本
// @namespace    Schwi
// @version      1.0
// @description  调用 API 来收集自己的 Bilibili 收藏集,并筛选未领取的奖励。注意,一套收藏集中至少存在一张卡牌才能本项目的接口被检测到!
// @author       Schwi
// @match        *://*.bilibili.com/*
// @connect      api.bilibili.com
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @noframes
// @supportURL   https://github.com/cyb233/script
// @license      GPL-3.0
// ==/UserScript==

(function () {
    "use strict";

    const REDEEM_ITEM_TYPE = {
        Card: 1,
        Emoji: 2,
        Pendant: 3,
        Suit: 4,
        MaterialCombination: 5,
        AudioCard: 6,
        Jump: 7,
        Cdk: 8,
        RealGoods: 9,
        LimitMaterialCombination: 10,
        CustomReward: 11,
        DynamicEmoji: 15,
        DiamondAvatar: 1000,
        CollectorMedal: 1001,
    };

    // 创建进度条容器
    function createProgressBar(totalTasks) {
        const progressContainer = document.createElement("div");
        progressContainer.style.position = "fixed";
        progressContainer.style.top = "50%";
        progressContainer.style.left = "50%";
        progressContainer.style.transform = "translate(-50%, -50%)";
        progressContainer.style.width = "80%";
        progressContainer.style.padding = "10px";
        progressContainer.style.backgroundColor = "#fff";
        progressContainer.style.borderRadius = "10px";
        progressContainer.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.2)";
        progressContainer.style.zIndex = "10000";
        progressContainer.style.textAlign = "center";

        const progressTitle = document.createElement("h3");
        progressTitle.textContent = "任务进行中...";
        progressContainer.appendChild(progressTitle);

        const progressBar = document.createElement("progress");
        progressBar.style.width = "100%";
        progressBar.max = totalTasks;
        progressBar.value = 0;
        progressContainer.appendChild(progressBar);

        const progressText = document.createElement("p");
        progressText.style.marginTop = "10px";
        progressText.textContent = `0/${totalTasks} 完成`;
        progressContainer.appendChild(progressText);

        document.body.appendChild(progressContainer);

        return {
            update: function (currentTask) {
                progressBar.value = currentTask;
                progressText.textContent = `${currentTask}/${totalTasks} 完成`;
            },
            hide: function () {
                document.body.removeChild(progressContainer);
            }
        };
    }

    // 发起 API 请求的函数
    function apiRequest(url, callback) {
        console.log(`正在请求: ${url}`);
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            onload: function (response) {
                try {
                    const data = JSON.parse(response.responseText);
                    console.log(`来自 ${url} 的响应:`, data);
                    callback(data);
                } catch (error) {
                    console.error(`解析来自 ${url} 的响应时出错:`, error);
                    callback(null);
                }
            },
            onerror: function (error) {
                console.error(`请求 ${url} 失败:`, error);
                callback(null);
            },
        });
    }

    // 主函数,用于收集收藏集
    function collectDigitalCards() {
        console.log("开始收集收藏集...");
        const collectionUrl =
              "https://api.bilibili.com/x/vas/smelt/my_decompose/info?scene=1";
        let collectList = [];

        apiRequest(collectionUrl, function (collectionData) {
            if (!collectionData || collectionData.code !== 0) {
                const errorMsg = `获取收藏列表失败: ${collectionData ? collectionData.message : "无响应"}`
                console.error(errorMsg);
                alert(errorMsg)
                return;
            }
            if (!collectionData.data.list) {
                const errorMsg = `获取收藏列表失败: 您没有收藏集`
                console.error(errorMsg);
                alert(errorMsg)
                return;
            }

            console.log("成功获取收藏列表:", collectionData.data.list);
            const collections = collectionData.data.list;
            const collectionCount = collections.length;
            let processedCollections = 0;

            const progressBar = createProgressBar(collectionCount); // 创建进度条

            collections.forEach((collection, index) => {
                console.log(`处理收藏: ${collection.act_name}(ID: ${collection.act_id})`);
                const detailUrl = `https://api.bilibili.com/x/vas/dlc_act/act/basic?act_id=${collection.act_id}`;

                apiRequest(detailUrl, function (detailData) {
                    if (!detailData || detailData.code !== 0) {
                        console.error(
                            `获取 ${collection.act_name}(act_id:${collection.act_id}) 的基本信息失败:`,
                            detailData ? detailData.message : "无响应"
                        );
                        processedCollections++;
                        progressBar.update(processedCollections); // 更新进度条
                        checkCompletion();
                        return;
                    }

                    console.log(
                        `成功获取 ${collection.act_name}(act_id:${collection.act_id}) 的基本信息:`,
                        detailData.data
                    );
                    const lotteries = detailData.data.lottery_list;
                    let processedLotteries = 0;

                    lotteries.forEach((lottery) => {
                        console.log(
                            `处理详情: ${lottery.lottery_name} (ID: ${lottery.lottery_id})`
            );
                        const cardDetailUrl = `https://api.bilibili.com/x/vas/dlc_act/lottery_home_detail?act_id=${collection.act_id}&lottery_id=${lottery.lottery_id}`;

                        apiRequest(cardDetailUrl, function (cardData) {
                            if (!cardData || cardData.code !== 0) {
                                console.error(
                                    `获取 ${collection.act_name}(act_id:${collection.act_id}&lottery_id:${lottery.lottery_id}) 的详情失败:`,
                                    cardData ? cardData.message : "无响应"
                                );
                                processedLotteries++;
                                progressBar.update(processedCollections); // 更新进度条
                                checkLotteryCompletion();
                                return;
                            }

                            console.log(
                                `成功获取 ${collection.act_name}[${cardData.data.name}](act_id:${collection.act_id}&lottery_id:${lottery.lottery_id}) 的详情:`,
                                cardData.data
                            );
                            // 根据需要处理卡牌数据
                            collectList.push({ title: detailData.data.act_title, name: cardData.data.name, url: `https://www.bilibili.com/blackboard/activity-Mz9T5bO5Q3.html?id=${collection.act_id}&type=dlc`, act: detailData.data, lottery: cardData.data });
                            processedLotteries++;
                            progressBar.update(processedCollections); // 更新进度条
                            checkLotteryCompletion();
                        });

                        function checkLotteryCompletion() {
                            if (processedLotteries === lotteries.length) {
                                processedCollections++;
                                progressBar.update(processedCollections); // 更新进度条
                                checkCompletion();
                            }
                        }
                    });
                });

            });

            function checkCompletion() {
                if (processedCollections === collectionCount) {
                    console.log("所有收藏已处理。");
                    console.log("最终收集列表:", collectList);

                    // 筛选出符合条件的收藏集
                    const filteredCollectList = collectList.filter((collectItem) => {
                        return (
                            collectItem.lottery.collect_list.collect_infos?.some(
                                (lottery) =>
                                canGetReward(lottery)
                            ) ||
                            collectItem.lottery.collect_list.collect_chain?.some(
                                (lottery) =>
                                canGetReward(lottery)
                            )
                        );
                    });

                    console.log("筛选后的收集列表:", filteredCollectList);
                    progressBar.hide(); // 隐藏进度条
                    showResultDialog(collectList, filteredCollectList)
                }
            }

            /**
             * 别问我为啥这么写,B站前端JS就是这么判断的
             *
             * @param {object} reward 每条奖励的信息
             * @param {string} scene 不知道是啥,还没研究明白,先这么写着
             * @returns boolean 这个奖励是否能被领取
             */
            function canGetReward(reward, scene = "milestone") {
                const curTime = new Date().getTime();
                const has_redeemed_cnt = reward.has_redeemed_cnt;
                const redeem_item_type = reward.redeem_item_type;
                const total_stock = reward.total_stock;
                const remain_stock = reward.remain_stock;
                const redeem_cond_type = reward.redeem_cond_type;
                const owned_item_amount = reward.owned_item_amount;
                const require_item_amount = reward.require_item_amount;
                const unlock_condition = reward.unlock_condition;
                const redeem_count = reward.redeem_count;
                const end_time = reward.end_time;
                const unlock_condition_1 = unlock_condition || {};
                const unlocked = unlock_condition_1.unlocked;
                const lock_type = unlock_condition_1.lock_type;
                const unlock_threshold = unlock_condition_1.unlock_threshold;
                const expire_at = unlock_condition_1.expire_at;
                let exceedReceiveTime = false;
                if ([REDEEM_ITEM_TYPE.CollectorMedal, REDEEM_ITEM_TYPE.DiamondAvatar].includes(redeem_item_type)) {
                    exceedReceiveTime = curTime > end_time;
                } else {
                    if (!(curTime > end_time)) {
                        exceedReceiveTime = true;
                    }
                    if (!reward.effective_forever) {
                        exceedReceiveTime = true;
                    }
                }
                if (unlocked || "milestone" === scene) {
                    if (!(has_redeemed_cnt && [REDEEM_ITEM_TYPE.CustomReward].includes(redeem_item_type))) {
                        if (!(has_redeemed_cnt && "card_number" !== redeem_cond_type)) {
                            if (!((+total_stock > -1 && +remain_stock <= 0) || exceedReceiveTime)) {
                                if (!("custom" === redeem_cond_type || [REDEEM_ITEM_TYPE.DiamondAvatar].includes(redeem_item_type))) {
                                    if (!((owned_item_amount || 0) < require_item_amount)) {
                                        return true
                                    }
                                }
                            }
                        }
                    }
                }
                return false
            }

            function showResultDialog(collectList, filteredCollectList) {
                // 创建弹窗
                const dialog = document.createElement("div");
                dialog.style.position = "fixed";
                dialog.style.top = "50%";
                dialog.style.left = "50%";
                dialog.style.transform = "translate(-50%, -50%)";
                dialog.style.backgroundColor = "#fff";
                dialog.style.border = "1px solid #ccc";
                dialog.style.padding = "20px";
                dialog.style.zIndex = "10000";
                dialog.style.width = "90%";
                dialog.style.height = "90%";
                dialog.style.overflowY = "auto";
                dialog.style.overflowX = "hidden";
                dialog.style.fontFamily = "Arial, sans-serif";
                dialog.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.2)";
                dialog.style.display = "flex";
                dialog.style.flexDirection = "column";

                // 标题和按钮容器
                const header = document.createElement("div");
                header.style.display = "flex";
                header.style.justifyContent = "space-between";
                header.style.alignItems = "center";
                header.style.marginBottom = "20px";

                // 左侧标题和 Debug 按钮容器
                const titleContainer = document.createElement("div");
                titleContainer.style.display = "flex";
                titleContainer.style.alignItems = "center";
                titleContainer.style.gap = "10px"; // 间距

                // 标题
                const title = document.createElement("h2");
                title.textContent = "筛选结果";
                title.style.margin = "0"; // 去掉默认边距
                titleContainer.appendChild(title);

                // Debug 按钮
                const debugButton = document.createElement("button");
                debugButton.textContent = "Debug";
                debugButton.style.padding = "5px 10px";
                debugButton.style.backgroundColor = "#4caf50";
                debugButton.style.color = "#fff";
                debugButton.style.border = "none";
                debugButton.style.borderRadius = "5px";
                debugButton.style.cursor = "pointer";
                titleContainer.appendChild(debugButton);

                // Debug 按钮的点击事件
                debugButton.addEventListener("click", () => {
                    const groupedByType = {};

                    collectList.forEach((item) => {
                        // 从奖励数据中获取类型并分组
                        const rewardList = item.lottery.collect_list?.collect_infos || [];
                        rewardList.forEach((reward) => {
                            const type = Object.keys(REDEEM_ITEM_TYPE).find(
                                (key) => REDEEM_ITEM_TYPE[key] === reward.redeem_item_type
                            ) || `未知类型(${reward.redeem_item_type})`;

                            if (!groupedByType[type]) {
                                groupedByType[type] = [];
                            }
                            groupedByType[type].push(item);
                        });
                    });

                    console.log("按类型分组的收藏集:", groupedByType);
                });

                // 关闭按钮
                const closeButton = document.createElement("button");
                closeButton.textContent = "关闭";
                closeButton.style.padding = "5px 10px";
                closeButton.style.backgroundColor = "#ff4d4d";
                closeButton.style.color = "#fff";
                closeButton.style.border = "none";
                closeButton.style.borderRadius = "5px";
                closeButton.style.cursor = "pointer";

                closeButton.addEventListener("click", () => {
                    document.body.removeChild(dialog);
                });

                // 将标题和按钮容器添加到标题栏
                header.appendChild(titleContainer);
                header.appendChild(closeButton);
                dialog.appendChild(header);

                // 复选框控制区域
                const filterBox = document.createElement("div");
                filterBox.style.marginBottom = "20px";

                const filterLabel = document.createElement("label");
                filterLabel.textContent = "筛选";
                filterLabel.style.marginRight = "10px";

                const filterCheckbox = document.createElement("input");
                filterCheckbox.type = "checkbox";
                filterBox.appendChild(filterLabel);
                filterBox.appendChild(filterCheckbox);
                dialog.appendChild(filterBox);

                // 网格容器
                const gridContainer = document.createElement("div");
                gridContainer.style.display = "grid";
                gridContainer.style.gridTemplateColumns = "repeat(auto-fill, minmax(200px, 1fr))";
                gridContainer.style.gap = "15px";
                gridContainer.style.flex = "1"; // 占满剩余高度
                dialog.appendChild(gridContainer);

                // 渲染列表函数
                function renderList(showFiltered) {
                    filterCheckbox.checked = showFiltered; // 更新复选框的初始状态

                    gridContainer.innerHTML = ""; // 清空之前的内容
                    const list = showFiltered ? filteredCollectList : collectList;
                    if (list.length === 0) {
                        const emptyMessage = document.createElement("p");
                        emptyMessage.textContent = showFiltered
                            ? "没有符合筛选条件的收藏集。"
                        : "没有收藏集数据。";
                        gridContainer.appendChild(emptyMessage);
                        return;
                    }

                    list.forEach((item) => {
                        const card = document.createElement("div");
                        card.style.position = "relative";
                        card.style.border = "1px solid #ddd";
                        card.style.borderRadius = "10px";
                        card.style.overflow = "hidden";
                        card.style.height = "200px";
                        card.style.backgroundImage = `url(${item.act.act_square_img})`;
                        card.style.backgroundSize = "cover";
                        card.style.backgroundPosition = "center";
                        card.style.display = "flex";
                        card.style.flexDirection = "column";
                        card.style.justifyContent = "flex-end";
                        card.style.padding = "10px";
                        card.style.color = "#fff";

                        // 标题容器
                        const titleContainer = document.createElement("div");
                        titleContainer.style.background = "rgba(0, 0, 0, 0.5)";
                        titleContainer.style.backdropFilter = "blur(5px)";
                        titleContainer.style.borderRadius = "5px";
                        titleContainer.style.padding = "5px";
                        titleContainer.style.marginBottom = "5px";

                        // 标题
                        const cardTitle = document.createElement("div");
                        cardTitle.style.fontWeight = "bold";
                        cardTitle.style.textShadow = "0 2px 4px rgba(0, 0, 0, 0.8)";
                        cardTitle.textContent = item.title;

                        // 副标题
                        const cardSubtitle = document.createElement("div");
                        cardSubtitle.style.fontSize = "14px";
                        cardSubtitle.style.marginTop = "2px";
                        cardSubtitle.textContent = item.name;

                        titleContainer.appendChild(cardTitle);
                        titleContainer.appendChild(cardSubtitle);

                        const link = document.createElement("a");
                        link.href = item.url;
                        link.target = "_blank";
                        link.textContent = "查看详情";
                        link.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
                        link.style.color = "#fff";
                        link.style.padding = "5px 10px";
                        link.style.borderRadius = "5px";
                        link.style.textDecoration = "none";
                        link.style.textAlign = "center";

                        card.appendChild(titleContainer);
                        card.appendChild(link);

                        gridContainer.appendChild(card);
                    });
                }

                // 初始渲染为筛选后的列表
                renderList(true);

                // 添加复选框事件
                filterCheckbox.addEventListener("change", () => {
                    renderList(filterCheckbox.checked);
                });

                // 将弹窗添加到页面
                document.body.appendChild(dialog);
            }

        })
    }

    // 在 Tampermonkey 菜单中添加一个按钮
    GM_registerMenuCommand("检查收藏集", collectDigitalCards);
})();