Greasy Fork is available in English.

Bilibili弹幕查询发送者

bilibili(b站/哔哩哔哩)根据弹幕查询发送者信息

"use strict";
// ==UserScript==
// @name         Bilibili弹幕查询发送者
// @namespace    https://github.com/qianjiachun
// @version      2024.01.15.01
// @icon         https://static.hdslb.com/mobile/img/512.png
// @description  bilibili(b站/哔哩哔哩)根据弹幕查询发送者信息
// @author       小淳
// @match        *://www.bilibili.com/video/*
// @match        *://www.bilibili.com/festival/*
// @match        *://www.bilibili.com/bangumi/play/*
// @match        *://www.bilibili.com/cheese/play/*
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @require      https://lib.baomitu.com/protobufjs/6.11.2/protobuf.min.js
// @connect      bilibili.com
// @run-at       document-start
// @license      MIT
// ==/UserScript==

unsafeWindow.requestHookList = [];
unsafeWindow.requestHookCallback = function (xhr) {
	if (xhr.responseURL.includes("/seg.so")) {
		let data = new Uint8Array(xhr.response);
		protobuf.loadFromString("dm", protoStr).then(root => {
				let dmList = root.lookupType("dm.dmList").decode(data);
				handleDanmakuList(dmList.list);
		})
	}
};

var originalOpen = XMLHttpRequest.prototype.open;
var originalSend = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.open = function () {
  this._url = arguments[1];
  originalOpen.apply(this, arguments);
};

XMLHttpRequest.prototype.send = function () {
  var self = this;
  this.addEventListener("load", function () {
    if (self.readyState === 4 && self.status === 200) {
      unsafeWindow.requestHookList.push(self);
      unsafeWindow.requestHookCallback(self);
    }
  });
  originalSend.apply(this, arguments);
};


function init() {
	init_Router();
}

function initStyles() {
	let style = document.createElement("style");
	style.appendChild(document.createTextNode(`.senderinfo__wrap {    width: 280px;    min-height: 110px;    height: auto;    z-index: 1;    background-color: white;    border-radius: 8px;    box-shadow: 0 0 30px 2px rgb(0 0 0 / 10%);    position: absolute;    left: 50%;    top: 50%;    transform: translate(-50%, -50%);    max-height: 300px;    box-sizing: border-box;    padding: 5px;    overflow: auto;}.senderinfo__card {    margin-bottom: 5px;    margin-top: 5px;}.senderinfo__github {    width: 16px;    height: 16px;    position: absolute;}.senderinfo__close {    margin-right: 5px;    margin-top: 5px;    cursor: pointer;    position: absolute;    margin-left: 260px;    margin-top: 0px;}.senderinfo__avatar {    width: 100%;    height: 70px;    overflow: hidden;    text-align: center;}.senderinfo__img-loding {    width: 70px;    height: 70px;    border-radius: 50%;    background-color: rgb(225,232,238);    display: inline-block;}.senderinfo__avatar img {    width: 70px;    height: 70px;    border-radius: 50%;}.senderinfo__user {    text-align: center;    margin-top: 10px;}.senderinfo__name {    font-size: 16px;    font-weight: bold;    color: black;}.senderinfo__name-loading {    width: 100px;    height: 16px;    background-color: rgb(225,232,238);    display: inline-block;}.senderinfo__level {    line-height: 17px;    margin-left: 5px;    position: absolute;    color: #99a2aa;}.senderinfo__sign {    color: #99a2aa;    word-break: break-all;    word-wrap: break-word;    margin-top: 10px;    text-align: center;    line-height: 12px;}.senderinfo__sign-loading {    width: 150px;    height: 16px;    background-color: rgb(225,232,238);    display: inline-block;}.senderinfo__wrap::-webkit-scrollbar {    width: 4px;    }.senderinfo__wrap::-webkit-scrollbar-thumb {    border-radius: 10px;    box-shadow: inset 0 0 5px rgba(0,0,0,0.2);    background: rgba(0,0,0,0.2);}.senderinfo__wrap::-webkit-scrollbar-track {    box-shadow: inset 0 0 5px rgba(0,0,0,0.2);    border-radius: 0;    background: rgba(0,0,0,0.1);}`));
	document.head.appendChild(style);
}

let allDanmaku = {}

const DOM_MENU_MAIN = ".player-auxiliary-context-menu-container"
const DOM_MENU_BANGUMI = ".bpx-player-contextmenu.bpx-player-active"
const DOM_MENU_CHEESE = ".bpx-player-contextmenu.bpx-player-active"


