- // ==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://gf.qytechs.cn/scripts/407985-ajax-hook/code/Ajax-hook.js?version=832614
- // @require https://cdn.jsdelivr.net/npm/moment@2.29.1/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;
- }