[Bilibili] 自动切P

自动在多P分集中切换下一P或跳过进度

// ==UserScript==
// @name         [Bilibili] 自动切P
// @namespace    ckylin-bilibili-auto-next-part
// @version      0.1
// @description  自动在多P分集中切换下一P或跳过进度
// @author       CKylinMC
// @match        https://*.bilibili.com/video/av*
// @match        https://*.bilibili.com/video/BV*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @require      https://greasyfork.org/scripts/429720-cktools/code/CKTools.js?version=1023553
// @grant        unsafeWindow
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';
    class Logger {
        constructor(prefix = '[logUtil]') {
            this.prefix = prefix;
        }
        log(...args) {
            console.log(this.prefix, ...args);
        }
        info(...args) {
            console.info(this.prefix, ...args);
        }
        warn(...args) {
            console.warn(this.prefix, ...args);
        }
        error(...args) {
            console.error(this.prefix, ...args);
        }
    }
    const logger = new Logger("[AUTOP]");
    if (CKTools.ver < 1.2) {
        logger.warn("Library script 'CKTools' was loaded incompatible version " + CKTools.ver + ", so that SNI may couldn't work correctly. Please consider update your scripts.");
    }
    const { get, getAll, domHelper, wait, waitForDom, waitForPageVisible, addStyle, modal, bili } = CKTools;

    const getVideoID = () => {
        let id = new URL(location.href).pathname.replace('/video/', '')
        if (id.endsWith('/')) {
            id = id.substring(0, id.length - 2);
        }
        if (id.startsWith('av')) {
            return { type: 'aid', id };
        }
        if (id.startsWith('BV')) {
            return { type: 'bvid', id };
        }
        return { type: 'unknown', id };
    }
    const getCurrentTime = () => dataStore.vid?.currentTime??-1;
    const getCurrentPart = () => {
        let part = new URL(location.href).searchParams.get('p');
        if (!part) part = '1';
        return +part;
    }
    async function playerReady() {
        let i = 150;
        while (--i > 0) {
            await wait(100);
            if (unsafeWindow.player?.isInitialized() ?? false) break;
        }
        if (i < 0) return false;
        await waitForPageVisible();
        while (1) {
            await wait(200);
            if (document.querySelector(".bilibili-player-video-control-wrap, .bpx-player-control-wrap")) return true;
        }
    }
    const dataStore = unsafeWindow.autonextpart = {
        p: 0,
        id: null,
        vid: null,
        config: {
            autoNextAt: -1,//>0 to enable
            partsDefined: [
                null,
                /* 1: *//*{
                    startsAt: -1,
                    endsAt: -1,
                    ignoreGlobal: false,
                    skip: [
                        {
                            from: -1,
                            to: -1
                        }
                    ]
                }*/
            ]
        },
        next: () => {
            unsafeWindow.dispatchEvent(new KeyboardEvent("keydown", {
                key: "]",
                keyCode: 221,
                code: "BracketRight",
                which: 221,
                shiftKey: false,
                ctrlKey: false,
                metaKey: false
            }));
        },
        hasNext: () => {

        }
    };

    function parseDesc(desc) {
        const rootRegex = /AP:=(GP!(?<GP>\d+)!GP;){0,1}(?<parts>.+)*=:AP/m;
        let rootResult = rootRegex.exec(desc);
        if (!rootRegex || !rootResult.groups) return false;

        const { GP, parts } = rootResult.groups;
        if (!isNaN(+GP)) dataStore.config.autoNextAt = +GP;

        if (parts.length) {
            let partsSplited = parts.split(';').filter(i => i.trim().length);
            for (let part of partsSplited) {
                let [partName, start, end, subs, ignoreGlobal] = part.split('!');
                let partId = +(partName.substring(1));
                if (isNaN(partId)) continue;
                let config = { startAt: -1, endsAt: -1, skip: [] };
                if (!isNaN(+start)) config.startAt = +start;
                if (!isNaN(+end)) config.endsAt = +end;
                let subsParts = subs.split("+").filter(i => i.trim().length);
                for (let sub of subsParts) try {
                    const [from, to] = JSON.parse(sub);
                    if (!isNaN(+from) && !isNaN(+to)) config.skip.push({ from, to });
                } catch (e) { continue; }
                if (ignoreGlobal) config.ignoreGlobal = true;
                logger.info("发现配置: 分P", partId, "设定", config);
                dataStore.config.partsDefined[+partId] = config;
            }
        }
        return true;
    }

    async function tryInject() {
        logger.log("注入开始");
        dataStore.vid = document.querySelector('.bpx-player-video-wrap>video');
        if (!dataStore.vid) {
            logger.error("未能找到播放器...");
            return false;
        }
        logger.info("已找到播放器:", dataStore.vid);
        dataStore.id = getVideoID();
        if (dataStore.id.type == "unknown") {
            logger.error("无法识别的视频ID:", dataStore.id.id);
            // return;
        } else {
            logger.log("视频ID", dataStore.id);
        }
        dataStore.p = getCurrentPart();
        if (isNaN(dataStore.p)) {
            logger.error("未知分P:", dataStore.p);
            return;
        } else {
            logger.log("视频分P", dataStore.p);
        }
        dataStore.vid.removeEventListener("timeupdate", onTimeUpdate);
        dataStore.vid.addEventListener("timeupdate", onTimeUpdate);
        logger.log("视频进度已hook");
        try {
            let desc = document.querySelector('.desc-info-text');
            if (!desc) throw "";
            let descTxt = desc.textContent;
            // let descTxt = `AP:=GP!5!GP;=:AP`;
            if (descTxt.includes("AP:=")) {
                let startIdx = descTxt.indexOf("AP:=");
                let endIdx = descTxt.indexOf("=:AP");
                if (startIdx === -1 || endIdx === -1) throw "";
                parseDesc(descTxt);
            } else throw "";
            unsafeWindow.player?.toast.create({text:"自动切P已启用"})
        } catch (e) {
            logger.log("没有在描述中发现信息", e);
        }
        logger.log("注入完成");
    }

    function onTimeUpdate(event) {
        let t = getCurrentTime();
        if (t == -1) return;
        if (dataStore.config.partsDefined[dataStore.p]) {
            let cfg = dataStore.config.partsDefined[dataStore.p];
            if (cfg.startsAt > -1 && t < cfg.startsAt) {
                if (unsafeWindow.player)
                    unsafeWindow.player.seek?.(cfg.startsAt);
                else if (dataStore.vid)
                    dataStore.vid.currentTime = cfg.startsAt;
                return;
            }
            if (cfg.endsAt > -1 && t >= cfg.endsAt) {
                if (unsafeWindow.player){
                    unsafeWindow.player.pause();
                    unsafeWindow.player.toast.create({text:"正在切换下一P"})
                }
                else if (dataStore.vid)
                    dataStore.vid.pause();
                dataStore.next();
                dataStore.p++;
                return;
            }
            //skip
        }
        if (dataStore.config.autoNextAt > -1 && t >= dataStore.config.autoNextAt) {
            if (unsafeWindow.player){
                unsafeWindow.player.pause();
                unsafeWindow.player.toast.create({text:"正在切换下一P"})
            }
            else if (dataStore.vid)
                dataStore.vid.pause();
            dataStore.next();
            dataStore.p++;
            
            return;
        }
    }

    function run() {
        logger.log("等待播放器...");
        playerReady().then(tryInject);
    }
    run();
})();