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