function formatSeconds(value) {
	var secondTime = parseInt(value / 1000); // 秒
	var minuteTime = 0; // 分
	if (secondTime > 60) {
		minuteTime = parseInt(secondTime / 60);
		secondTime = parseInt(secondTime % 60);
	}
	var result = "" + (parseInt(secondTime) < 10 ? "0" + parseInt(secondTime) : parseInt(secondTime));

	// if (minuteTime > 0) {
	result = "" + (parseInt(minuteTime) < 10 ? "0" + parseInt(minuteTime) : parseInt(minuteTime)) + ":" + result;
	// }
	return result;
}

function toSecond(e) {
	var time = e;
	var len = time.split(':')
	let min = "";
	let hour = "";
	let sec = "";
	if (len.length == 3) {
		hour = time.split(':')[0];
		min = time.split(':')[1];
		sec = time.split(':')[2];
		return Number(hour * 3600) + Number(min * 60) + Number(sec);
	}
	if (len.length == 2) {
		min = time.split(':')[0];
		sec = time.split(':')[1];
		return Number(min * 60) + Number(sec);
	}
	if (len.length == 1) {
		sec = time.split(':')[0];
		return Number(sec);
	}

	// var hour = time.split(':')[0];
	// var min = time.split(':')[1];
	// var sec = time.split(':')[2];
	// return  Number(hour*3600) + Number(min*60) + Number(sec);
}


function getStrMiddle(str, before, after) {
	let m = str.match(new RegExp(before + '(.*?)' + after));
	return m ? m[1] : false;
}
let protoStr = `
syntax = "proto3";

package dm;

message dmList{
    repeated dmItem list=1;
}
message dmItem{
    int64 id = 1;
    int32 progress = 2;
    int32 mode = 3;
    int32 fontsize = 4;
    uint32 color = 5;
    string midHash = 6;
    string content = 7;
    int64 ctime = 8;
    int32 weight = 9;
    string action = 10;
    int32 pool = 11;
    string idStr = 12;
}`;
let videoCid = "";


function initPkg_CollectAllDanmaku() {
    initPkg_CollectAllDanmaku_Dom();
    initPkg_CollectAllDanmaku_Func();
}

function initPkg_CollectAllDanmaku_Dom() {
}  

function initPkg_CollectAllDanmaku_Func() {
    collectAllDanmaku(1);
}

function collectAllDanmaku(page) {
    if (page > 30) {
        // 熔断
        return;
    }
    fetch(
        `https://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid=${videoCid}&segment_index=${page}`
    ).then(response => {
        return response.arrayBuffer();
    }).then(ret => {
        let data = new Uint8Array(ret);
        protobuf.loadFromString("dm", protoStr).then(root => {
            let dmList = root.lookupType("dm.dmList").decode(data);
            handleDanmakuList(dmList.list);
        })
        if (ret.byteLength > 0) {
            collectAllDanmaku(page + 1);
        }
    }).catch(err => {
        console.log(err);
    })
}

function handleDanmakuList(list) {
    for (let i = 0; i < list.length; i++) {
        let item = list[i];
        let content = item.content;
        let progress = "progress" in item ? item.progress : 0;
        let keyName = `${content}|${parseInt(progress / 1000)}`;
        if (keyName in allDanmaku) {
            allDanmaku[keyName].push(item.midHash);
        } else {
            allDanmaku[keyName] = [item.midHash];
        }
    }
}

async function refreshAllDanmaku() {
    let route = getRoute();
    switch (route) {
        case 0:
            // 在普通页面
            videoCid = getVideoCid_Main();
            initPkg_CollectAllDanmaku();
            break;
        case 1:
            // 在番剧页面
            videoCid = getVideoCid_Bangumi();
            initPkg_CollectAllDanmaku();
            break;
        case 2:
            // 在课程页面
            videoCid = await getVideoCid_Cheese();
            initPkg_CollectAllDanmaku();
            break;
        default:
            videoCid = getVideoCid_Main();
            initPkg_CollectAllDanmaku();
            break;
    }
}
function initPkg_Main() {
    initPkg_Main_Dom();
    initPkg_Main_Func();
}

function initPkg_Main_Dom() {
    
}

