// ==UserScript== // @name 湖南大学继续教育自动播放助手 // @namespace http://tampermonkey.net/ // @version 2.0 // @description 自动播放课程视频,支持倍速、后台计时、自动切换下一节 // @author HNU Helper // @match https://jxjyjd.hnu.edu.cn/* // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ============ 配置项 ============ const CONFIG = { defaultPlaybackRate: 2, // 默认播放倍速 autoNext: true, // 自动切换下一节 retryInterval: 2000, // 重试间隔(毫秒) maxRetries: 100, // 最大重试次数 pollInterval: 1000, // 状态检查间隔(毫秒) debug: false, // 调试模式(控制台输出详细信息) speedOptions: [1, 1.25, 1.5, 2, 3, 4] // 可选的播放速度 }; // ============ 状态管理 ============ const state = { currentVideo: null, videoIndex: 0, totalVideos: 0, completedCount: 0, isRunning: false, retryCount: 0, lastVideoSrc: '', stuckCounter: 0, totalStudyTime: 0, sessionStartTime: null, status: 'idle', currentSpeed: CONFIG.defaultPlaybackRate, videoElements: new Set() }; // ============ 日志函数 ============ function log(message, type = 'info') { if (!CONFIG.debug && type === 'debug') return; const prefix = '[HNU自动播放] '; const styles = { info: 'color: #2196F3; font-weight: bold;', success: 'color: #4CAF50; font-weight: bold;', warning: 'color: #FF9800; font-weight: bold;', error: 'color: #F44336; font-weight: bold;', debug: 'color: #9E9E9E;' }; console.log(`%c${prefix}${message}`, styles[type] || styles.info); } // ============ 创建悬浮控制面板 ============ function createControlPanel() { // 移除已存在的面板 const existingPanel = document.getElementById('hnu-control-panel'); if (existingPanel) { existingPanel.remove(); } const panel = document.createElement('div'); panel.id = 'hnu-control-panel'; panel.innerHTML = `
🎓 自动学习助手
📊 课程进度 0/0
⏱️ 学习时长 00:00
🎬 当前状态 初始化...
🎚️ 当前速度 ${CONFIG.defaultPlaybackRate}x
等待开始...
${CONFIG.defaultPlaybackRate}x
${CONFIG.speedOptions.map(speed => `` ).join('')}
`; document.body.appendChild(panel); // 绑定事件 bindPanelEvents(panel); return panel; } // ============ 绑定面板事件 ============ function bindPanelEvents(panel) { // 倍速按钮 const speedButtons = panel.querySelectorAll('.btn-speed'); speedButtons.forEach(btn => { btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); const speed = parseFloat(this.dataset.speed); log(`点击倍速按钮: ${speed}x`, 'debug'); setPlaybackRate(speed); // 更新按钮状态 speedButtons.forEach(b => b.classList.remove('active')); this.classList.add('active'); // 更新显示 const speedDisplay = document.getElementById('hnu-speed-display'); const speedIndicator = document.getElementById('hnu-speed-indicator'); if (speedDisplay) speedDisplay.textContent = `${speed}x`; if (speedIndicator) speedIndicator.textContent = `${speed}x`; updateStatus(`已切换至 ${speed}x 倍速`, 'success'); }); }); // 暂停/继续按钮 const pauseBtn = panel.querySelector('#hnu-pause-btn'); pauseBtn.addEventListener('click', () => { if (state.isRunning) { pauseVideo(); pauseBtn.textContent = '▶️ 继续'; pauseBtn.classList.remove('running'); updateStatus('已暂停', 'warning'); } else { resumeVideo(); pauseBtn.textContent = '⏸️ 暂停'; pauseBtn.classList.add('running'); updateStatus('已继续', 'success'); } }); // 下一节按钮 const nextBtn = panel.querySelector('#hnu-next-btn'); nextBtn.addEventListener('click', () => { goToNextVideo(); }); // 刷新按钮 const refreshBtn = panel.querySelector('#hnu-refresh-btn'); refreshBtn.addEventListener('click', () => { location.reload(); }); // 折叠面板 const collapseBtn = panel.querySelector('.collapse-btn'); collapseBtn.addEventListener('click', () => { panel.classList.toggle('collapsed'); collapseBtn.textContent = panel.classList.contains('collapsed') ? '▸' : '▾'; }); // 拖拽功能 makeDraggable(panel); } // ============ 面板拖拽 ============ function makeDraggable(panel) { const header = panel.querySelector('.panel-header'); let isDragging = false; let startX, startY, offsetX, offsetY; header.addEventListener('mousedown', (e) => { if (e.target.closest('.collapse-btn')) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panel.getBoundingClientRect(); offsetX = startX - rect.left; offsetY = startY - rect.top; panel.style.transition = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const x = e.clientX - offsetX; const y = e.clientY - offsetY; panel.style.left = x + 'px'; panel.style.top = y + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; panel.style.transition = 'all 0.3s ease'; }); } // ============ 更新面板信息 ============ function updatePanel() { const progressEl = document.getElementById('hnu-progress'); const timeEl = document.getElementById('hnu-time'); const statusEl = document.getElementById('hnu-status'); const fillEl = document.getElementById('hnu-progress-fill'); const statusTextEl = document.getElementById('hnu-status-text'); const speedDisplay = document.getElementById('hnu-speed-display'); const speedIndicator = document.getElementById('hnu-speed-indicator'); if (progressEl) { progressEl.textContent = `${state.completedCount}/${state.totalVideos || 0}`; } if (fillEl && state.totalVideos > 0) { fillEl.style.width = `${(state.completedCount / state.totalVideos) * 100}%`; } if (statusEl) { const statusMap = { 'idle': '💤 待机中', 'playing': '▶️ 播放中', 'paused': '⏸️ 已暂停', 'loading': '🔄 加载中', 'complete': '✅ 已完成', 'error': '❌ 错误' }; statusEl.textContent = statusMap[state.status] || state.status; } if (speedDisplay) { speedDisplay.textContent = `${state.currentSpeed}x`; } if (speedIndicator) { speedIndicator.textContent = `${state.currentSpeed}x`; } } function updateStatus(message, type = 'info') { log(message, type); const statusTextEl = document.getElementById('hnu-status-text'); if (statusTextEl) { statusTextEl.textContent = message; } } // ============ 计时器 ============ function startTimer() { if (!state.sessionStartTime) { state.sessionStartTime = Date.now(); } setInterval(() => { if (state.isRunning) { const currentTime = Date.now(); state.totalStudyTime = Math.floor((currentTime - state.sessionStartTime) / 1000); updateTimerDisplay(); } }, 1000); } function updateTimerDisplay() { const timeEl = document.getElementById('hnu-time'); if (timeEl) { const hours = Math.floor(state.totalStudyTime / 3600); const minutes = Math.floor((state.totalStudyTime % 3600) / 60); const seconds = state.totalStudyTime % 60; timeEl.textContent = `${String(hours).padStart(2,'0')}:${String(minutes).padStart(2,'0')}:${String(seconds).padStart(2,'0')}`; } } // ============ 视频操作 ============ function findVideo() { const videos = document.querySelectorAll('video'); if (videos.length === 0) return null; // 返回第一个可见的视频元素 for (const video of videos) { if (video.offsetParent !== null || video.style.display !== 'none') { return video; } } // 如果没有可见的,返回第一个 return videos[0]; } function getAllVideos() { const videos = document.querySelectorAll('video'); state.totalVideos = videos.length; videos.forEach(v => state.videoElements.add(v)); return videos; } function setPlaybackRate(rate) { state.currentSpeed = rate; CONFIG.defaultPlaybackRate = rate; const videos = document.querySelectorAll('video'); let successCount = 0; videos.forEach(video => { try { video.playbackRate = rate; // 尝试多种方式设置速度 if (video.playbackRate !== rate) { // 某些视频需要先暂停才能设置速度 const wasPaused = video.paused; video.pause(); video.playbackRate = rate; if (!wasPaused) { video.play(); } } successCount++; log(`视频速度已设置为 ${rate}x`, 'debug'); } catch (e) { log(`设置视频速度失败: ${e.message}`, 'error'); } }); if (successCount > 0) { log(`成功设置 ${successCount} 个视频的播放速度为 ${rate}x`, 'success'); updatePanel(); } else { log('未找到视频元素,无法设置速度', 'warning'); } return successCount > 0; } function playVideo() { const video = findVideo(); if (!video) { updateStatus('未找到视频元素,等待加载...', 'warning'); retryPlayVideo(); return false; } try { // 设置播放速度 video.playbackRate = state.currentSpeed; // 解除静音限制(但为了自动播放,可能需要静音) if (video.muted) { video.muted = false; } // 播放 const playPromise = video.play(); if (playPromise) { playPromise.then(() => { state.isRunning = true; state.status = 'playing'; state.retryCount = 0; updateStatus('视频播放中', 'success'); updatePanel(); // 确保速度设置成功 setTimeout(() => { if (video.playbackRate !== state.currentSpeed) { video.playbackRate = state.currentSpeed; log('重新设置播放速度', 'debug'); } }, 100); }).catch(err => { log(`播放失败: ${err.message}`, 'error'); state.status = 'error'; // 尝试静音播放(浏览器策略可能阻止有声播放) video.muted = true; video.play().then(() => { state.isRunning = true; state.status = 'playing'; state.retryCount = 0; updateStatus('视频播放中(已静音)', 'warning'); updatePanel(); }).catch(err2 => { log(`静音播放也失败: ${err2.message}`, 'error'); retryPlayVideo(); }); }); } return true; } catch (e) { log(`播放异常: ${e.message}`, 'error'); retryPlayVideo(); return false; } } function pauseVideo() { const videos = document.querySelectorAll('video'); videos.forEach(video => { try { video.pause(); } catch (e) { log(`暂停视频失败: ${e.message}`, 'debug'); } }); state.isRunning = false; state.status = 'paused'; updatePanel(); log('视频已暂停', 'warning'); } function resumeVideo() { const video = findVideo(); if (video) { video.playbackRate = state.currentSpeed; const playPromise = video.play(); if (playPromise) { playPromise.then(() => { state.isRunning = true; state.status = 'playing'; updatePanel(); log('视频已继续播放', 'success'); }).catch(err => { log(`继续播放失败: ${err.message}`, 'error'); video.muted = true; video.play(); state.isRunning = true; state.status = 'playing'; updatePanel(); }); } } } function retryPlayVideo() { if (state.retryCount >= CONFIG.maxRetries) { updateStatus('重试次数已达上限,请手动检查', 'error'); return; } state.retryCount++; state.status = 'loading'; updatePanel(); updateStatus(`等待视频加载... (第${state.retryCount}次重试)`, 'warning'); setTimeout(() => { playVideo(); }, CONFIG.retryInterval); } // ============ 监听视频事件 ============ function attachVideoListeners(video) { if (!video || video.dataset.hnuListenerAttached) return; video.dataset.hnuListenerAttached = 'true'; state.videoElements.add(video); // 视频结束事件 video.addEventListener('ended', () => { log('当前视频播放完毕', 'success'); state.completedCount++; updatePanel(); if (CONFIG.autoNext) { updateStatus('准备切换到下一节...', 'info'); setTimeout(() => { goToNextVideo(); }, 1000); } else { updateStatus('当前视频已完成', 'success'); } }); // 播放事件 video.addEventListener('playing', () => { state.isRunning = true; state.status = 'playing'; state.retryCount = 0; // 确保播放速度正确 if (video.playbackRate !== state.currentSpeed) { video.playbackRate = state.currentSpeed; log(`播放事件中调整速度到 ${state.currentSpeed}x`, 'debug'); } updatePanel(); }); // 暂停事件 video.addEventListener('pause', () => { if (state.isRunning) { state.isRunning = false; state.status = 'paused'; updatePanel(); log('视频被暂停', 'warning'); // 自动恢复播放 setTimeout(() => { if (!state.isRunning && findVideo()) { log('尝试自动恢复播放...', 'info'); resumeVideo(); } }, 2000); } }); // 等待事件 video.addEventListener('waiting', () => { state.status = 'loading'; updatePanel(); log('视频缓冲中...', 'debug'); }); // 错误事件 video.addEventListener('error', (e) => { log(`视频加载错误: ${e.message || '未知错误'}`, 'error'); state.status = 'error'; updatePanel(); }); // 时间更新事件(用于检测卡死) video.addEventListener('timeupdate', () => { if (state.status === 'playing') { state.stuckCounter = 0; } }); // 速率变化事件 video.addEventListener('ratechange', () => { log(`视频速率已变化为: ${video.playbackRate}x`, 'debug'); }); } // ============ 切换到下一节 ============ function goToNextVideo() { log('查找下一节视频...', 'info'); // 尝试多种方式查找下一节 const strategies = [ // 策略1:查找"下一节"相关按钮 () => { const nextButtons = document.querySelectorAll('button, a, span, div'); for (const btn of nextButtons) { const text = (btn.textContent || '').trim(); const className = (btn.className || '').toString(); const title = (btn.title || ''); const ariaLabel = (btn.getAttribute('aria-label') || ''); if ( (text.includes('下一') || text.includes('下节') || text.includes('下集') || className.includes('next') || title.includes('下一') || ariaLabel.includes('下一')) && text.length < 20 // 避免匹配到长文本 ) { if (!btn.disabled) { log(`找到下一节按钮: "${text}"`, 'success'); btn.click(); return true; } } } return false; }, // 策略2:查找课程列表中的下一个未完成项 () => { const courseItems = document.querySelectorAll('[class*="lesson"], [class*="chapter"], [class*="course"], [class*="video"], li'); let foundCurrent = false; for (const item of courseItems) { const isActive = item.classList.contains('active') || item.classList.contains('current') || item.classList.contains('playing') || item.classList.contains('selected') || item.querySelector('.active'); if (isActive) { foundCurrent = true; continue; } if (foundCurrent && !item.classList.contains('completed') && !item.classList.contains('finished')) { log('找到下一节课程项,正在点击...', 'success'); item.click(); return true; } } return false; }, // 策略3:刷新页面重新开始 () => { log('尝试刷新页面...', 'info'); setTimeout(() => { location.reload(); }, 2000); return true; } ]; // 依次尝试各策略 for (const strategy of strategies) { try { if (strategy()) { // 成功切换到下一节 state.retryCount = 0; setTimeout(() => { const newVideo = findVideo(); if (newVideo) { attachVideoListeners(newVideo); newVideo.playbackRate = state.currentSpeed; playVideo(); } else { // 等待页面加载 setTimeout(() => { const video = findVideo(); if (video) { attachVideoListeners(video); video.playbackRate = state.currentSpeed; playVideo(); } else { updateStatus('下一节视频未找到,请检查', 'error'); } }, 3000); } }, 2000); return; } } catch (e) { log(`切换策略失败: ${e.message}`, 'debug'); } } // 所有策略都失败 updateStatus('未找到下一节,可能已完成所有课程', 'warning'); log('🎉 所有课程已完成!', 'success'); showCompletionNotification(); } // ============ 完成通知 ============ function showCompletionNotification() { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px 40px; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); z-index: 999999; text-align: center; font-family: 'Microsoft YaHei', sans-serif; animation: hnu-popup 0.5s ease; `; notification.innerHTML = `
🎉
恭喜!所有课程已完成
本次学习时长:${formatTime(state.totalStudyTime)}
完成课程数:${state.completedCount} 节
`; document.body.appendChild(notification); // 记录学时到localStorage saveStudyRecord(); } // ============ 学时记录 ============ function saveStudyRecord() { const record = { date: new Date().toISOString(), courseUrl: window.location.href, studyTime: state.totalStudyTime, completedCount: state.completedCount, playbackRate: state.currentSpeed }; // 保存到localStorage let records = JSON.parse(localStorage.getItem('hnu_study_records') || '[]'); records.push(record); localStorage.setItem('hnu_study_records', JSON.stringify(records)); log('学习记录已保存到浏览器', 'success'); console.log('📊 学习记录详情:', record); } // ============ 工具函数 ============ function formatTime(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; let result = ''; if (hours > 0) result += `${hours}小时`; if (minutes > 0) result += `${minutes}分钟`; result += `${secs}秒`; return result; } // ============ 检测卡死 ============ function startStuckDetector() { setInterval(() => { const video = findVideo(); if (video && state.status === 'playing') { state.stuckCounter++; if (state.stuckCounter > 30) { // 30秒无进度 log('检测到视频可能卡死,尝试恢复...', 'warning'); state.stuckCounter = 0; // 尝试重新设置速度并播放 try { video.playbackRate = state.currentSpeed; video.play(); } catch (e) { log(`恢复播放失败: ${e.message}`, 'error'); } } } }, 1000); } // ============ 页面变化监听 ============ function observePageChanges() { const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { const newVideos = document.querySelectorAll('video'); newVideos.forEach(video => { if (!state.videoElements.has(video)) { log('检测到新的视频元素', 'debug'); attachVideoListeners(video); video.playbackRate = state.currentSpeed; } }); // 如果没有在播放且有视频,尝试播放 if (!state.isRunning && newVideos.length > 0) { const video = findVideo(); if (video && video.paused) { playVideo(); } } } } }); observer.observe(document.body, { childList: true, subtree: true }); } // ============ 保持页面活跃(防止浏览器休眠) ============ function keepPageAlive() { // 使用Page Visibility API document.addEventListener('visibilitychange', () => { if (!document.hidden && !state.isRunning) { log('页面重新激活,检查播放状态...', 'debug'); const video = findVideo(); if (video && video.paused) { playVideo(); } } }); // 定期检查播放状态 setInterval(() => { const video = findVideo(); if (video && state.status === 'playing' && video.paused) { log('检测到视频意外暂停,尝试恢复...', 'warning'); playVideo(); } }, 5000); } // ============ 初始化 ============ function init() { log('🚀 湖南大学继续教育自动播放助手已启动 (v2.0)', 'success'); log(`默认倍速: ${CONFIG.defaultPlaybackRate}x`, 'info'); // 创建控制面板 createControlPanel(); state.status = 'idle'; state.currentSpeed = CONFIG.defaultPlaybackRate; updatePanel(); // 启动计时器 startTimer(); // 启动卡死检测 startStuckDetector(); // 监听页面变化 observePageChanges(); // 保持页面活跃 keepPageAlive(); // 初始化视频 const initVideo = () => { const video = findVideo(); if (video) { log('找到视频元素,开始初始化...', 'info'); attachVideoListeners(video); // 设置播放速度 video.playbackRate = state.currentSpeed; log(`设置初始播放速度为 ${state.currentSpeed}x`, 'success'); // 延迟一秒后自动播放 setTimeout(() => { playVideo(); }, 1000); } else { log('未找到视频元素,等待加载...', 'warning'); updateStatus('等待视频加载...', 'info'); } }; // 立即尝试初始化 initVideo(); // 如果立即初始化失败,定期重试 if (!findVideo()) { const videoWaitInterval = setInterval(() => { if (findVideo()) { clearInterval(videoWaitInterval); initVideo(); } }, 1000); // 30秒后停止等待 setTimeout(() => { clear