您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
让你自律地看多集视频
// ==UserScript== // @name B站大学课程辅助器 // @namespace http://tampermonkey.net/ // @version 3.0 // @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,isImmediately = false) { let initUrl = window.location.href.split("#")[0]; function urlChangeCheck() { let currentUrl = window.location.href.split("#")[0]; if (initUrl != currentUrl) { // 新的=>旧的 initUrl = currentUrl; fun(); } } if(isImmediately) fun(); 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); } }, 200); 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); } }, 200); 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; // 视图节点对象 // 页面改变需要修改的元素选择器 let pageElementSelector = { pListBox: ".rcmd-tab", // 集数列表盒子,脚本控制器会放在它上面 p2: ".video-pod__body > div > div:nth-child(2)", //也决定了是否为多集视频 currentP: ".amt" // innerHtml应是(n/m)这种才能解析,否则需要修改逻辑 }; // 刷新视频信息 function refreshVideoInfo() { // 存放集列表的盒子,如果有证明是多集视频(page-change-change) // 脚本控制器将放在这上面,且证明是否多集 pList = document.querySelector(pageElementSelector.pListBox); 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; } // 视图初始化 function initView() { // 之前的集数 let tp = cache.get(TP_CACHE_KEY) ?? 0; 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; `; // 加data就可以让内容可以选中使用,不然都不能选中,如input如何聚焦编辑 let dataAttrName = getDataAttributes(pList)[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> `; // 在目标元素前插入新的兄弟元素 setTimeout(()=>{ // 这里必须等待页面,否则页面将功能异常(由向页面插入元素引起) pList.before(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(); }, 1000); refreshViewState(); tpInput.addEventListener('input', () => refresh()); },2000) } function refreshControlVisibility() { const p2 = document.querySelector(pageElementSelector.p2) if (p2 == null && controlElement != null) { console.log("1.1 refreshControlVistor") // 多集视频 -> 单视频 执行 controlElement?.remove(); controlElement = null; } else if(p2 != null && controlElement == null){ console.log("1.2 refreshControlVistor") // 单视频 -> 多集视频时 执行 initView(); } } // 更新视图状态 async function refreshViewState() { refreshVideoInfo(); refreshControlVisibility(); // 当前集数 currentEpisodes = await new Promise((resolve, reject) => { let timer = null; timer = setInterval(() => { let activeItem = document.querySelector(pageElementSelector.currentP); if (activeItem == null) return; const text = activeItem.innerText; const regex = /(\d+)\/(\d+)/; // 提取分子和分母 const match = text.match(regex); if (!match) return; const current = parseInt(match[1], 10); // 当前进度 const total = parseInt(match[2], 10); // 总进度 console.log(`当前进度: ${current}, 总进度: ${total}`); if (activeItem == null) { clearInterval(timer); resolve(null); return; } if (current != null && current >= 1) { clearInterval(timer); resolve(current); } }, 200); }); 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) return; tpInput.value = tp; currentEpisodesElement.innerHTML = `${currentEpisodes}`; let statusMsg = (viewed >= sumP) ? (tp == 0 ? "第一步设置目标!" : "太棒了,任务完成了!") : "看完当前+1"; tpMsg.innerHTML = ` , 进度 ${viewed}/${sumP}集!${statusMsg}`; debugger // 检查 if (tp == 0) return; // 没有设置目标值 if (currentEpisodes >= tp + 1) { setTimeout(() => { play(false); alert(`你已经达到本次任务!${currentEpisodes > tp + 1 ? "请更新目标" : ""}`); }, 100); // 设置状态为暂停 } } // === 扩展功能-暂停与自动播放控制 === function setupVisibilityChangeListener() { // 当页面失去焦点时暂停,活动时播放(前提是自动关闭的) document.addEventListener("visibilitychange", function() { if (document.visibilityState === "visible") { // 活动 if (isIntervene) { // 只有干预过,才可自动恢复播放 play(true); isIntervene = false; // 重置为未干预 } } else if (getPlayStatus()) { // 不活动 & 在播放时 isIntervene = true; // 设置为已干预 play(false); } }); } // 全局变量,用于扩展功能控制 let isIntervene = false; // ========== 主函数区 ========== function main() { // 如果处于iframe内则不执行脚本 if (currentIsIframe()) return; // 程序入口:等待集数目录加载完成-初始化视图 window.onload = function() { // 集数目录加载完时,执行初始化视图(如果视图比集数目录显示在前面,可能集数行内容空白) onUrlChange(() => refreshViewState(),true) }; // 设置扩展功能 setupVisibilityChangeListener(); } // 调用主函数启动程序 main(); })();