// ==UserScript==
// @name 国家开放大学实验学院/全网办/乡村振兴学院加速自动刷视频
// @namespace ouchn-syxy-auto
// @version 2.2.4
// @description 国开实验学院全自动刷课,支持倍速切换、自动翻页、自动续跑
// @author 去有风的地方
// @match *://moodle.syxy.ouchn.cn/*
// @grant none
// @run-at document-end
// @license All Rights Reserved
// ==/UserScript==
(function () {
'use strict';
const CONFIG = {
speed: 2,
mute: true,
delay: 1800, // 课页内翻页延迟(ms)
apiInterval: 2500 // API模式:每节课之间的间隔(ms),防触发风控
};
let logBox;
let isDragging = false;
let offsetX, offsetY;
let stopFlag = false;
// ---------- 日志 ----------
function log(msg, type = "info") {
const t = new Date().toLocaleTimeString();
const color = { info: "#88ccff", success: "#73d18c", warn: "#ffd173", error: "#ff7373" }[type];
if (logBox) {
logBox.innerHTML += `
[${t}] ${msg}
`;
logBox.scrollTop = logBox.scrollHeight;
}
console.log(`[全自动程序] ${msg}`);
}
// ---------- 拖拽 ----------
function makeDraggable(el) {
el.style.cursor = "move";
el.addEventListener("mousedown", e => {
isDragging = true;
offsetX = e.clientX - el.getBoundingClientRect().left;
offsetY = e.clientY - el.getBoundingClientRect().top;
});
document.addEventListener("mousemove", e => {
if (!isDragging) return;
el.style.left = e.clientX - offsetX + "px";
el.style.top = e.clientY - offsetY + "px";
el.style.right = "auto";
});
document.addEventListener("mouseup", () => isDragging = false);
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
// ---------- 控制面板 ----------
function createPanel(showApiBtn) {
const panel = document.createElement("div");
panel.style = `
position:fixed;top:20px;right:20px;z-index:9999999;
width:340px;background:#1f2937;color:#fff;padding:14px;border-radius:12px;
box-shadow:0 4px 20px rgba(0,0,0,0.3);font-size:13px;user-select:none;
`;
panel.innerHTML = `
国开实验学院全自动程序 v3.0
播放倍速
${showApiBtn ? `
` : ""}
答题或批量合作,联系Q:640105435
Q群1:949193546丨Q群2:756253160
`;
document.body.appendChild(panel);
makeDraggable(panel);
logBox = document.getElementById("logBox");
document.getElementById("startBtn").onclick = startAll;
document.getElementById("stopBtn").onclick = () => { stopFlag = true; log("⏹ 已请求停止,当前任务结束后停下", "warn"); };
document.getElementById("speedSel").onchange = e => {
CONFIG.speed = Number(e.target.value);
log(`已切换倍速:${CONFIG.speed}x`);
};
if (showApiBtn) document.getElementById("apiBtn").onclick = apiCompleteAll;
}
// =========================================================
// 模式一:API 直连批量完成(在课程首页使用)
// =========================================================
// 获取视频真实时长(只加载元数据,不下载视频)
function getVideoDuration(uploadid) {
return new Promise(resolve => {
const v = document.createElement("video");
v.preload = "metadata";
v.muted = true;
const timer = setTimeout(() => { v.src = ""; resolve(0); }, 8000);
v.onloadedmetadata = () => { clearTimeout(timer); resolve(v.duration || 0); };
v.onerror = () => { clearTimeout(timer); resolve(0); };
v.src = `/local/one/video_preview.php?uploadid=${uploadid}`;
});
}
// 抓取单个课页并上报完成
async function processPage(id, title) {
// 1. 访问页面(服务器会记录"已查看",文档页到此即完成)
const resp = await fetch(`/mod/page/view.php?id=${id}&forceview=1`, { credentials: "include" });
const html = await resp.text();
// 2. 提取视频追踪参数
const mKey = html.match(/videokey:\s*'(\d+_\d+_\d+)'/);
const mSign = html.match(/sign:\s*'([0-9a-f]{32})'/);
const mUp = html.match(/video_preview\.php\?uploadid=(\d+)/);
if (!mKey || !mSign) {
log(`📄 [${id}] ${title} —— 纯文档页,已标记查看`, "success");
return true;
}
// 3. 有视频:获取真实时长后上报"看到结尾"
let duration = 0;
if (mUp) duration = await getVideoDuration(mUp[1]);
const position = duration > 10 ? Math.floor(duration - 1) : 99999;
const r = await fetch("/customscripts/mod/playbackposition.php", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ position, videokey: mKey[1], sign: mSign[1] })
});
if (r.ok) {
log(`🎬 [${id}] ${title} —— 视频已上报完成(时长${Math.round(duration)}s)`, "success");
return true;
} else {
log(`⚠️ [${id}] ${title} —— 上报返回 ${r.status},建议用挂机模式补刷`, "warn");
return false;
}
}
async function apiCompleteAll() {
stopFlag = false;
document.getElementById("apiBtn").disabled = true;
log("===== API 批量完成开始 =====");
// 从当前课程首页收集所有 mod/page 链接(视频/文档都是"网页"类型)
const seen = new Set();
const items = [];
document.querySelectorAll('a[href*="/mod/page/view.php"]').forEach(a => {
const m = a.href.match(/id=(\d+)/);
if (m && !seen.has(m[1])) {
seen.add(m[1]);
const title = (a.textContent || "").trim().replace(/\s*网页\s*$/, "").slice(0, 30);
items.push({ id: m[1], title });
}
});
if (items.length === 0) {
log("❌ 当前页未找到课程章节链接,请到课程首页(course/view.php)再点此按钮", "error");
document.getElementById("apiBtn").disabled = false;
return;
}
log(`共发现 ${items.length} 个网页节,开始逐个处理...`);
let ok = 0, fail = 0;
for (let i = 0; i < items.length; i++) {
if (stopFlag) { log("⏹ 已停止", "warn"); break; }
try {
const good = await processPage(items[i].id, items[i].title);
good ? ok++ : fail++;
} catch (e) {
fail++;
log(`❌ [${items[i].id}] ${items[i].title} —— ${e.message}`, "error");
}
log(`—— 进度 ${i + 1}/${items.length} ——`);
await sleep(CONFIG.apiInterval);
}
log(`===== 完成:成功 ${ok},失败/跳过 ${fail} =====`, ok > 0 ? "success" : "warn");
log("提示:测验/作业/讨论需人工完成;2分钟后刷新首页查看进度", "info");
document.getElementById("apiBtn").disabled = false;
}
// =========================================================
// 模式二:课页挂机(自动播放+自动翻页,含第一页修复)
// =========================================================
async function autoScroll() {
return new Promise(resolve => {
log("正在自动浏览页面...");
let pos = 0;
const interval = setInterval(() => {
window.scrollBy(0, 120);
pos += 120;
if (pos >= document.body.scrollHeight - 500) {
clearInterval(interval);
setTimeout(() => { log("✅ 页面处理完成", "success"); resolve(); }, 1000);
}
}, 120);
});
}
async function playVideo() {
return new Promise(async resolve => {
let video = document.querySelector("video");
if (!video) {
for (const f of document.querySelectorAll("iframe")) {
try {
const v = f.contentDocument?.querySelector("video");
if (v) { video = v; break; }
} catch (e) { }
}
}
if (!video) return resolve(false);
video.playbackRate = CONFIG.speed;
video.muted = CONFIG.mute;
log(`检测到视频,${CONFIG.speed}倍速自动播放`);
try { await video.play(); } catch (e) { }
video.onended = () => {
log("✅ 视频播放完毕", "success");
goNext();
resolve(true);
};
video.onpause = () => {
if (!video.ended) setTimeout(() => video.play().catch(() => { }), 600);
};
});
}
// --- 修复:通用"下一页"按钮查找(原版写死哈希class导致第一页失效) ---
function isVisible(el) {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}
function findNextInDoc(doc) {
if (!doc) return null;
const candidates = [...doc.querySelectorAll('a[class*="newgk-next"], button[class*="newgk-next"], a[class*="next"], button[class*="next"]')];
for (const el of candidates) {
const cls = String(el.className || "");
const txt = (el.textContent || "").trim();
if (/prev|pre-|上一页|上一节/i.test(cls) || /上一页|上一节/.test(txt)) continue;
if (isVisible(el)) return el;
}
const byText = [...doc.querySelectorAll("a, button")].filter(el => {
const txt = (el.textContent || "").trim();
return /^(下一页|下一节|下一步|继续学习)\s*[›»>]?$/.test(txt) && !/上一页|上一节/.test(txt) && isVisible(el);
});
return byText[0] || null;
}
function findNextBtn() {
let btn = findNextInDoc(document);
if (btn) return btn;
for (const f of document.querySelectorAll("iframe")) {
try {
btn = findNextInDoc(f.contentDocument);
if (btn) return btn;
} catch (e) { }
}
return null;
}
function goNext() {
setTimeout(() => {
const btn = findNextBtn();
if (btn) {
log("➡️ 自动点击下一页", "info");
btn.click();
} else {
log("⚠️ 未找到下一页按钮:可能是最后一页", "warn");
}
}, CONFIG.delay);
}
async function startAll() {
if (location.href.includes("/course/view.php")) {
log("当前是课程首页:请点蓝色按钮用 API 批量完成,无需挂机", "info");
return;
}
log("===== 课页挂机任务开始 =====");
const hasVideo = await playVideo();
if (hasVideo) return;
await autoScroll();
goNext();
}
// ---------- 入口 ----------
window.addEventListener("load", () => {
const isCourseHome = location.href.includes("/course/view.php");
createPanel(isCourseHome);
setTimeout(() => {
if (!isCourseHome && document.getElementById("autoStart").checked) {
log("🔄 页面加载完成,自动执行挂机任务", "success");
startAll();
} else if (isCourseHome) {
log("🔄 课程首页就绪,点击蓝色按钮一键 API 完成", "success");
}
}, 1200);
});
})();