Vecorder

直播间内容记录

目前为 2020-12-18 提交的版本。查看 最新版本

// ==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/[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">&times;</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;
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址