Greasy Fork is available in English.

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

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

// ==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));
    }
 
};