function initPkg_Main_Func() {
    let selectedDom = null;
    document.getElementById("danmukuBox").addEventListener("contextmenu", (e) => {
        let path = e.path || (e.composedPath && e.composedPath());
        setTimeout(() => {
            selectedDom = getSelectedDom(path);
            let dom = document.querySelector(DOM_MENU_MAIN) || document.querySelector(DOM_MENU_BANGUMI) || document.querySelector(DOM_MENU_CHEESE);
            if (dom) {
                if (dom.querySelector("#query-sender")) {
                    return;
                }
                removeSenderInfoWrap();
                
                let ul = dom.querySelector("ul");
                let li = document.createElement("li");
                li.id = "query-sender";
                li.className = "context-line context-menu-function";
                li.innerHTML = `
                <a style="color:#444" class="context-menu-a js-action" href="javascript:void(0);" data-disabled="0">
                    查看发送者
                </a>`;
                if (ul) {
                    ul.appendChild(li);
                } else {
                    dom.appendChild(li);
                }

                li.addEventListener("click", () => {
                    if (selectedDom) {
                        renderSenderInfoWrap();
                        showSelectedInfo(selectedDom);
                    }
                })
            }
        }, 0);
    }, true)
}

function getSelectedDom(path) {
    let ret = null;
    for (let i = 0; i < path.length; i++) {
        if (path[i].className && (path[i].className.includes("danmaku-info-row") || path[i].className.includes("dm-info-row"))) {
            ret = path[i];
            break;
        }
    }
    return ret;
}

function showSelectedInfo(dom) {
    let domTime = dom.getElementsByClassName("danmaku-info-time")[0];
    let domContent = dom.getElementsByClassName("danmaku-info-danmaku")[0];
    let progress = domTime ? domTime.innerText :dom.getElementsByClassName("dm-info-time")[0].innerText;
    let content = domContent ? domContent.title : dom.getElementsByClassName("dm-info-dm")[0].title;
    let keyName = `${content}|${toSecond(progress)}`;
    let uidList = [];
    if (keyName in allDanmaku) {
        for (let i = 0; i < allDanmaku[keyName].length; i++) {
            let uhash = allDanmaku[keyName][i];
            let list = uhash2uid(uhash);
            uidList.push(...list);
        }
        renderSenderInfoCard(uidList);
    }
}

function renderSenderInfoWrap() {
    removeSenderInfoWrap();
    let div = document.createElement("div");
    div.className = "senderinfo__wrap";
    div.innerHTML = `
    <div class="senderinfo__close">X</div>
    <a title="点个Star吧~" href="https://github.com/qianjiachun/bilibili-danmaku-tracker" target="_blank" class="senderinfo__github"><svg t="1639304975096" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2323" width="16" height="16"><path d="M512 42.666667A464.64 464.64 0 0 0 42.666667 502.186667 460.373333 460.373333 0 0 0 363.52 938.666667c23.466667 4.266667 32-9.813333 32-22.186667v-78.08c-130.56 27.733333-158.293333-61.44-158.293333-61.44a122.026667 122.026667 0 0 0-52.053334-67.413333c-42.666667-28.16 3.413333-27.733333 3.413334-27.733334a98.56 98.56 0 0 1 71.68 47.36 101.12 101.12 0 0 0 136.533333 37.973334 99.413333 99.413333 0 0 1 29.866667-61.44c-104.106667-11.52-213.333333-50.773333-213.333334-226.986667a177.066667 177.066667 0 0 1 47.36-124.16 161.28 161.28 0 0 1 4.693334-121.173333s39.68-12.373333 128 46.933333a455.68 455.68 0 0 1 234.666666 0c89.6-59.306667 128-46.933333 128-46.933333a161.28 161.28 0 0 1 4.693334 121.173333A177.066667 177.066667 0 0 1 810.666667 477.866667c0 176.64-110.08 215.466667-213.333334 226.986666a106.666667 106.666667 0 0 1 32 85.333334v125.866666c0 14.933333 8.533333 26.88 32 22.186667A460.8 460.8 0 0 0 981.333333 502.186667 464.64 464.64 0 0 0 512 42.666667" p-id="2324"></path></svg></a>
    <div style="display:flex;justify-content:center;">请先左键选中弹幕再右键查询</div>
    <div class="senderinfo__content">
        <div class="senderinfo__loading">
            <div class="senderinfo__card">
                <div class="senderinfo__avatar">
                    <div class="senderinfo__img-loding"></div>
                </div>
                <div class="senderinfo__user">
                    <span class="senderinfo__name-loading"></span>
                </div>
                <div class="senderinfo__sign">
                    <span class="senderinfo__sign-loading"></span>
                </div>
            </div>
        </div>
    </div>
    `
    let b = document.getElementsByClassName("bui-collapse-wrap")[0];
    b.insertBefore(div, b.childNodes[0]);

    document.getElementsByClassName("senderinfo__close")[0].addEventListener("click", () => {
        div.remove();
    })
}

