"use strict"; // ==UserScript== // @name Bilibili弹幕查询发送者 // @namespace https://github.com/qianjiachun // @version 2024.12.12.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 // @downloadURL none // ==/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 = ` 查看发送者 `; 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 = `