您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
直播间内容记录 https://github.com/Xinrea/Vecorder
// ==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/[email protected]/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/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; }