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