A userscript + websocket to observe and record Bilibili live danmakus.
// ==UserScript==
// @name ayase
// @namespace https://github.com/Vincent-the-gamer/ayase
// @version 0.1.0
// @author Vincent-the-gamer
// @description A userscript + websocket to observe and record Bilibili live danmakus.
// @license https://github.com/Vincent-the-gamer/ayase/blob/main/LICENSE.md
// @icon https://img.moegirl.org.cn/common/6/61/%E4%B8%89%E5%8F%B8%E7%BB%AB%E6%BF%91_%E8%A7%92%E8%89%B2%E6%AD%8C%E4%B8%93%E8%BE%91%E5%B0%81%E9%9D%A2.jpg
// @match https://live.bilibili.com/*
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
const d=new Set;const importCSS = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):document.head.appendChild(document.createElement("style")).append(t);})(e));};
function setupObserver(element, serverLink) {
const startObserver = () => {
try {
const ws = new WebSocket(serverLink);
const observer2 = new MutationObserver((mutations, _) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
const addedNodes = Array.from(mutation.addedNodes);
const node = addedNodes[0];
const danmaku = {
uname: node.querySelector("span.user-name")?.innerHTML,
text: node.getAttribute("data-danmaku"),
img: "",
replacement: ""
};
const emoticon = node.querySelector("span.emoticon");
if (emoticon) {
danmaku.img = emoticon.querySelector("img.open-menu")?.getAttribute("src");
danmaku.replacement = emoticon.querySelector("span.open-menu")?.innerHTML;
}
ws.send(
JSON.stringify(danmaku)
);
}
});
});
const config = {
attributes: false,
childList: true,
subtree: true
};
const danmakuDOMList = document.querySelector(".chat-history-list");
if (danmakuDOMList) {
observer2.observe(danmakuDOMList, config);
}
alert("WebSocket连接: " + serverLink);
return observer2;
} catch (e) {
alert("WebSocket连接错误: " + e);
}
};
let observer;
element.addEventListener("click", () => {
if (observer) {
observer.disconnect();
}
observer = startObserver();
});
}
const styleCss = ".config{position:fixed;display:flex;flex-direction:row;justify-content:center;align-items:center;gap:7px;top:5px;right:5px;width:fit-content;height:fit-content;border-radius:10px;padding:8px;z-index:1000;background:#40e0d0}.config img{width:30px;height:30px}.config input{width:200px;height:20px}.config button{background-color:#000;color:#fff;height:25px;border-radius:3px}.config button:hover{background-color:orange}";
importCSS(styleCss);
(() => {
const app = document.createElement("div");
document.body.append(app);
return app;
})().innerHTML = `
<div class="config" id="ayase-app">
<img src="https://i0.hdslb.com/bfs/article/eba9c4eeae160d5f72edf1d0c1eb409a3dd8f4e7.png"/>
<span>WebSocket地址: </span>
<input id="ayase-link" type="text" value="ws://localhost:8081/websocket"/>
<button id="start-ayase">连接</button>
</div>
`;
const input = document.querySelector("#ayase-link");
input.addEventListener("change", (event) => {
setupObserver(
document.querySelector("#start-ayase"),
event.target.value
);
});
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址