// ==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