B站循环助手-稳定版

稳定可靠的AB点循环工具,适配最新B站页面结构

当前为 2025-02-21 提交的版本,查看 最新版本

// ==UserScript==
// @name         B站循环助手-稳定版
// @namespace    bilibili-replayer
// @version      1.3
// @description  稳定可靠的AB点循环工具,适配最新B站页面结构
// @author       dms
// @match        https://www.bilibili.com/video/BV*
// @match        https://www.bilibili.com/bangumi/play/ep*
// @match        https://www.bilibili.com/medialist/play/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';
    const isNormalVideo = /^(https?:\/\/(www\.)bilibili\.com\/video\/(?:BV|AV)\w+).*/i.test(window.location.href);
    
    const copyText = text => {
        const textArea = document.createElement('textarea');
        textArea.setAttribute('readonly', 'readonly');
        textArea.value = text;
        document.body.appendChild(textArea);
        textArea.select();
        document.execCommand('copy');
        document.body.removeChild(textArea);
    };

    const createButton = (text, className, parent) => {
        const button = document.createElement('div');
        className.split(' ').forEach(c => button.classList.add(c));
        button.innerText = text;
        parent.appendChild(button);
        return button;
    };

    const createToolbar = () => {
        const toolbarbox = document.createElement('div');
        toolbarbox.style = 'width: 100%; height: 1.5rem;';
        const container = document.querySelector('#playerWrap') || document.querySelector('#player_module');
        container.appendChild(toolbarbox);
        if(document.querySelector('#player_module')) {
            document.querySelector('#player_module').style.marginBottom = '2rem';
        }

        const toolbarShadow = toolbarbox.attachShadow({mode: 'closed'});
        const toolbarStyle = document.createElement('style');
        toolbarStyle.innerHTML = `
            #replayer-toolbar {
                width: 100%;
                height: 1.5rem;
                font-size: 0.8rem;
                line-height: 1.6rem;
                margin: .2rem 0;
            }
            #replayer-toolbar::after {
                content: " ";
                display: block;
                clear: both;
            }
            .tool-item {
                float: left;
                padding: 0 0.3rem;
                border-radius: .2rem;
            }
            .tool-button {
                border: 1px solid rgba(0, 0, 0, .1);
                cursor: pointer;
            }
            .active-button {
                background-color: #00aeec;
                color: white;
            }
            .hide {
                display: none;
            }
        `;

        const getLinkClass = isNormalVideo ? '' : ' hide';
        toolbarShadow.appendChild(toolbarStyle);
        const toolbar = document.createElement('div');
        toolbar.id = 'replayer-toolbar';
        toolbarShadow.appendChild(toolbar);

        createButton('起点:', 'tool-item tool-text', toolbar);
        const pointA = createButton('起点A', 'tool-item tool-button', toolbar);
        const toA = createButton('跳到这里', 'tool-item tool-button', toolbar);
        const linkA = createButton('复制链接', 'tool-item tool-button' + getLinkClass, toolbar);
        createButton('|', 'tool-item tool-text', toolbar);
        createButton('终点:', 'tool-item tool-text', toolbar);
        const pointB = createButton('终点B', 'tool-item tool-button', toolbar);
        const toB = createButton('跳到这里', 'tool-item tool-button', toolbar);
        const linkB = createButton('复制链接', 'tool-item tool-button' + getLinkClass, toolbar);
        createButton('|', 'tool-item tool-text', toolbar);
        const Start = createButton('开始循环', 'tool-item tool-button', toolbar);
        createButton('|', 'tool-item tool-text' + getLinkClass, toolbar);
        createButton('当前:', 'tool-item tool-text' + getLinkClass, toolbar);
        const linkNow = createButton('复制链接', 'tool-item tool-button' + getLinkClass, toolbar);

        const video = document.querySelector('#bilibili-player video');
        const points = [0, video.duration-1];
        const pointButtons = [pointA, pointB];
        
        const setPoint = (i, val) => {
            if(val && val.trim()) {
                if(!/^(\d+h)?(\d+m)?\d+(\.\d+)?$/i.test(val)) {
                    alert('格式有误,请注意查看示例格式');
                    return;
                }
                if(typeof(val) === 'string') {
                    const hms = val.split(/h|m/g);
                    const h = /\d+h/.test(val) ? +hms[0] : 0;
                    const m = /\d+m/.test(val) ? +hms[hms.length-2] : 0;
                    const s = +hms[hms.length-1];
                    
                    points[i] = h*3600 + m*60 + s;
                } else {
                    points[i] = val;
                }
                pointButtons[i].classList.add('active-button');
                GM_setValue('Point_'+i, points[i]);
                return;
            }
            points[i] = i ? video.duration-1 : 0;
            pointButtons[i].classList.remove('active-button');
            GM_setValue('Point_'+i, null);
        };

        const savedPointA = GM_getValue('Point_0');
        const savedPointB = GM_getValue('Point_1');
        
        if(savedPointA !== null) {
            pointA.classList.add('active-button');
            points[0] = savedPointA;
        }
        if(savedPointB !== null) {
            pointB.classList.add('active-button');
            points[1] = savedPointB;
        }

        pointA.addEventListener('click', () => {
            const pointAInput = prompt('请输入一个时间(单位:秒),默认值是当前时间点。取消则清除此时间点,恢复为默认值:视频开头。\n输入值可以包含分钟,例如:三分十二秒写作 3m12\n输入值可以包含小时,例如:一小时三分十二秒写作 1h3m12', video.currentTime);
            setPoint(0, pointAInput);
        });
        pointB.addEventListener('click', () => {
            const pointBInput = prompt('请输入一个时间(单位:秒),默认值是当前时间点。取消则清除此时间点,恢复为默认值:视频结尾(前一秒)。\n输入值可以包含分钟,例如:三分十二秒写作 3m12\n输入值可以包含小时,例如:一小时三分十二秒写作 1h3m12', video.currentTime);
            setPoint(1, pointBInput);
        });

        let mainTimer = 0;
        Start.addEventListener('click', () => {
            if(mainTimer) {
                clearInterval(mainTimer);
                mainTimer = 0;
                Start.classList.remove('active-button');
                return;
            }
            Start.classList.add('active-button');
            mainTimer = setInterval(() => {
                const A = points[0] <= points[1] ? points[0] : points[1];
                const B = points[0] > points[1] ? points[0] : points[1];
                if(video.currentTime >= B) {
                    video.currentTime = A;
                }
            }, 200);
        });

        toA.addEventListener('click', () => { video.currentTime = points[0]; });
        toB.addEventListener('click', () => { video.currentTime = points[1]; });

        const getLink = t => {
            const link = window.location.href.replace(/^(https?:\/\/(www\.)bilibili\.com\/video\/(?:BV|AV)\w+).*/i, '$1') + '?t=' + t;
            copyText(link);
            alert('带时间标记的链接已复制到剪切板。');
        };

        linkA.addEventListener('click', () => { getLink(points[0]); });
        linkB.addEventListener('click', () => { getLink(points[1]); });
        linkNow.addEventListener('click', () => { getLink(video.currentTime); });
    };

    window.addEventListener('load', () => {
        const waitTimer = setInterval(() => {
            if(document.querySelector('#bilibili-player video')) {
                clearInterval(waitTimer);
                createToolbar();
            }
        }, 1000);
    });
})();

QingJ © 2025

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