B站弹幕查询器(查发布者)

通过B站视频查询弹幕并查找指定用户

// ==UserScript==
// @name         B站弹幕查询器(查发布者)
// @namespace    PyHaoCoder
// @version      1.0
// @description  通过B站视频查询弹幕并查找指定用户
// @author       PyHaoCoder
// @icon         https://www.bilibili.com/favicon.ico
// @match        https://www.bilibili.com/video/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      api.bilibili.com
// ==/UserScript==


(function () {
    'use strict';

    // 定时器间隔时间(单位:毫秒)
    const intervalTime = 1000; // 5秒

    // ==================== 获取视频CID ====================
    function fetchCID() {
        const bvid = location.href.split('/')[4].split('?')[0];
        const url = `https://api.bilibili.com/x/player/pagelist?bvid=${bvid}&jsonp=jsonp`

        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: function (response) {
                // 获取页面内容
                const pageContent = response.responseText;
                const data = JSON.parse(pageContent);
                if (data.data) {
                    // 获取视频CID
                    window._cid = data.data[0].cid; // 更新全局变量
                    console.log('获取视频CID:', window._cid);

                    // 更新 _url
                    if (!window._url || location.href !== _url) {
                        window._url = location.href;
                    }
                } else {
                    console.error('获取视频CID失败');
                }
            },
            onerror: function (err) {
                console.log('获取视频CID失败:' + err.statusText);
            }
        });
    }

    // 隐藏表格
    function hideTable() {
        const container = document.getElementById('resultContainer')
        container.style.display = "none"

    }

    // 初始化定时器
    function startTimer() {
        setInterval(() => {
            // 如果 _url 未定义或当前 URL 不等于 _url,则更新 CID
            if (window._url && location.href !== window._url) {
                fetchCID();
                hideTable();
            }
        }, intervalTime);
    }

    // 首次初始化
    fetchCID();

    // 启动定时器
    startTimer();
})();

(function () {
    'use strict';

    // ==================== 哈希转换模块 ====================
    window.BiliBili_midcrc = function () {
        'use strict';
        const CRCPOLYNOMIAL = 0xEDB88320;
        const startTime = new Date().getTime(),
            crctable = new Array(256),
            create_table = function () {
                let crcreg,
                    i, j;
                for (i = 0; i < 256; ++i) {
                    crcreg = i;
                    for (j = 0; j < 8; ++j) {
                        if ((crcreg & 1) != 0) {
                            crcreg = CRCPOLYNOMIAL ^ (crcreg >>> 1);
                        } else {
                            crcreg >>>= 1;
                        }
                    }
                    crctable[i] = crcreg;
                }
            },
            crc32 = function (input) {
                if (typeof (input) != 'string')
                    input = input.toString();
                let crcstart = 0xFFFFFFFF, len = input.length, index;
                for (let i = 0; i < len; ++i) {
                    index = (crcstart ^ input.charCodeAt(i)) & 0xff;
                    crcstart = (crcstart >>> 8) ^ crctable[index];
                }
                return crcstart;
            },
            crc32lastindex = function (input) {
                if (typeof (input) != 'string')
                    input = input.toString();
                let crcstart = 0xFFFFFFFF, len = input.length, index;
                for (let i = 0; i < len; ++i) {
                    index = (crcstart ^ input.charCodeAt(i)) & 0xff;
                    crcstart = (crcstart >>> 8) ^ crctable[index];
                }
                return index;
            },
            getcrcindex = function (t) {
                for (let i = 0; i < 256; i++) {
                    if (crctable[i] >>> 24 == t)
                        return i;
                }
                return -1;
            },
            deepCheck = function (i, index) {
                let tc = 0x00, str = '',
                    hash = crc32(i);
                tc = hash & 0xff ^ index[2];
                if (!(tc <= 57 && tc >= 48))
                    return [0];
                str += tc - 48;
                hash = crctable[index[2]] ^ (hash >>> 8);
                tc = hash & 0xff ^ index[1];
                if (!(tc <= 57 && tc >= 48))
                    return [0];
                str += tc - 48;
                hash = crctable[index[1]] ^ (hash >>> 8);
                tc = hash & 0xff ^ index[0];
                if (!(tc <= 57 && tc >= 48))
                    return [0];
                str += tc - 48;
                hash = crctable[index[0]] ^ (hash >>> 8);
                return [1, str];
            };
        create_table();
        const index = new Array(4);

        // 单次转换函数
        const singleConvert = function (input) {
            let ht = parseInt('0x' + input) ^ 0xffffffff,
                snum, i, lastindex, deepCheckData;
            for (i = 3; i >= 0; i--) {
                index[3 - i] = getcrcindex(ht >>> (i * 8));
                snum = crctable[index[3 - i]];
                ht ^= snum >>> ((3 - i) * 8);
            }
            for (i = 0; i < 100000000; i++) {
                lastindex = crc32lastindex(i);
                if (lastindex == index[3]) {
                    deepCheckData = deepCheck(i, index)
                    if (deepCheckData[0])
                        break;
                }
            }

            if (i == 100000000)
                return -1;
            return i + '' + deepCheckData[1];
        };

        // 批量转换函数
        const batchConvert = function (hashArray) {
            return hashArray.map(function (hash) {
                return singleConvert(hash);
            });
        };

        return {
            singleConvert: singleConvert, // 单次转换
            batchConvert: batchConvert   // 批量转换
        };
    };
})();

