tampermonkey_script/qingshuxuetang_auto_play.us...

298 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==UserScript==
// @name 平顶山学院(青书学堂)自动学习脚本
// @namespace http://tampermonkey.net/
// @version 2026-06-03
// @description try to take over the world!
// @author You
// @match https://degree.qingshuxuetang.com/pdsu/Student/Course/CourseStudy*
// @match https://degree.qingshuxuetang.com/pdsu/Student/Course/CourseShow*
// @icon https://www.google.com/s2/favicons?sz=64&domain=qingshuxuetang.com
// @updateURL https://git.zaobai.com/public/tampermonkey_script/raw/branch/master/qingshuxuetang_auto_play.user.js
// @downloadURL https://git.zaobai.com/public/tampermonkey_script/raw/branch/master/qingshuxuetang_auto_play.user.js
// @grant none
// ==/UserScript==
(function () {
"use strict";
const url = window.location.href;
const MIN_STUDY_SECONDS = 5 * 60;
const VIDEO_SELECTOR = "#vjs_video_3_html5_api";
const TEXT_SELECTOR = "#easyXDM_default2872_provider";
const TOAST_CONTAINER_ID = "qingshu-auto-toast-container";
const TOAST_STYLE_ID = "qingshu-auto-toast-style";
const TOAST_DEFAULT_DURATION = 4000;
// 创建右下角 toast 容器和样式。
// 每次通知都会创建一个新的 toast所以多个通知会一起堆叠显示。
const ensureToastContainer = () => {
let container = document.querySelector("#" + TOAST_CONTAINER_ID);
if (!document.querySelector("#" + TOAST_STYLE_ID)) {
const style = document.createElement("style");
style.id = TOAST_STYLE_ID;
style.textContent = `
#${TOAST_CONTAINER_ID} {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 2147483647;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
max-width: min(360px, calc(100vw - 36px));
pointer-events: none;
}
#${TOAST_CONTAINER_ID} .qingshu-toast {
box-sizing: border-box;
min-width: 220px;
max-width: 100%;
padding: 10px 12px;
border-left: 4px solid #3b82f6;
border-radius: 8px;
background: rgba(17, 24, 39, 0.94);
color: #fff;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.24);
font-size: 13px;
line-height: 1.45;
opacity: 0;
transform: translateY(10px);
transition: opacity 180ms ease, transform 180ms ease;
pointer-events: auto;
word-break: break-word;
}
#${TOAST_CONTAINER_ID} .qingshu-toast.show {
opacity: 1;
transform: translateY(0);
}
#${TOAST_CONTAINER_ID} .qingshu-toast.success {
border-left-color: #22c55e;
}
#${TOAST_CONTAINER_ID} .qingshu-toast.warning {
border-left-color: #f59e0b;
}
#${TOAST_CONTAINER_ID} .qingshu-toast.error {
border-left-color: #ef4444;
}
`;
(document.head || document.documentElement).appendChild(style);
}
if (!container) {
container = document.createElement("div");
container.id = TOAST_CONTAINER_ID;
(document.body || document.documentElement).appendChild(container);
}
return container;
};
// 显示一条 toast同时保留 console 输出。
// type 可用 info、success、warning、errorduration 是自动隐藏时间。
const notify = (message, type = "info", duration = TOAST_DEFAULT_DURATION) => {
const log = type === "error" ? console.error : console.log;
log(message);
const container = ensureToastContainer();
const toast = document.createElement("div");
toast.className = "qingshu-toast " + type;
toast.innerText = message;
container.appendChild(toast);
requestAnimationFrame(() => {
toast.classList.add("show");
});
const removeToast = () => {
toast.classList.remove("show");
setTimeout(() => {
toast.remove();
}, 180);
};
toast.addEventListener("click", removeToast);
setTimeout(removeToast, duration);
};
notify("插件加载完成,等待界面渲染", "info");
// 等待某个 DOM 条件成立。
// 页面里的课程列表、视频、文本课件经常是后加载的,所以不用固定延迟,
// 而是监听 DOM 变化,直到 getResult() 返回有效结果。
const waitForCondition = (getResult) => {
const result = getResult();
if (result) {
return Promise.resolve(result);
}
return new Promise((resolve) => {
const observer = new MutationObserver(() => {
const result = getResult();
if (result) {
observer.disconnect();
resolve(result);
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
});
};
// 把“共4分钟49秒”“共1小时2分钟3秒”这类文字换算成秒数
// 方便判断是否已经学满 5 分钟。
const parseStudySeconds = (text) => {
const hourMatch = text.match(/(\d+)\s*小时/);
const minuteMatch = text.match(/(\d+)\s*分钟/);
const secondMatch = text.match(/(\d+)\s*秒/);
return (
(hourMatch ? Number(hourMatch[1]) * 3600 : 0) +
(minuteMatch ? Number(minuteMatch[1]) * 60 : 0) +
(secondMatch ? Number(secondMatch[1]) : 0)
);
};
// 判断一个课程节点是否需要进入:
// 1. 没有 study_being说明还没学过需要进入
// 2. 学过但累计时间低于 5 分钟,也需要再次进入;
// 3. 学习时间达到 5 分钟及以上,就跳过。
const shouldEnterCourseware = (item) => {
const mark = item.querySelector(".study_being");
if (!mark) {
return true;
}
const studiedSeconds = parseStudySeconds(mark.innerText);
return studiedSeconds < MIN_STUDY_SECONDS;
};
// 返回课程列表页。
// 优先点击页面自带的返回按钮;如果没找到,就用浏览器历史记录兜底。
const returnToStudyPage = () => {
const backLink = document.querySelector(".back-link");
if (backLink) {
backLink.click();
return;
}
history.back();
};
// 播放页可能是视频课件,也可能是文本课件。
// 这里会一直等到其中一种课件元素真正出现在页面上。
const waitForCoursewareElement = () =>
waitForCondition(
() =>
document.querySelector(VIDEO_SELECTOR) ||
document.querySelector(TEXT_SELECTOR)
);
const onPageLoad = async () => {
notify("页面加载完成,开始执行脚本", "info");
if (url.includes("CourseStudy")) {
notify("进入准备阶段,等待课程列表", "info");
// 课程列表是异步渲染的,等到至少有一个课程节点后再开始遍历。
const items = await waitForCondition(() => {
const list = document.querySelector("#list");
const items = list ? list.querySelectorAll("li > a.node") : [];
return items.length > 0 ? items : null;
});
notify("获取到课程数量:" + items.length, "success");
// 从上到下找到第一个“没学过”或“学习不足5分钟”的课件并进入。
for (const item of items) {
if (shouldEnterCourseware(item)) {
const mark = item.querySelector(".study_being");
const studiedSeconds = mark ? parseStudySeconds(mark.innerText) : 0;
notify(
"点击未完成学习时长的视频:" +
item.querySelector(".title").innerText +
",已学习:" +
studiedSeconds +
"秒",
"success",
6000
);
item.click();
return;
}
}
notify("所有视频均已播放完毕,结束脚本", "success", 6000);
} else if (url.includes("CourseShow")) {
notify("进入学习阶段,等待课件加载", "info");
// 等到播放页真正渲染出视频或文本课件,再决定如何处理。
const coursewareElement = await waitForCoursewareElement();
// 文本课件没有 video 标签,不需要播放;停留 5 分钟后返回列表页。
if (coursewareElement.matches(TEXT_SELECTOR)) {
notify("检测到文本课件等待5分钟后返回准备页面", "warning", 6000);
setTimeout(() => {
notify("文本课件等待完成,返回准备页面", "success");
returnToStudyPage();
}, MIN_STUDY_SECONDS * 1000);
return;
}
// 视频课件需要周期性检查播放状态:启动播放、设置倍速、播完返回。
let videoMissingNotified = false;
const videoInterval = setInterval(() => {
const video = document.querySelector(VIDEO_SELECTOR);
// 有些播放器会重建 video 元素,短暂找不到时继续等下一轮。
if (!video) {
if (!videoMissingNotified) {
notify("视频元素暂时不可用,继续等待", "warning");
videoMissingNotified = true;
}
return;
}
videoMissingNotified = false;
if (video.paused && video.currentTime === 0) {
video.play();
notify("视频未播放,开始播放视频", "success");
return;
}
if (video.currentTime >= video.duration - 1) {
notify("视频播放结束,返回准备页面", "success");
clearInterval(videoInterval);
returnToStudyPage();
}
if (video.playbackRate === 1) {
notify("将视频倍速设置为2倍速", "success");
video.playbackRate = 2;
}
console.log("视频正在播放,当前时间:" + video.currentTime);
}, 3000);
}
};
// 等待页面加载完成后执行
if (document.readyState === "complete") {
onPageLoad();
} else {
window.addEventListener("load", onPageLoad);
}
})();