b站辅助(时长/集数/倍速/单集循环)

更好地使用bilibili(b站)!统计分集视频和视频合集的时长和集数(已看时长、未看时长、总时长、集数..);增强倍速功能,最高16倍速;单集循环快捷键..。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         b站辅助(时长/集数/倍速/单集循环)
// @namespace    http://tampermonkey.net/
// @version      1.0.2
// @description  更好地使用bilibili(b站)!统计分集视频和视频合集的时长和集数(已看时长、未看时长、总时长、集数..);增强倍速功能,最高16倍速;单集循环快捷键..。  
// @author       eleky
// @match        https://www.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        none
// @license MIT
// ==/UserScript==
 
/*
### 功能
1. 统计视频已看时长、正在观看的这一集的时长、未看时长、总时长,显示到右侧。按“j”或“J”键。
2. 统计视频已看集数、正在看的集数、未看集数、总集数,显示到右侧。按“j”或“J”键。
3. 视频倍速播放及快捷键,按“,”或“<”速度减0.25,按“。”或“<”速度加0.25。b站自带的倍速调整会失效。
4. 设置单集循环,按“k”或“K”键。
*/
 
window.onload = function () {
    console.log("b站辅助(时长/集数/倍速/单集循环)...");
    var jq = document.createElement('script');
    jq.setAttribute('type', 'text/javascript');
    jq.src = "https://cdn.bootcss.com/jquery/3.5.0/jquery.min.js";
    document.getElementsByTagName('head')[0].appendChild(jq);
    var rate = 1;
    var stop = 1;
    var videoSelector = "bwp-video"; // 视频元素选择器
    var keyMap = {
        forward0_25: '.', // 速度增加 0.25x
        back0_25: ',', // 速度减少 0.25x
        rate1: '1', // 设为 1x
        rate3: '3', // 设为 3x
        stats_key: 'j',
        stats_key2: 'J',
        select_repeat: 'k',
        select_repeat2: 'K',
    };
    var el = document.querySelector(videoSelector) || document.querySelector('video');
 
    window.addEventListener('keydown', (e) => {
        // console.log("e.key ",e.key);
        if (e.key === keyMap.rate1) {
            rate = 1;
        } else if (e.key === keyMap.rate3) {
            rate = 3;
        } else if (e.key === keyMap.forward0_25 && rate < 16) {
            rate += 0.25;
        } else if (e.key === keyMap.back0_25 && rate > 0.25) {
            rate -= 0.25;
        } else if (e.key === keyMap.stats_key || e.key === keyMap.stats_key2) {
            stats(); // 统计集数和时长
            // showVideoInfo("stats");
            return;
        } else if (e.key === keyMap.select_repeat || e.key === keyMap.select_repeat2) {
            select_repeat(); // 选中洗脑循环
            // showVideoInfo("repeat");
            return;
        } else {
            return;
        }
        setVideoRate();
        showVideoInfo("X" + rate);
    })
 
    // window.onkeydown = function(ev){
    //     console.log(ev.keyCode);
    // }
 
    function setVideoRate() {
        if (el) {
            el.playbackRate = rate;
        }
    }
 
    function showVideoInfo(info) { //显示倍速
        //找到显示位置
        var position = document.getElementsByClassName("bpx-player-video-area")[0];
 
        //获取标签
        var tag = document.getElementById("mytag");
        var isNull = tag === null;
 
        //没有创建过这个标签就创建
        if (isNull) {
            //创建显示标签
            tag = document.createElement("div");
            tag.setAttribute("id", "mytag");
            tag.style = '\n' +
                '    width: 50px;\n' +
                '    height: 28px;\n' +
                '    background-color: #666666;\n' +
                '    position: absolute;\n' +
                '    top: 50%;\n' +
                '    left: 50%;\n' +
                '    transform: translate(-50%, -50%);\n' +
                '    border-radius: 2px;\n' +
                '    z-index: 99999999;\n' +
                '    text-align: center;\n' +
                '    line-height: 28px;\n' +
                '    font-size: 14px;\n' +
                '    color: #fff;\n' +
                '    ';
        }
        $("#mytag").css("display", "block");
 
        //写入html
        tag.innerHTML = info;
 
        //数据添加到面板
        if (isNull) {
            position.after(tag);
        }
 
        //定时消失
        sleep(1000).then(() => {
            $("#mytag").css("display", "none");
        })
 
    }
 
    // function select_repeat() { // 原写法,没用
    //     //$(".bilibili-player-iconfont .bilibili-player-iconfont-setting").trigger('mouseover');
    //     $(".bilibili-player-video-btn-setting").trigger('mouseover'); //使循环播放按钮出现
    //     $(".bilibili-player-video-btn-setting").trigger('mouseout'); //使循环播放按钮消失
    //     //let setting = document.querySelector(".bilibili-player-iconfont .bilibili-player-iconfont-setting");
    //     //setting.click();
    //     $(".bilibili-player-video-btn-setting-left-repeat .bui-switch-input").trigger('click'); //真实选中洗脑循环
    //     $(".bilibili-player-video-btn-setting-left-repeat .bui-switch-input").attr('checked', true); //只是看起来选中了
 
    //     // var a = document.getElementsByClassName("bpx-player-ctrl-btn bpx-player-ctrl-setting")[0];
    //     // // 不通过鼠标,自动 mouseover 事件。其他事件也类推。
    //     // var ev = new Event("mouseover");
    //     // a.dispatchEvent(ev);
    // }
 
    //获取设置列表
    function select_repeat() {
        var setting_btn = document.getElementsByClassName("bpx-player-ctrl-btn bpx-player-ctrl-setting")[0];
        var setting_list = setting_btn.getElementsByClassName("bpx-player-ctrl-setting-box")[0];
        var setting_repeat = setting_list.getElementsByTagName("input")[1];
        // 此时setting_repeat = <input class="bui-switch-input" type="checkbox" aria-label="洗脑循环">
        // var ev_click = new Event("click");// 这么写没用
        // setting_repeat.dispatchEvent(ev_click);
        setting_repeat.click();
    }
 
 
    function stats() {
 
        let nodeList;
        try {
            //获取视频列表节点
            nodeList = getVideoList();
            // console.log("已获取视频列表节点");
        } catch (e) {
            console.log("没有视频列表,不是视频选集");
            show2(); //单集视频显示时长
            clearInterval(stop);
            return;
        }

        //sleep(10000).then(() => {
        //获取当前观看索引
        let index = getCurrentLookVideoIndex(nodeList);
        // console.log("当前观看索引:" + index);
        //全部视频个数
        let all_num = nodeList.length;
        // console.log("全部视频个数:" + all_num);
        //已看视频个数
        let looked = index;
        // console.log("已看视频个数:" + looked);
        //未看视频个数
        let number = all_num - index - 1;
        // console.log("未看视频个数:" + number);
 
        //获取视频全部时间的数组
        let allTime = getTimeArray(nodeList, 0, nodeList.length);
        // console.log("视频全部时间的数组:" + allTime);
        //所有时间数组,格式 [h,m]
        let all_time_arr = format(allTime);
        // console.log("所有时间数组,格式 [h,m,s]:" + all_time_arr);
 
        //获取已观看的视频时间数组
        let looked_time = getTimeArray(nodeList, 0, index);
        // console.log("已观看的视频时间数组:" + looked_time);
        //已看时间数组,格式 [h,m]
        let looked_time_arr = format(looked_time);
        // console.log("已看时间数组,格式 [h,m,s]:" + looked_time_arr);
 
        //获取正在观看的视频时间数组
        let looking_time = getTimeArray(nodeList, index, index + 1);
        // console.log("正在观看的视频时间数组:" + looking_time);
        //正在观看时间数组,格式 [h,m]
        let looking_time_arr = format(looking_time);
        // console.log("正在观看时间数组,格式 [h,m,s]:" + looking_time_arr);
 
        //获取未观看的视频时间数组
        let timeArray = getTimeArray(nodeList, index + 1, nodeList.length);
        // console.log("未观看的视频时间数组:" + timeArray);
        //未看时间数组,格式 [h,m]
        let undone_time_arr = format(timeArray);
        // console.log("未看时间数组,格式 [h,m,s]:" + undone_time_arr);
 
        //显示到网页
        // console.log("begin 显示到网页");
        show(looked_time_arr, looking_time_arr, undone_time_arr, all_time_arr, looked, number, all_num);
        // console.log("end 显示到网页");
        //})
 
    }
 
    //单集视频显示时长
    function show2() {
        //获取总时长
        let time_box = document.getElementsByClassName('bpx-player-ctrl-time-duration')[0];
        let time_num = time_box.innerHTML;
 
        //console.log("显示到网页:" + time_num);
 
        //找到显示位置
        let plain = document.getElementById("danmukuBox");
        let data_tag = document.getElementById("data_tag");
        let isNull = data_tag === null;
 
        //没有创建过这个标签就创建
        if (isNull) {
            //创建
            data_tag = document.createElement("div");
            //console.log(data_tag)
            //id赋值,用于下次更新查找
            data_tag.setAttribute("id", "data_tag");
            data_tag.setAttribute("width", "100%");
        }
 
        //写入html 
        data_tag.innerHTML = "<div>总时长:" + time_num + "</div>";
 
        //数据添加到面板
        if (isNull) {
            plain.after(data_tag);
        }
 
    }
 
    //获取视频索引列表
    function getVideoList() {
        let list_box = document.getElementsByClassName('video-pod__list')[0];
        return list_box.childNodes;
    }
 
    //获取到当前观看视频的索引
    function getCurrentLookVideoIndex(nodeList) {
        let index = null;
        for (let i = 0; i < nodeList.length; i++) {
            //当前观看的视频
            let current = nodeList[i];
            //延迟之后获取class值
 
            let class_name = current.className;
            let dataScrolled = current.getAttribute('data-scrolled');
 
            //当前观看
            if (class_name.includes('active')||dataScrolled=='true') {
                //console.log(class_name)  //类名
                index = i;
                console.log("当前视频索引:"+index)
                break;
            }
            //循环结束时还没有获取到索引(正常不会之前,前面就跳出了)
            if (i === nodeList.length - 1) {
                console.log("未获取当前视频索引")
            }
 
        }
        return index;
    }
 
    //获取到时间时间字符串
    function getTimeArray(nodeList, start_index, end_index) {
        let parent_array = [];
        for (let i = start_index; i < end_index; i++) {
            // nodeList[i]代表列表中的每一个li
            let div = nodeList[i].getElementsByClassName("duration");
            //每个视频的时长
            let duration = div[0].innerHTML;
            // console.log(duration);   //格式:'09:29'
 
            //添加到数组
            let child_array = duration.split(":");
            if (child_array.length < 3) {
                //数组首部添加0
                child_array.unshift('0');
            }
            parent_array.push(child_array);
 
        }
        return parent_array;
    }
 
    //计算时间/格式化
    function format(timeArray) {
 
        //console.log("视频列表长度:"+timeArray.length);  //如果为0没有数据,就出错了
 
        let h = 0,
            m = 0,
            s = 0;
        for (let i = 0; i < timeArray.length; i++) {
            h += Number(timeArray[i][0]);
            m += Number(timeArray[i][1]);
            s += Number(timeArray[i][2]);
        }
 
        //将秒转换为分钟
        let temp1 = s / 60;
        let m1 = Math.floor(temp1);
        m += m1;
 
        //小于一分钟的转换为秒
        let s2 = ('0.' + String(temp1).split('.')[1]) * 60;
 
        //分钟转换为小时
        let temp = m / 60;
        let h1 = Math.floor(temp);
 
        //小于一小时的转换为分钟
        let m2 = ('0.' + String(temp).split('.')[1]) * 60;
 
 
        //最终结果
        h += h1;
        s = Math.floor(s2);
        m = Math.floor(m2);
 
        //分钟出现NaN,原因是因为没有分钟,全是小时,直接赋值
        if (isNaN(m)) {
            m = "0";
        }
        //同理
        if (isNaN(s)) {
            s = "0";
        }
 
        //console.log("小时:"+h);
        //console.log("分钟:"+m);
        //console.log("秒:"+s);
 
        return [h, m, s];
 
    }
 
    //在评论上显示
    function show(looked_time_arr, looking_time_arr, undone_time_arr, all_time_arr, looked, number, all_num) {
        //找到显示面板
        let plain = document.getElementById("danmukuBox");
 
        let data_tag = document.getElementById("data_tag");
 
        let isNull = data_tag === null;
 
        //没有创建过这个标签就创建
        if (isNull) {
            //创建
            data_tag = document.createElement("table");
            //console.log(data_tag)
 
            //id赋值,用于下次更新查找
            data_tag.setAttribute("id", "data_tag");
            data_tag.setAttribute("class", "multi-page-v1 small-mode");
            data_tag.setAttribute("width", "100%");
 
        }
 
        //写入html
        data_tag.innerHTML = "<tr><th></th><th>已看</th><th>正在</th><th>未看</th><th>全部</th></tr>" + show_Str();
 
        //数据添加到面板
        if (isNull) {
            plain.after(data_tag);
        }
 
        $("#data_tag th,#data_tag td").css("width", "20%");
        $("#data_tag th,#data_tag td").css("padding", "5px");
        $("#data_tag th,#data_tag td").css("text-align", "center");
 
        function show_Str() {
            let looked_time = looked_time_arr.join(':');
            let looking_time = looking_time_arr.join(':');
            let undone_time = undone_time_arr.join(':');
            let all_time = all_time_arr.join(':');
            let l1 = "<tr><td>集数</td><td>" + looked + "</td><td>" + 1 + "</td><td>" + number + "</td><td>" + all_num + "</td></tr>";
            let l2 = "<tr><td>时长</td><td>" + looked_time + "</td><td>" + looking_time + "</td><td>" + undone_time + "</td><td>" + all_time + "</td></tr>";
 
            return l1 + l2;
        }
    }
 
    //跳过充电鸣谢
    function pass() {
        let jumpButton = '.bilibili-player-electric-panel-jump';
        setInterval(() => {
            if ($(jumpButton).length > 0) {
                $(jumpButton).trigger('click')
            }
        }, 200)
    }
 
    //延时函数
    function sleep(time) {
        return new Promise((resolve) => setTimeout(resolve, time));
    }
 
};