function renderSenderInfoCard(uidList) {
    let domCard = document.getElementsByClassName("senderinfo__content")[0];
    if (!domCard) {
        return;
    }
    let domLoading = document.getElementsByClassName("senderinfo__loading")[0];
    for (let i = 0; i < uidList.length; i++) {
        let uid = uidList[i];
        // fetch(`https://api.bilibili.com/x/space/acc/info?mid=${uid}&token=&platform=web&jsonp=jsonp`)
        // .then(res => res.json())
        // .then(ret => {
        //     const {data} = ret;
        //     domLoading.style.display = "none";
        //     let head = data.face;
        //     let name = data.name;
        //     let sign = data.sign
        //     // 此时arr[0]为名字 arr[1]为签名
        //     let html = `
        //         <div class="senderinfo__card">
        //             <div class="senderinfo__avatar">
        //                 <a href="https://space.bilibili.com/${uid}" target="_blank"><img src="${head}" /></a>
        //             </div>
        //             <div class="senderinfo__user">
        //                 <a href="https://space.bilibili.com/${uid}" target="_blank"><span class="senderinfo__name">${name}</span></a>
        //             </div>
        //             <div class="senderinfo__sign">${sign}</div>
        //         </div>
        //     `
        //     domCard.innerHTML += html;
        // })
        GM_xmlhttpRequest({
            method: "GET",
            url: "https://m.bilibili.com/space/" + uid,
            headers: {
                "cookie": document.cookie,
                "user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/105.0.0.0"
            },
            responseType: "text",
            onload: function(response) {
                domLoading.style.display = "none";
                let ret = response.response;
                let parser = new DOMParser();
                let doc = parser.parseFromString(ret, "text/html");
                if (!doc) return;
                let name = String(getStrMiddle(ret, `<title data-vue-meta="true">`, "的个人空间"));
                let head = String(doc.querySelector(".m-space-info").querySelector(".face").querySelector("img").src);
                let sign = String(doc.querySelector(".desc").querySelector(".content").innerHTML);
                if (!name || name === "" || name === "false") return;
                let html = `
                    <div class="senderinfo__card">
                        <div class="senderinfo__avatar">
                            <a href="https://space.bilibili.com/${uid}" target="_blank"><img src="${head}" /></a>
                        </div>
                        <div class="senderinfo__user">
                            <a href="https://space.bilibili.com/${uid}" target="_blank"><span class="senderinfo__name">${name}</span></a>
                        </div>
                        <div class="senderinfo__sign">${sign}</div>
                    </div>
                `
                domCard.innerHTML += html;
            }
        });
    }
}

function removeSenderInfoWrap() {
    let domWrapList = document.getElementsByClassName("senderinfo__wrap");
    if (domWrapList.length > 0) {
        domWrapList[0].remove();
    }
}
function make_crc32_cracker() {
    var POLY = 0xedb88320;
    var crc32_table = new Uint32Array(256);
    function make_table() {
        for (var i = 0; i < 256; i++) {
            var crc = i;
            for (var _ = 0; _ < 8; _++) {
                if (crc & 1) {
                    crc = ((crc >>> 1) ^ POLY) >>> 0;
                } else {
                    crc = crc >>> 1;
                }
            }

            crc32_table[i] = crc;
        }
    }
    make_table();
    function update_crc(by, crc) {
        return ((crc >>> 8) ^ crc32_table[(crc & 0xff) ^ by]) >>> 0;
    }
    function compute(arr, init) {
        var crc = init || 0;
        for (var i = 0; i < arr.length; i++) {
            crc = update_crc(arr[i], crc);
        }
        return crc;
    }
    function make_rainbow(N) {
        var rainbow = new Uint32Array(N);
        for (var i = 0; i < N; i++) {
            var arr = [].slice.call(i.toString()).map(Number);
            rainbow[i] = compute(arr);
        }
        return rainbow;
    }
    var rainbow_0 = make_rainbow(100000);
    var five_zeros = Array(5).fill(0);
    var rainbow_1 = rainbow_0.map(function (crc) {
        return compute(five_zeros, crc);
    });
    var rainbow_pos = new Uint32Array(65537);
    var rainbow_hash = new Uint32Array(200000);
    function make_hash() {
        for (var i = 0; i < rainbow_0.length; i++) {
            rainbow_pos[rainbow_0[i] >>> 16]++;
        }
        for (var i = 1; i <= 65536; i++) {
            rainbow_pos[i] += rainbow_pos[i - 1];
        }
        for (var i = 0; i <= rainbow_0.length; i++) {
            var po = --rainbow_pos[rainbow_0[i] >>> 16];
            rainbow_hash[po << 1] = rainbow_0[i];
            rainbow_hash[po << 1 | 1] = i;
        }
    }
    function lookup(crc) {
        var results = [];
        var first = rainbow_pos[crc >>> 16],
            last = rainbow_pos[1 + (crc >>> 16)];
        for (var i = first; i < last; i++) {
            if (rainbow_hash[i << 1] == crc)
                results.push(rainbow_hash[i << 1 | 1]);
        }
        return results;
    }
    make_hash();
    function crack(maincrc, max_digit) {
        var results = [];
        maincrc = (~maincrc) >>> 0;
        var basecrc = 0xffffffff;
        for (var ndigits = 1; ndigits <= max_digit; ndigits++) {
            basecrc = update_crc(0x30, basecrc);
            if (ndigits < 6) {
                var first_uid = Math.pow(10, ndigits - 1),
                    last_uid = Math.pow(10, ndigits);
                for (var uid = first_uid; uid < last_uid; uid++) {
                    if (maincrc == ((basecrc ^ rainbow_0[uid]) >>> 0)) {
                        results.push(uid);
                    }
                }
            } else {
                var first_prefix = Math.pow(10, ndigits - 6);
                var last_prefix = Math.pow(10, ndigits - 5);
                for (var prefix = first_prefix; prefix < last_prefix; prefix++) {
                    var rem = (maincrc ^ basecrc ^ rainbow_1[prefix]) >>> 0;
                    var items = lookup(rem);
                    items.forEach(function (z) {
                        results.push(prefix * 100000 + z);
                    })
                }
            }
        }
        return results;
    }
    return {
        crack: crack
    };
}

