- // ==UserScript==
- // @name Vecorder
- // @namespace https://www.joi-club.cn/
- // @version 0.80
- // @description 直播间内容记录 https://github.com/Xinrea/Vecorder
- // @author Xinrea
- // @license MIT
- // @match https://live.bilibili.com/*
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_deleteValue
- // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
- // @require https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.min.js
- // @run-at document-end
- // ==/UserScript==
-
- function vlog(msg) {
- console.log("[Vecorder]" + msg);
- }
-
- function p(msg) {
- return {
- time: new Date().getTime(),
- content: msg,
- };
- }
-
- var dbname = "vdb" + getRoomID();
-
- var db = JSON.parse(GM_getValue(dbname, "[]"));
- var Option = JSON.parse(GM_getValue("vop", '{"reltime":false,"toffset":0}'));
-
- function nindexOf(n) {
- for (let i in db) {
- if (!db[i].del && db[i].name == n) return i;
- }
- return -1;
- }
-
- function tindexOf(id, t) {
- for (let i in db[id].lives) {
- if (!db[id].lives[i].del && db[id].lives[i].title == t) return i;
- }
- return -1;
- }
-
- function gc() {
- for (let i = db.length - 1; i >= 0; i--) {
- if (db[i].del) {
- db.splice(i, 1);
- continue;
- }
- for (let j = db[i].lives.length - 1; j >= 0; j--) {
- if (db[i].lives[j].del) {
- db[i].lives.splice(j, 1);
- continue;
- }
- }
- }
- GM_setValue(dbname, JSON.stringify(db));
- }
-
- function addPoint(t, msg) {
- let ltime = t * 1000;
- if (ltime == 0) return;
- let [name, link, title] = getRoomInfo();
- let id = nindexOf(name);
- if (id == -1) {
- db.push({
- name: name,
- link: link,
- del: false,
- lives: [
- {
- title: title,
- time: ltime,
- del: false,
- points: [p(msg)],
- },
- ],
- });
- } else {
- let lid = tindexOf(id, title);
- if (lid == -1) {
- db[id].lives.push({
- title: title,
- time: ltime,
- points: [p(msg)],
- });
- } else {
- db[id].lives[lid].points.push(p(msg));
- }
- }
- GM_setValue(dbname, JSON.stringify(db));
- }
-
- function getMsg(body) {
- var vars = body.split("&");
- for (var i = 0; i < vars.length; i++) {
- var pair = vars[i].split("=");
- if (pair[0] == "msg") {
- return decodeURI(pair[1]);
- }
- }
- return false;
- }
-
- function getRoomInfo() {
- let resp = $.ajax({
- url: "https://api.live.bilibili.com/xlive/web-room/v1/index/getH5InfoByRoom?room_id="+getRoomID(),
- async: false}).responseJSON.data;
- console.log(resp)
- return [resp.anchor_info.base_info.uname, "https://space.bilibili.com/" + resp.room_info.uid, resp.room_info.title]
- }
-
- // 根据当前地址获取直播间ID
- function getRoomID() {
- var url_text = window.location.href + "";
- var i = url_text.indexOf("roomid=");
- var m = 0;
- if (i != -1) m = url_text.slice(i + 7);
- else m = url_text.slice(url_text.indexOf(".com/") + 5);
- let rid = parseInt(m);
- console.log("Try1:",m);
- if (isNaN(rid)) {
- m = url_text.slice(url_text.indexOf(".com/blanc/") + 11);
- console.log("Try2:"+m);
- rid = parseInt(m);
- }
- return rid; //获取当前房间号
- }
-
- function tryAddPoint(msg) {
- // https://api.live.bilibili.com/room/v1/Room/room_init?id={roomID}
- console.log(msg, getRoomID());
- $.ajax({
- url:
- "https://api.live.bilibili.com/room/v1/Room/room_init?id=" + getRoomID(),
- async: true,
- success: function (resp) {
- let t = 0;
- if (resp.data.live_status != 1) t = 0;
- else t = resp.data.live_time;
- addPoint(t, msg);
- },
- });
- }
-
- let toggle = false;
-
- waitForKeyElements(
- "#control-panel-ctnr-box > div.chat-input-ctnr.p-relative",
- (n) => {
- let recordInput = $(
- '<textarea id="vecorder-input" placeholder="在此记录当前直播内容,回车确认" rows="1"></textarea>'
- );
- recordInput.attr(
- "style",
- `
- height: 12px;
- width: 266px;
- resize: none;
- outline: none;
- background-color: #fff;
- border-radius: 4px;
- padding: 8px 8px 10px;
- color: #333;
- overflow: hidden;
- font-size: 10px;
- line-height: 14px;
- display: flex;
- border: 1px solid #e9eaec;
- margin-top: 3px;
- `
- );
- recordInput.bind("keypress", function (event) {
- if (event.keyCode == "13") {
- window.event.returnValue = false;
- console.log("Enter detected");
- tryAddPoint($("#vecorder-input").val());
- $("#vecorder-input").val("");
- }
- });
- n.after(recordInput);
- }
- );
-
- waitForKeyElements(
- "#chat-control-panel-vm > div > div.bottom-actions.p-relative > div",
- (n) => {
- // Resize original input
- $(
- "#control-panel-ctnr-box > div.chat-input-ctnr.p-relative > div:nth-child(2) > textarea"
- ).css("height", "36px");
- $(
- "#control-panel-ctnr-box > div.chat-input-ctnr.p-relative > div.medal-section"
- ).css("height", "36px");
- $("#control-panel-ctnr-box > div.bottom-actions.p-relative").css(
- "margin-top",
- "4px"
- );
- // Setup recordButton
- let recordBtn = $('<button><span class="txt">记录</span></button>');
- recordBtn.attr(
- "style",
- "font-family: sans-serif;\
- text-transform: none;\
- position: relative;\
- box-sizing: border-box;\
- line-height: 1;\
- margin: 0;\
- margin-left: 3px;\
- padding: 6px 12px;\
- border: 0;\
- cursor: pointer;\
- outline: none;\
- overflow: hidden;\
- background-color: #23ade5;\
- color: #fff;\
- border-radius: 4px;\
- min-width: 40px;\
- height: 24px;\
- font-size: 12px;"
- );
- recordBtn.hover(
- function () {
- if (!toggle) recordBtn.css("background-color", "#58bae2");
- },
- function () {
- if (!toggle) recordBtn.css("background-color", "#23ade5");
- }
- );
- recordBtn.click(function () {
- if (toggle) {
- $("#vPanel").remove();
- gc();
- toggle = false;
- $(this).css("background-color", "#58bae2");
- return;
- }
- let panel = $(
- '<div id="vPanel"><div id="vArrow"></div><p style="font-size:20px;font-weight:bold;margin:0px;" class="vName">🍊直播笔记</p></div>'
- );
- let contentList = dbToListview();
- panel.append(contentList);
- let clearBtn = $('<button><span class="txt">清空</span></button>');
- clearBtn.attr(
- "style",
- "font-family: sans-serif;\
- text-transform: none;\
- position: relative;\
- box-sizing: border-box;\
- line-height: 1;\
- margin: 0;\
- margin-left: 3px;\
- padding: 6px 12px;\
- border: 0;\
- cursor: pointer;\
- outline: none;\
- overflow: hidden;\
- background-color: #23ade5;\
- color: #fff;\
- border-radius: 4px;\
- min-width: 40px;\
- height: 24px;\
- font-size: 12px;"
- );
- clearBtn.hover(
- function () {
- clearBtn.css("background-color", "#58bae2");
- },
- function () {
- clearBtn.css("background-color", "#23ade5");
- }
- );
- clearBtn.click(function () {
- contentList.empty();
- db = [];
- GM_deleteValue(dbname);
- });
- panel.append(clearBtn);
- let closeBtn = $(
- '<a style="position:absolute;right:7px;top:5px;font-size:20px;" class="vName">×</a>'
- );
- closeBtn.click(function () {
- panel.remove();
- gc();
- toggle = false;
- recordBtn.css("background-color", "#23ade5");
- });
- panel.append(closeBtn);
- let timeop = $(`<hr style="border:0;height:1px;background-color:#58bae2;margin-top:10px;margin-bottom:10px;"/><div id="timeop">\
- <div><input type="checkbox" id="reltime" value="false" style="vertical-align:middle;margin-right:5px;"/><label for="reltime" class="vName" style="vertical-align:middle;">按相对时间导出</label></div>\
- <div style="margin-top:10px;"><label for="toffset" class="vName" style="vertical-align:middle;">时间偏移(秒):</label><input type="number" id="toffset" value="${Option.toffset}" style="vertical-align:middle;width:35px;outline-color:#23ade5;"/></div>\
- </div>`);
- panel.append(timeop);
- $("#chat-control-panel-vm > div").append(panel);
- if (Option.reltime) {
- $("#reltime").attr("checked", true);
- }
- $("#reltime").change(function () {
- Option.reltime = $(this).prop("checked");
- GM_setValue("vop", JSON.stringify(Option));
- });
- $("#toffset").change(function () {
- Option.toffset = $(this).val();
- GM_setValue("vop", JSON.stringify(Option));
- });
- $(this).css("background-color", "#0d749e");
- toggle = true;
- });
- n.append(recordBtn);
-
- let styles = $("<style></style>");
- styles.text(
- "#vPanel {\
- line-height: 1.15;\
- font-size: 12px;\
- font-family: Arial,Microsoft YaHei,Microsoft Sans Serif,Microsoft SanSerf,\\5FAE8F6F96C59ED1!important;\
- display: block;\
- box-sizing: border-box;\
- background: #fff;\
- border: 1px solid #e9eaec;\
- border-radius: 8px;\
- box-shadow: 0 6px 12px 0 rgba(106,115,133,.22);\
- animation: scale-in-ease cubic-bezier(.22,.58,.12,.98) .4s;\
- padding: 16px;\
- position: absolute;\
- right: 0px;\
- bottom: 50px;\
- z-index: 999;\
- transform-origin: right bottom;\
- }\
- #vPanel ul {\
- list-style-type: none;\
- padding-inline-start: 0px;\
- color: #666;\
- }\
- #vPanel li {\
- margin-top: 10px;\
- white-space: nowrap;\
- }\
- .vName {\
- color: #23ade5;\
- cursor: pointer;\
- }\
- #vArrow {\
- line-height: 1.15;\
- font-size: 12px;\
- font-family: Arial,Microsoft YaHei,Microsoft Sans Serif,Microsoft SanSerf,\\5FAE8F6F96C59ED1!important;\
- position: absolute;\
- top: 100%;\
- width: 0;\
- height: 0;\
- border-left: 4px solid transparent;\
- border-right: 4px solid transparent;\
- border-top: 8px solid #fff;\
- right: 25px;\
- }\
- "
- );
- n.append(styles);
- }
- );
-
- function dbToListview() {
- let urlObject = window.URL || window.webkitURL || window;
- let content = $("<ul></ul>");
- for (let i in db) {
- let list = $("<li></li>");
- if (db[i].del) {
- continue;
- }
- let innerlist = $("<ul></ul>");
- for (let j in db[i].lives) {
- if (db[i].lives[j].del) continue;
- let item = $(
- "<li>" +
- `[${moment(db[i].lives[j].time).format("YYYY/MM/DD")}]` +
- db[i].lives[j].title +
- "[" +
- db[i].lives[j].points.length +
- "]" +
- "</li>"
- );
- let ep = $('<a class="vName" style="font-weight:bold;">[导出]</a>');
- let cx = $(
- '<a class="vName" style="color:red;font-weight:bold;">[删除]</a>'
- );
- ep.click(function () {
- exportRaw(
- db[i].lives[j],
- db[i].name,
- `[${db[i].name}][${db[i].lives[j].title}][${moment(
- db[i].lives[j].time
- ).format("YYYY-MM-DD")}]`
- );
- });
- cx.click(function () {
- if (db[i].lives.length == 1) {
- db[i].del = true;
- item.remove();
- list.remove();
- } else {
- db[i].lives[j].del = true;
- item.remove();
- }
- GM_setValue(dbname, JSON.stringify(db));
- });
- item.append(ep);
- item.prepend(cx);
- innerlist.append(item);
- }
- list.append(innerlist);
- content.append(list);
- }
- return content;
- }
-
- function exportRaw(live, v, fname) {
- var urlObject = window.URL || window.webkitURL || window;
- var export_blob = new Blob([rawToString(live, v)]);
- var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
- save_link.href = urlObject.createObjectURL(export_blob);
- save_link.download = fname;
- save_link.click();
- }
-
- function rawToString(live, v) {
- let r =
- "# 由Vecorder自动生成,不妨关注下可爱的@轴伊Joi_Channel:https://space.bilibili.com/61639371/\n";
- r += `# ${v} \n`;
- r += `# ${live.title} - 直播开始时间:${moment(live.time).format(
- "YYYY-MM-DD HH:mm:ss"
- )}\n\n`;
- for (let i in live.points) {
- if (!Option.reltime)
- r += `[${moment(live.points[i].time)
- .add(Option.toffset, "seconds")
- .format("HH:mm:ss")}] ${live.points[i].content}\n`;
- else {
- let seconds =
- moment(live.points[i].time).diff(moment(live.time), "second") +
- Number(Option.toffset);
- let minutes = Math.floor(seconds / 60);
- let hours = Math.floor(minutes / 60);
- seconds = seconds % 60;
- minutes = minutes % 60;
- r += `[${f(hours)}:${f(minutes)}:${f(seconds)}] ${
- live.points[i].content
- }\n`;
- }
- }
- return r;
- }
-
- function f(num) {
- if (String(num).length > 2) return num;
- return (Array(2).join(0) + num).slice(-2);
- }
-
- function waitForKeyElements(
- selectorTxt /* Required: The jQuery selector string that
- specifies the desired element(s).
- */,
- actionFunction /* Required: The code to run when elements are
- found. It is passed a jNode to the matched
- element.
- */,
- bWaitOnce /* Optional: If false, will continue to scan for
- new elements even after the first match is
- found.
- */,
- iframeSelector /* Optional: If set, identifies the iframe to
- search.
- */
- ) {
- var targetNodes, btargetsFound;
-
- if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt);
- else targetNodes = $(iframeSelector).contents().find(selectorTxt);
-
- if (targetNodes && targetNodes.length > 0) {
- btargetsFound = true;
- /*--- Found target node(s). Go through each and act if they
- are new.
- */
- targetNodes.each(function () {
- var jThis = $(this);
- var alreadyFound = jThis.data("alreadyFound") || false;
-
- if (!alreadyFound) {
- //--- Call the payload function.
- var cancelFound = actionFunction(jThis);
- if (cancelFound) btargetsFound = false;
- else jThis.data("alreadyFound", true);
- }
- });
- } else {
- btargetsFound = false;
- }
-
- //--- Get the timer-control variable for this selector.
- var controlObj = waitForKeyElements.controlObj || {};
- var controlKey = selectorTxt.replace(/[^\w]/g, "_");
- var timeControl = controlObj[controlKey];
-
- //--- Now set or clear the timer as appropriate.
- if (btargetsFound && bWaitOnce && timeControl) {
- //--- The only condition where we need to clear the timer.
- clearInterval(timeControl);
- delete controlObj[controlKey];
- } else {
- //--- Set a timer, if needed.
- if (!timeControl) {
- timeControl = setInterval(function () {
- waitForKeyElements(
- selectorTxt,
- actionFunction,
- bWaitOnce,
- iframeSelector
- );
- }, 300);
- controlObj[controlKey] = timeControl;
- }
- }
- waitForKeyElements.controlObj = controlObj;
- }