B站大学课程辅助器

让你自律地看多集视频

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         B站大学课程辅助器
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  让你自律地看多集视频
// @author       zhuangjie
// @match        https://www.bilibili.com/video/**
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @license MIT
// ==/UserScript==

(async function() {
    'use strict';
    // 【url改变监听器】
    function onUrlChange(fun) {
        let initUrl = window.location.href.split("#")[0];
        function urlChangeCheck() {
            let currentUrl = window.location.href.split("#")[0];
            if(initUrl != currentUrl) {
                console.log("路径改变了")
                // 新的=>旧的
                initUrl = currentUrl;
                fun();
                initUrl = currentUrl;
            }
        }
        let si = setInterval(urlChangeCheck,460)
        window.onblur = function() {
            clearInterval(si);
        }
        window.onfocus = function() {
            si = setInterval(urlChangeCheck,460)
        }
    }
    // 数据缓存器
    let cache = {
        get(key) {
            return GM_getValue(key);
        },
        set(key,value) {
            GM_setValue(key,value);
        },
        jGet(key) {
            let value = GM_getValue(key);
            if( value == null) return value;
            return JSON.parse(value);
        },
        jSet(key,value) {
            value = JSON.stringify(value)
            GM_setValue(key,value);
        },
        remove(key) {
            GM_deleteValue(key);
        },
        cookieSet(cname,cvalue,exdays) {
            var d = new Date();
            d.setTime(d.getTime()+exdays);
            var expires = "expires="+d.toGMTString();
            document.cookie = cname + "=" + cvalue + "; " + expires;
        },
        cookieGet(cname) {
            var name = cname + "=";
            var ca = document.cookie.split(';');
            for(var i=0; i<ca.length; i++)
            {
                var c = ca[i].trim();
                if (c.indexOf(name)==0) return c.substring(name.length,c.length);
            }
            return "";
        }
    }
    // 防抖函数
    function debounce(func, delay) {
        let timeoutId;
        return function() {
            const context = this;
            const args = arguments;

            clearTimeout(timeoutId);

            timeoutId = setTimeout(function() {
                func.apply(context, args);
            }, delay);
        };
    }
    // 获取视频的ID
    function getVideoId() {
        let regex = /.*?video\/([^?\/]*).*/; // 匹配 /video/ 后面的字符,直到遇到 /
        let match = window.location.href.match(regex); // 使用正则表达式匹配
        if (match && match[1]) {
            let videoId = match[1];
            return videoId;
        } else {
            return null;
        }
    }
    // 获取指定属性data-开头的属性名-返回数组
    function getDataAttributes(element) {
        var dataAttributes = [];

        if (element && element.attributes) {
            var attributes = element.attributes;

            for (var i = 0; i < attributes.length; i++) {
                var attributeName = attributes[i].name;

                if (attributeName.startsWith('data-')) {
                    dataAttributes.push(attributeName);
                }
            }
        }
        return dataAttributes;
    }
    // 判断当前是否在iframe里面,
    function currentIsIframe() {
        if (self.frameElement && self.frameElement.tagName == "IFRAME") return true;
        if (window.frames.length != parent.frames.length) return true;
        if (self != top) return true;
        return false;
    }

    // 播放状态修改
    function getPlayStatus() { // 播放 true,暂停false
        var element = document.querySelector('.bpx-player-state-play');
        var computedStyle = getComputedStyle(element);
        var display = computedStyle.getPropertyValue('display');
        var visibility = computedStyle.getPropertyValue('visibility');
        var isVisible = (display !== 'none' && visibility !== 'hidden');
        return !isVisible;
    }
    // 修改视频播放状态
    function play(isPlay = false) {
       if(getPlayStatus() == isPlay) return;
       // 如果状态不一致,让状态一致
       var button = document.getElementsByClassName("bpx-player-ctrl-play")[0];
        // 创建并初始化一个点击事件
        var clickEvent = new MouseEvent("click", {
            bubbles: true,
            cancelable: true
        });
        // 派发(click)触发点击事件
        button.dispatchEvent(clickEvent);
    }
    // 监听某个元素内容变化
    let elementChange = {
        existCheck(select,timeout = 6000) {
           return new Promise((resolve,reject)=>{
              let timer = null;
              timer = setInterval(()=>{
                 let element = document.querySelector(select);
                 if( element != null) {
                     resolve(element);
                     clearInterval(timer);
                 };
              },100)
              setTimeout(()=>{clearInterval(timer);},timeout)
           })
        },
        hasContentCheck(select,count = 1,timeout = 6000) {
           return new Promise((resolve,reject)=>{
              let timer = null;
              timer = setInterval(()=>{
                 let element = document.querySelector(select);
                 let isHasContent = false;
                 if(element == null) return;
                 let innerText = element.innerText;
                 isHasContent = element.childNodes.length >= count && innerText != "" && ! /^\s*<!--[^<>]*-->\s*$/.test(innerText);
                 if( isHasContent ) {
                    resolve(element);
                    clearInterval(timer);
                 }
              },100)
              setTimeout(()=>{clearInterval(timer);},timeout)
           })
        }
    }

    //=== 脚本主逻辑 ===>

    let pList = null;
    let TP_CACHE_KEY = null;
    let WHEN_SAVING_P_CACHE_KEY = null;
    let currentEpisodes = null;
    let controlElement = null; // 视图节点对象
    function refreshVideoInfo() {
       // 刷新是否多集
       pList = document.querySelector("#multi_page > div.cur-list > ul");
       let oldVideoId = TP_CACHE_KEY;
       let currentVideoId = TP_CACHE_KEY = getVideoId()
       WHEN_SAVING_P_CACHE_KEY = TP_CACHE_KEY+":WHEN_SAVING_P_CACHE_KEY"
       let isVideoChange = oldVideoId != currentVideoId;
    }
    // 刷新视频信息
    refreshVideoInfo();
    // 【程序入口】等待集数目录加载完成-初始化视图
    elementChange.hasContentCheck("#multi_page > div.cur-list > ul > li:nth-child(1)").then(()=>{
        // 集数目录加载完时,执行初始化视图(如果视图比集数目录显示在前面,可能集数行内容空白)
        initView();
        // url改变时
        onUrlChange(()=>{
           if(TP_CACHE_KEY == null) return;
           let videoIdChange = refreshVideoInfo();
           let videoPChange = ! window.location.href.includes("p="+currentEpisodes);
           if( videoPChange || videoIdChange ) {
               if(pList == null && controlElement != null) {
                   // 多集视频 -> 单视频  执行
                   controlElement.remove()
                   controlElement = null;
               }else if( pList != null && controlElement == null){
                   // 单视频 -> 多集视频时 执行
                   initView();
                   return;
               }
               // 多集视频时集数切换 执行更新视图变量
               if(pList != null) refreshViewState()
           }
        })
    })

    if(pList == null || currentIsIframe() || TP_CACHE_KEY == null) return;

    // -- 是集合(有集数)的视频 --
    // 视图初始化
    function initView () {
        // 之前的集数
        let tp = cache.get(TP_CACHE_KEY)??0;
        let multiPage = document.querySelector("#multi_page");

        let inputStyle = `
           height: 20px;
           border-radius: 5px;
           border: 1.5px solid pink;
           padding: 2px 5px;
           box-sizing: border-box;
           max-width: 60px;
        `

        // 创建新的 <div> 元素
        controlElement = document.createElement('div');
        // 视图容器样式
        controlElement.style = `
               margin: 10px 0px;
               line-height:25px;
               color:#FB7299;
               font-weight: 500;
            `

        let dataAttrName = getDataAttributes(document.querySelector("#multi_page"))[0]
        controlElement.innerHTML = `
              <span >当前P<span id="current_episodes">--</span> , 本次目标P</span>
              <input type="number" style="${inputStyle}" value="${tp}" id="tp_input" ${dataAttrName}="" />
              <span id="tp_msg">--</span>
        `
        // 在目标元素前插入新的兄弟元素
        multiPage.insertAdjacentElement('beforebegin', controlElement);

        // 使用防抖修改内容
        let tpInput = document.querySelector('#tp_input');
        let refresh = debounce(()=>{
            // 在这里编写输入值改变事件的处理逻辑
            cache.set(TP_CACHE_KEY,parseInt(tpInput.value));
            cache.set(WHEN_SAVING_P_CACHE_KEY,currentEpisodes);
            refreshViewState();
        },1500)
        tpInput.addEventListener('input', ()=>refresh());
        refreshViewState();
    }
    // 更新视图状态
    async function refreshViewState() {
        // 当前集数
        currentEpisodes= await new Promise((resolve,reject)=>{
            let timer = null;
            timer = setInterval(()=>{
                let activeItem = document.querySelector("#multi_page > div.cur-list > ul .watched,.on .page-num")
                let episodes = null;
                if(activeItem == null) {
                   clearInterval(timer);
                   resolve(null)
                   return;
                }
                episodes = parseInt(activeItem.innerText.replace("P",""))
                if(episodes != null && episodes >= 1) {
                    clearInterval(timer)
                    resolve(episodes)
                }
            },100)
        })
        let tpInput = document.querySelector('#tp_input');
        let tp = cache.get(TP_CACHE_KEY)??0;
        let tpMsg = document.querySelector('#tp_msg');
        let currentEpisodesElement = document.querySelector('#current_episodes');
        let residueP = tp-currentEpisodes;
        let whenSavingP = cache.get(WHEN_SAVING_P_CACHE_KEY);
        let sumP = whenSavingP === undefined?"--":(tp - whenSavingP + 1);
        let viewed = (typeof sumP === "string")?"--":(sumP - residueP - 1);

        if( tpInput != null )tpInput.value = tp;
        currentEpisodesElement.innerHTML = `${currentEpisodes}`
        let statusMsg = (viewed >= sumP)? (tp == 0?"第一步设置目标!":"太棒了,任务完成了!") :"看完当前+1" ;
        tpMsg.innerHTML = ` , 进度 ${viewed}/${sumP}集!${statusMsg}`;
        // 检查
        if(tp == 0) return; // 没有设置目标值
        if(currentEpisodes >= tp+1) {
            setTimeout(()=>{
                play(false);
                alert(`你已经达到本次任务!${currentEpisodes > tp+1?"请更新目标":""}`)
            },100); // 设置状态为暂停
        }



    }
    // === 扩展功能-暂停与自动播放控制===
    (()=>{
        // 当页面失去焦点时播放,活动时播放(前提是自动关闭的)
        let isIntervene = false;
        document.addEventListener("visibilitychange", function() {
            if (document.visibilityState === "visible") {
                // 活动
                if(isIntervene) { // 只有干预过,才可自动恢复播放
                    play(true);
                    isIntervene = false; // 重置为未干预
                }
            } else if(getPlayStatus()){
                // 不活动 & 在播放时
                isIntervene = true; // 设置为已干预
                play(false);
            }
        });
    })()

})();