加载本地弹幕

在哔哩哔哩网页加载本地xml弹幕

// ==UserScript==
// @name         加载本地弹幕
// @namespace    https://greasyfork.org/zh-CN/users/1202577-%E5%AD%90%E5%9F%9F
// @supportURL   https://greasyfork.org/zh-CN/users/1202577-%E5%AD%90%E5%9F%9F
// @version      0.2
// @description  在哔哩哔哩网页加载本地xml弹幕
// @author       子域
// @match        *://www.bilibili.com/video/*
// @match        *://www.bilibili.com/bangumi/play/*
// @match        *://www.bilibili.com/list/watchlater?*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// @run-at       document-start
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @license      MIT
// ==/UserScript==

/* globals jQuery,$*/
function init(){
    function hookLoadHistory(){
        Object.defineProperty(Object.prototype,"allHistory",{
            set(v){
                delete Object.prototype.allHistory;
                let that =this;
                this.allHistory=v;
                this.allHistory=new Proxy(this.allHistory,{
                    get(target,prop){
                        that.dmListStore.basList=[];
                        $("div.bas-danmaku",that.nodes.basDanmaku).html("");
                        if(prop!=="0"){
                            return target[prop];
                        }else{
                            let decodeMsg=$("#localDmFile").data("decodeMsg");
                            if(decodeMsg){
                                return Promise.resolve(decodeMsg);
                            }else{
                                return target[prop];
                            }
                        }
                    },
                });
            },
            get(){
                return this._allHistory;
            },
            configurable:true,
        });
        return;
    }
    function loadDatePicker(){
        let task={};
        let ob=new MutationObserver(function(recode){
            getElement(task);
        });
        function getElement({selector,context=$(document),callback}){
            let elem=context.find(selector).eq(0);
            if(elem.length){
                callback(elem);
            }
        }
        return new Promise(resolve=>{
            task.selector="#danmukuBox";
            task.callback=resolve;
            getElement(task);
        }).then(danmukuBox=>new Promise(resolve=>{
            ob.disconnect();
            ob.observe(
                danmukuBox[0],
                {"childList":true,"subtree":true}
            );
            task.selector="div.bui-collapse-wrap";
            task.context=danmukuBox;
            task.callback=resolve;
            getElement(task);
        })).then(collapseWrap=>new Promise(resolve=>{
            if(collapseWrap.hasClass("bui-collapse-wrap-folded")){
                collapseWrap.find("div.bui-collapse-header").click();
            }
            task.selector="div.bpx-player-dm-btn-history";
            task.context=collapseWrap;
            task.callback=resolve;
            getElement(task);
        })).then(datePickerBtn=>new Promise(resolve=>{
            ob.disconnect();
            ob.observe(
                datePickerBtn[0],
                {"attributes":true,
                 "childList":true,
                 "subtree":true,}
            );
            setTimeout(datePickerBtn.click.bind(datePickerBtn));
            task.selector="div.bpx-player-date-picker.bpx-player-show";
            task.context=datePickerBtn;
            task.callback=resolve;
            getElement(task);
        })).then(datePicker=>{
            datePicker.closest("div.bpx-player-dm-btn-history").click();
        }).finally(()=>{
            ob.disconnect();
        });
    }
    function insertInputBtn(){
        let input=$(document.createElement('input'));
        input.attr({
            "type":"file",
            "id":"localDmFile",
            "style":"display:block;position:fixed;bottom:10px;left:10px;z-index:100;border:solid 2px #9e9e9e;background-color:white;width:160px;opacity:0.8;",
        });
        input.on("click",function(e){
            this.value=null;
            e.stopPropagation();
        });
        input.on("change",function(){
            try{
                if(!this.files.length){
                    return;
                }
                let file=this.files[0];
                let reader=new FileReader();
                reader.readAsArrayBuffer(file);
                reader.onload = loadDmFromFile;
            }catch(e){
                throw new Error("读取文件错误\n"+e.stack);
            }
        });
        input.on("mouseenter",function(){
            $(this).fadeTo(100,1);
        });
        input.on("mouseleave",function(){
            $(this).fadeTo("slow",0.5,"swing");
        });
        $(document.body).append(input);
        function loadDmFromFile(){
            let result;
            if(this.result instanceof window.ArrayBuffer){
                result=new TextDecoder("utf-8").decode(this.result);
            }else if(typeof this.result ==="string"){
                result=this.result;
            }else{
                throw Error(`未知类型\n${Error().stack}`);
            }
            const xml = new DOMParser().parseFromString(result,"text/xml");
            let decodeMsg=[];
            let stimeOffset=parseInt(prompt("弹幕时间偏移(单位:毫秒)\n(正数延迟出现,负数反之,默认为0)",0));
            for (let value of xml.querySelectorAll("d")){
                let [stime,mode,size,color,date,weight,uhash,dmid]=value.getAttribute('p').split(',');
                decodeMsg.push({
                    "stime":+stime*1e3+stimeOffset,//出现时间
                    "mode":+mode,//弹幕类型:1:滚动,4:底部,5:顶部,6:逆向,7:高级(字幕),8:代码,9:BAS
                    "size":+size,//字号
                    "color":+color,//颜色
                    "date":+date,//时间戳(秒)
                    "weight":+weight,//智能屏蔽等级
                    "uhash":uhash,//用户hash
                    "dmid":dmid,//弹幕id
                    "text":value.textContent.replace(/[\/\\]n/g,"\n"),//弹幕内容
                    "pool":({7:1,8:2,9:2}[mode])||0,//弹幕池类型,0=普通,1=高级(字幕),2=代码
                    "attr":2,//attr<0时带拇指点赞图标
                    "asNormal":false
                });
            }
            $("#localDmFile").data('decodeMsg',decodeMsg);
            let datePickerSelector="#danmukuBox div.bpx-player-dm-btn-history div.bpx-player-date-picker";
            let fakeElement=$(datePickerSelector)
            .find("div.bpx-player-date-picker-day-content span.bpx-player-date-picker-day")
            .eq(0)
            .clone()
            .attr({
                "data-timestamp": new Date().setHours(0,0,0,0)/1e3,
                "data-action": "changeDay",
            })[0];
            let fakeEvent=new MouseEvent("click");
            Object.defineProperty(fakeEvent,"target",{value:fakeElement});
            $(datePickerSelector)[0].dispatchEvent(fakeEvent);
        }
    }

    hookLoadHistory();
    $(document).ready(function(){
        loadDatePicker()
            .then(insertInputBtn)
            .catch(e=>{
            throw new Error("初始化失败\n"+e.stack);
        });
    });
    return;
}

(function(){
    "use strict";
    try{
        init();
    }catch(e){
        alert(`运行异常:${e.message},请刷新重试`);
    }
})();