// ==UserScript== // @name 蕴瑜课堂自动播放 - 通用版 // @namespace http://tampermonkey.net/ // @version 7.0 // @description 自动完成视频,支持课程索引中的任意 fsresource // @match https://courses.gdut.edu.cn/course/view.php?id=* // @match https://courses.gdut.edu.cn/mod/fsresource/view.php?id=* // @grant none // ==/UserScript== (function() { 'use strict'; const TARGET = 90; const CHECK_INTERVAL = 3000; const LONG_PRESS_MS = 6500; const MAX_EMPTY = 3; const STUCK_CHECK_INTERVAL = 60000; let isNavigating = false; let emptyCount = 0; let isLongPressing = false; let playRetry = 0; let lastProgress = -1; let lastWatchTime = -1; let stuckTimer = null; function log(msg) { console.log(`[自动] ${new Date().toLocaleTimeString()} ${msg}`); } // 获取课程索引中的所有 fsresource(不限制名称,但会排除明显的非视频如 .pdf, .docx) function getVideoList() { const container = document.querySelector('#courseindex, .courseindex'); if (!container) return null; const items = container.querySelectorAll('li.courseindex-item'); const videos = []; for (let item of items) { const link = item.querySelector('a.courseindex-link'); if (!link) continue; const href = link.href; if (!href || !href.includes('/mod/fsresource/view.php')) continue; const name = link.innerText || ''; // 过滤掉明显的文档(.pdf, .docx, .ppt)和测试(包含“测试”二字) if (name.includes('.pdf') || name.includes('.docx') || name.includes('.ppt') || name.includes('测试')) { continue; } const compSpan = item.querySelector('.completioninfo'); let completed = false; if (compSpan) { if (compSpan.classList.contains('completion_complete') || compSpan.getAttribute('data-value') === '1') { completed = true; } } const idMatch = href.match(/id=(\d+)/); if (idMatch) { videos.push({ id: parseInt(idMatch[1]), url: href, name: name, completed: completed }); } } return videos.length ? videos : null; } function getCurrentId() { const m = location.search.match(/[?&]id=(\d+)/); return m ? parseInt(m[1]) : 0; } // 获取课程ID(从URL或面包屑) function getCourseId() { let m = location.pathname.match(/\/course\/view\.php\?id=(\d+)/); if (m) return m[1]; const breadcrumb = document.querySelector('.breadcrumb a[href*="/course/view.php"]'); if (breadcrumb) { let mm = breadcrumb.href.match(/id=(\d+)/); if (mm) return mm[1]; } return null; } // 跳转到课程主页 function backToCourse() { let cid = getCourseId(); if (cid) { location.href = `/course/view.php?id=${cid}`; } else { alert('无法返回课程主页,请手动进入'); } } function goToNext() { if (isNavigating) return; isNavigating = true; let videos = getVideoList(); if (!videos) { log('索引未加载,稍后重试'); setTimeout(() => { isNavigating = false; goToNext(); }, 1000); return; } const curId = getCurrentId(); let curIdx = videos.findIndex(v => v.id === curId); if (curIdx === -1) curIdx = 0; let nextIdx = -1; for (let i = curIdx + 1; i < videos.length; i++) { if (!videos[i].completed) { nextIdx = i; break; } } if (nextIdx === -1) { for (let i = 0; i < curIdx; i++) { if (!videos[i].completed) { nextIdx = i; break; } } } if (nextIdx === -1) { log('🎉 所有视频已完成!'); stopStuckDetection(); isNavigating = false; return; } const next = videos[nextIdx]; log(`跳转: ${next.name} (ID=${next.id})`); stopStuckDetection(); location.href = next.url; setTimeout(() => { isNavigating = false; }, 3000); } function isNonVideo() { const title = document.querySelector('.page-header-headings h1, .activity-title'); if (title) { const txt = title.innerText; if (txt.includes('PDF') || txt.includes('文档') || txt.includes('测试')) return true; } return false; } function simulateLongPress() { if (isLongPressing) return; const btns = document.querySelectorAll('button, .btn'); for (let btn of btns) { if (!btn.offsetParent) continue; const text = btn.innerText || btn.textContent; if (text.includes('按住') || text.includes('长按')) { log(`长按: ${btn.id || '无id'}`); isLongPressing = true; const rect = btn.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; const opts = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y, buttons: 1 }; btn.dispatchEvent(new PointerEvent('pointerdown', opts)); btn.dispatchEvent(new MouseEvent('mousedown', opts)); btn.dispatchEvent(new TouchEvent('touchstart', { bubbles: true, touches: [new Touch({ identifier: Date.now(), target: btn, clientX: x, clientY: y })] })); setTimeout(() => { btn.dispatchEvent(new PointerEvent('pointerup', opts)); btn.dispatchEvent(new MouseEvent('mouseup', opts)); btn.dispatchEvent(new TouchEvent('touchend', opts)); btn.dispatchEvent(new MouseEvent('click', opts)); isLongPressing = false; }, LONG_PRESS_MS); break; } } } function dismissPopups() { const keywords = ['确定', '确认', '继续', '我知道了', '关闭']; document.querySelectorAll('button, .btn').forEach(btn => { if (!btn.offsetParent) return; const text = btn.innerText || btn.textContent; if (keywords.some(kw => text.includes(kw))) { btn.click(); log(`弹窗: ${text}`); } }); } async function ensurePlay(video) { if (!video) return false; if (!video.paused) return true; video.muted = true; video.dispatchEvent(new MouseEvent('click', { bubbles: true })); simulateLongPress(); try { await video.play(); playRetry = 0; log('视频开始播放'); return true; } catch (err) { log(`播放失败: ${err.message}`); if (playRetry++ < 3) { setTimeout(() => ensurePlay(video), 1500); } return false; } } function getProgressAndCompletion() { const completedSpan = document.querySelector('.tips-completion'); if (completedSpan && completedSpan.innerText.includes('已完成')) { return { progress: 100, completed: true }; } const progressSpan = document.querySelector('.num-bfjd span'); if (progressSpan) { let val = parseFloat(progressSpan.innerText); if (!isNaN(val)) return { progress: val, completed: val >= TARGET }; } const video = document.querySelector('video'); if (video && video.duration && video.duration !== Infinity) { let val = (video.currentTime / video.duration) * 100; return { progress: val, completed: val >= TARGET }; } return { progress: 0, completed: false }; } function getProgressAndWatchTime() { let progress = -1, watchTime = -1; const progSpan = document.querySelector('.num-bfjd span'); if (progSpan) { let val = parseFloat(progSpan.innerText); if (!isNaN(val)) progress = val; } const timeSpan = document.querySelector('.num-gksc span'); if (timeSpan) { let val = parseInt(timeSpan.innerText, 10); if (!isNaN(val)) watchTime = val; } return { progress, watchTime }; } function checkStuck() { if (isNavigating) return; const { progress, watchTime } = getProgressAndWatchTime(); if (progress === -1 || watchTime === -1) return; if (lastProgress !== -1 && lastWatchTime !== -1) { if (progress === lastProgress && watchTime === lastWatchTime) { log(`检测到卡死(进度=${progress}%,时长=${watchTime}秒),刷新页面`); location.reload(); return; } } lastProgress = progress; lastWatchTime = watchTime; log(`卡死检测:进度=${progress}%,时长=${watchTime}秒`); } function startStuckDetection() { if (stuckTimer) clearInterval(stuckTimer); lastProgress = -1; lastWatchTime = -1; stuckTimer = setInterval(() => { if (location.pathname.includes('/mod/fsresource/view.php')) { checkStuck(); } }, STUCK_CHECK_INTERVAL); } function stopStuckDetection() { if (stuckTimer) { clearInterval(stuckTimer); stuckTimer = null; } } async function handleVideoPage() { dismissPopups(); simulateLongPress(); if (isNonVideo()) { log('非视频内容,跳过'); goToNext(); return; } const video = document.querySelector('video'); if (!video) { emptyCount++; if (emptyCount >= MAX_EMPTY) { log(`连续 ${MAX_EMPTY} 次无视频,可能页面错误,返回课程主页`); emptyCount = 0; backToCourse(); } return; } emptyCount = 0; await ensurePlay(video); const { progress, completed } = getProgressAndCompletion(); log(`进度: ${progress.toFixed(1)}%, 已完成: ${completed}`); if (completed || progress >= TARGET) { log('达到目标,跳转下一个'); goToNext(); } } function handleCoursePage() { let videos = getVideoList(); if (!videos) { log('课程索引尚未加载,等待1秒后重试'); setTimeout(handleCoursePage, 1000); return; } const firstUnfinished = videos.find(v => !v.completed); if (firstUnfinished) { log(`开始第一个未完成视频: ${firstUnfinished.name}`); location.href = firstUnfinished.url; } else { log('所有视频已完成!'); } } // 入口 if (location.pathname.includes('/course/view.php')) { setTimeout(handleCoursePage, 2000); } else if (location.pathname.includes('/mod/fsresource/view.php')) { startStuckDetection(); let waitTimes = 0; function waitForIndexAndStart() { if (getVideoList()) { handleVideoPage(); } else if (waitTimes++ < 6) { setTimeout(waitForIndexAndStart, 500); } else { log('索引加载超时,但仍尝试执行视频逻辑(可能页面无左侧索引)'); handleVideoPage(); } } waitForIndexAndStart(); } setInterval(() => { if (location.pathname.includes('/mod/fsresource/view.php')) { dismissPopups(); simulateLongPress(); const { progress, completed } = getProgressAndCompletion(); if (completed || progress >= TARGET) { log('定时检测达标,立即跳转'); goToNext(); } } }, CHECK_INTERVAL); })();