function uhash2uid(uidhash, max_digit = 10) {
    let _crc32_cracker = null;
    _crc32_cracker = _crc32_cracker || make_crc32_cracker();
    return _crc32_cracker.crack(parseInt(uidhash, 16), max_digit);
}
function getVideoCid_Bangumi() {
    return String(unsafeWindow.__INITIAL_STATE__.epInfo.cid);
}

function getVideoCid_Cheese() {
    // let episodes = unsafeWindow.PlayerAgent.getEpisodes();
    // let _id = unsafeWindow.$('li.on.list-box-li').index();
    // return String(episodes[_id].cid);
    // let cid = "";
    // while (cid === "") {
    //     if (window.bpNC_1) {
    //         console.log(window.bpNC_1)
    //         cid = window.bpNC_1.config.cid;
    //     }
    // }
    return new Promise(resolve => {
       let timer = setInterval(() => {
        if (unsafeWindow.bpNC_1) {
            clearInterval(timer);
            resolve(unsafeWindow.bpNC_1.config.cid);
        }
       }, 1000); 
    });
    // return cid;
}

function getVideoCid_Main() {
    let cidMap = unsafeWindow.__INITIAL_STATE__.cidMap;
    let keys = Object.keys(cidMap);
    if (keys.length > 0) {
        let cids = cidMap[keys[0]].cids;
        let cidsKeys = Object.keys(cids);
        if (cidsKeys.length > 0) {
            return String(cids[cidsKeys[0]]);
        } else {
            return "";
        }
    } else {
        return "";
    }
}

protobuf.loadFromString = (name, protoStr) => {
    const Root = protobuf.Root;
    const fetchFunc = Root.prototype.fetch;
    Root.prototype.fetch = (_, cb) => cb(null, protoStr);
    const root = new Root().load(name);
    Root.prototype.fetch = fetchFunc;
    return root;
};
function init_Router() {
    // refreshAllDanmaku();
    initPkg_Main();
}

function getRoute() {
    // 规定 0是默认页面 1是番剧bangumi页面 2是cheese课程页面
    let ret = 0;
    let url = String(location.href);
    if (url.includes("bangumi/play")) {
        // 在番剧页面
        ret = 1;
    } else if (url.includes("cheese/play")) {
        // 在课程页面
        ret = 2;
    }
    return ret;
}

const _historyWrap = function (type) {
	const orig = history[type];
	const e = new Event(type);
	return function () {
		const rv = orig.apply(this, arguments);
		e.arguments = arguments;
		window.dispatchEvent(e);
		return rv;
	};
};
history.pushState = _historyWrap('pushState');
history.replaceState = _historyWrap('replaceState');

window.addEventListener('pushState', refreshAllDanmaku);
window.addEventListener('replaceState', refreshAllDanmaku);
window.addEventListener('hashchange', refreshAllDanmaku);
window.addEventListener('popstate', refreshAllDanmaku);
(async function () {
	let timer = setInterval(() => {
		let dom = document.getElementById("danmukuBox");
		if (dom) {
			clearInterval(timer);
			initStyles();
			init();
		}
	}, 500);
})();