您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
直播间内容记录
当前为
// ==UserScript== // @name Vecorder // @namespace https://www.joi-club.cn/ // @version 0.53 // @description 直播间内容记录 // @author Xinrea // @match https://live.bilibili.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @require https://cdn.staticfile.org/jquery/3.3.1/jquery.min.js // @require https://greasyfork.org/scripts/407985-ajax-hook/code/Ajax-hook.js?version=832614 // @require https://cdn.jsdelivr.net/npm/[email protected]/moment.min.js // @run-at document-end // ==/UserScript== function vlog(msg) { console.log('[Vecorder]['+getLiveStatus()+']'+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(msg) { let ltime = getLiveStartTime()*1000 if (ltime == 0) return let name = getName() let title = getRoomTitle() let id = nindexOf(name) if (id == -1) { db.push({ 'name':name, 'link':getLink(), '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)) } vlog('Init') ah.proxy({ onRequest: (config, handler) => { if (config.url === 'https://api.live.bilibili.com/msg/send') { let danmu = getMsg(config.body) if (danmu.charAt(danmu.length-1) == '】' && danmu.charAt(0) == '【') { vlog('Add Point: '+danmu.substring(1,danmu.length-1)) addPoint(danmu.substring(1,danmu.length-1)) handler.resolve({ config: config, status: 200, headers: {'content-type': 'application/json; charset=utf-8'}, response: '{"code":0,"data":[],"message":"","msg":"recorded"}' }) } else { vlog('Normal Danmu') handler.next(config); } } else { handler.next(config); } }, onError: (err, handler) => { handler.next(err) }, onResponse: (response, handler) => { handler.next(response) } }) function getLiveStatus() { return $('#head-info-vm > div > div > div.room-info-upper-row.p-relative > div.normal-mode > div:nth-child(1) > h1 > span.live-status-label.live-skin-highlight-text.live-skin-highlight-border.v-middle.preparing').text() } 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 getName(){ return $('#head-info-vm > div > div > div.room-info-down-row > a.room-owner-username.live-skin-normal-a-text.dp-i-block.v-middle').text() } function getLink() { return $('#head-info-vm > div > div > div.room-info-down-row > a.room-owner-username.live-skin-normal-a-text.dp-i-block.v-middle').attr('href') } function getRoomTitle() { return $('#head-info-vm > div > div > div.room-info-upper-row.p-relative > div.normal-mode > div:nth-child(1) > h1 > span.title-length-limit.live-skin-main-text.v-middle.dp-i-block').text() } // 根据当前地址获取直播间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); return parseInt(m);//获取当前房间号 } function getLiveStartTime() { // https://api.live.bilibili.com/room/v1/Room/room_init?id={roomID} let resp = JSON.parse($.ajax({ url: "https://api.live.bilibili.com/room/v1/Room/room_init?id="+getRoomID(), async: false }).responseText) if (resp.data.live_status != 1) return 0 else return resp.data.live_time } let toggle = false waitForKeyElements('#chat-control-panel-vm > div > div.bottom-actions.p-relative > div',(n)=>{ 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,\\5FAE\8F6F\96C5\9ED1!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,\\5FAE\8F6F\96C5\9ED1!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; }