(function () {
    'use strict';

    // ==================== 油猴脚本主逻辑 ====================
    // 创建UI界面
    function createUI() {
        const style = `
        <style>
            .bili-parser-container {
                position: fixed;
                top: 70px;
                right: 20px;
                z-index: 9999;
                background: white;
                padding: 20px;
                border-radius: 10px;
                box-shadow: 0 0 10px rgba(0,0,0,0.2);
                width: 300px;
                cursor: default;
                transition: transform 0.1s ease-out; /* 平滑复位效果 */
                will-change: transform; /* 提前声明变化属性 */
            }
            .bili-parser-header {
                cursor: move;
                padding: 10px 0;
                margin: -10px 0 10px;
                border-bottom: 1px solid #eee;
            }
            #resultContainer {
                max-height: 400px; /* 设置最大高度 */
                overflow-y: auto; /* 添加垂直滚动条 */
                margin-top: 0;
                display: none;
            }
            .bili-input {
                width: 100%;
                padding: 8px 0;
                margin: 0 0 8px;
                border: 1px solid #ddd;
                box-sizing: border-box; /* 确保宽度包括内边距和边框 */
            }
            #keywordInput.bili-input {
              padding-left: 8px;
              padding-right: 8px;
            }
            .bili-btn {
                background: #00a1d6;
                color: white;
                border: none;
                padding: 8px 15px;
                cursor: pointer;
                width: 100%;
                box-sizing: border-box; /* 确保宽度包括内边距和边框 */
            }
            .result-table {
                width: 100%;
                border-collapse: collapse;
                display: block;
            }
            .result-table td, .result-table th {
                border: 1px solid #ddd;
                padding: 8px;
                font-size: 12px;
            }
        </style>
    `;

        const html = `
        <div class="bili-parser-container">
           <div class="bili-parser-header"><h3>B站弹幕查询器 <span style="float: right;">By: @PyHaoCoder</span></h3></div>
            <input type="text" class="bili-input" id="keywordInput" placeholder="输入要查找的关键字">
            <button class="bili-btn" id="startSearch">开始搜索</button>
            <div id="resultContainer"></div>
        </div>
    `;

        document.body.insertAdjacentHTML('afterbegin', style + html);

        // 添加悬浮窗移动功能
        addDragFunctionality();
    }

    // ==================== 添加悬浮窗移动功能 ====================
    function addDragFunctionality() {
        const container = document.querySelector('.bili-parser-container');
        const header = document.querySelector('.bili-parser-header');

        let isDragging = false;
        let startX, startY, initialX, initialY;

        header.addEventListener('mousedown', (e) => {
            isDragging = true;
            startX = e.clientX;
            startY = e.clientY;
            initialX = container.offsetLeft;
            initialY = container.offsetTop;

            // 防止文本选中
            document.body.style.userSelect = 'none';
            document.body.style.webkitUserSelect = 'none';
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;

            const deltaX = e.clientX - startX;
            const deltaY = e.clientY - startY;

            // 计算新位置(限制在窗口范围内)
            const newX = Math.max(0, Math.min(window.innerWidth - container.offsetWidth, initialX + deltaX));
            const newY = Math.max(0, Math.min(window.innerHeight - container.offsetHeight, initialY + deltaY));

            container.style.left = `${newX}px`;
            container.style.right = 'auto';
            container.style.top = `${newY}px`;
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            document.body.style.userSelect = '';
            document.body.style.webkitUserSelect = '';
        });
    }

    // 获取视频CID
    function getVideoCID() {
        if (window._cid) {
            return window._cid
        }
    }

    // 显示结果
    function showResults(comments) {
        const container = document.getElementById('resultContainer');
        let html = `
        <table class="result-table">
            <tr>
                <th>用户MID</th>
                <th>时间</th>
                <th>内容</th>
            </tr>
    `;

        // 显示所有数据
        comments.forEach(comment => {
            // 如果弹幕长度超过 40,则截断并添加“...”
            const text = comment.text.length > 40 ? comment.text.substring(0, 40) + '...' : comment.text;
            html += `
            <tr>
                <td><a href="https://space.bilibili.com/${comment.mid}" target="_blank">${comment.mid}</a></td>
                <td>${comment.date}</td>
                <td>${text}</td>
            </tr>
        `;
        });

        html += '</table>';

        // 添加“共 n 条数据”提示
        html += `
        <div style="margin-top: 10px; color: #666; text-align: center;">
            共 ${comments.length} 条数据
        </div>
    `;

        container.innerHTML = html;
        container.style.marginTop = "10px"
        container.style.display = "block"

        // 动态调整表格高度和滚动条
        const table = container.querySelector('.result-table');
        if (table) {
            if (table.scrollHeight > 280) {
                table.style.maxHeight = '280px';
                table.style.overflowY = 'auto'; // 添加垂直滚动条
            } else {
                table.style.maxHeight = 'none';
                table.style.overflowY = 'visible';
            }
        }
    }

    // 主逻辑
    async function main(keyword) {
        const cid = getVideoCID();
        if (!cid) {
            alert('获取视频信息失败,请刷新页面重试');
            return;
        } else {
            console.log(`开始解析:https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`)
        }

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`,
            onload: function (response) {
                const parser = new DOMParser();
                const xmlDoc = parser.parseFromString(response.responseText, "text/xml");
                const ds = xmlDoc.getElementsByTagName('d');

                const comments = [];
                for (let d of ds) {
                    const text = d.textContent;
                    if (keyword && !text.includes(keyword)) continue;

                    const p = d.getAttribute('p').split(',');
                    comments.push({
                        hash: p[6],
                        ts: parseInt(p[4]) * 1000,
                        text: text
                    });
                }

                // 使用优化后的哈希转换模块
                const midcrc = new BiliBili_midcrc();
                const midBatch = midcrc.batchConvert(comments.map(comment => comment.hash))

                const results = comments.map((comment, idx) => ({
                    mid: midBatch[idx],
                    date: new Date(comment.ts).toLocaleString(),
                    text: comment.text
                })).filter(comment => comment.mid); // 过滤无效结果

                console.log('解析结果:', results)
                showResults(results);
            },
            onerror: function (err) {
                alert('获取弹幕失败:' + err.statusText);
            }
        });
    }

    // 初始化
    function init() {
        createUI();

        document.getElementById('startSearch').addEventListener('click', () => {
            const keyword = document.getElementById('keywordInput').value.trim();
            main(keyword || undefined);
        });
    }

    // 启动脚本
    init();
})();