// ==UserScript==
// @name 福建网络干部学院课程自动播放
// @name:en Fujian Cadre Network College Course Auto Player
// @namespace https://greasyforks.org/users/1410751-liuxing7954
// @version 1.0.0
// @description 自动播放福建网络干部学院课程视频,支持自动切换章节和科目,自动保存进度,自动处理token
// @description:en Auto play Fujian Cadre Network College course videos, auto switch chapters and subjects, auto save progress, auto handle token
// @author liuxing7954
// @match *://www.fsa.gov.cn/video*
// @match https://www.fsa.gov.cn/video*
// @match *://www.fsa.gov.cn/videoChoose*
// @match https://www.fsa.gov.cn/videoChoose*
// @license MIT
// @icon https://www.fsa.gov.cn/favicon.ico
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// 调试模式配置
let DEBUG = localStorage.getItem('debugMode') === 'true' || false; // 从localStorage读取调试状态
const DEBUG_INTERVAL = 5000; // 调试模式下的循环间隔(5秒)
const NORMAL_INTERVAL = 20000; // 正常模式下的循环间隔(20秒)
const TARGET_PROGRESS = 100; // 目标进度值
// 添加初始化标记和上次进度记录
let isFirstCheck = true;
let lastProgress = '';
// 全局变量
let currentCourseMode = localStorage.getItem('courseMode') || 'elective';
// 从localStorage获取studentId
function getStudentId() {
const studentId = localStorage.getItem('studentId');
if (DEBUG) {
log(`获取到studentId: ${studentId}`, true);
}
return studentId;
}
// 创建模拟控制台
function createConsole() {
const consoleDiv = document.createElement('div');
consoleDiv.id = 'custom-console';
consoleDiv.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
width: 450px;
height: 300px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 10px;
border-radius: 5px;
font-family: monospace;
font-size: 12px;
overflow-y: auto;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`;
document.body.appendChild(consoleDiv);
return consoleDiv;
}
// 自定义日志函数
function log(message, isDebugMsg = false) {
if (isDebugMsg && !DEBUG) return;
const consoleDiv = document.getElementById('custom-console');
const logLine = document.createElement('div');
logLine.style.borderBottom = '1px solid rgba(255,255,255,0.1)';
logLine.style.padding = '3px 0';
if (isDebugMsg) {
logLine.style.color = '#aaffaa';
message = '[DEBUG] ' + message;
}
logLine.textContent = `${new Date().toLocaleTimeString()} - ${message}`;
consoleDiv.appendChild(logLine);
consoleDiv.scrollTop = consoleDiv.scrollHeight;
const maxLines = DEBUG ? 20 : 10;
while (consoleDiv.children.length > maxLines) {
consoleDiv.removeChild(consoleDiv.firstChild);
}
if (isDebugMsg) {
console.debug(message);
} else {
console.log(message);
}
}
// 创建初始提示
function showInitialNotification() {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 15px;
border-radius: 5px;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`;
notification.textContent = "脚本已启动";
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 2000);
}
// 获取选修课程列表
async function fetchCourseList() {
const studentId = getStudentId();
const token = localStorage.getItem('wlxytk');
const refreshToken = localStorage.getItem('rt');
if (!studentId || !token || !refreshToken) {
log('未找到必要信息,无法获取课程列表');
if (DEBUG) {
log(`studentId: ${studentId}`, true);
log(`token: ${token}`, true);
log(`refreshToken: ${refreshToken}`, true);
}
return null;
}
const url = getApiUrl();
const requestBody = {
"studentId": studentId,
"size": 30,
"current": 1
};
try {
if (DEBUG) {
log('正在请求课程列表...', true);
log(`请求体: ${JSON.stringify(requestBody)}`, true);
log(`使用token: ${token}`, true);
log(`使用refreshToken: ${refreshToken}`, true);
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token,
'RefreshAuthorization': refreshToken
},
body: JSON.stringify(requestBody)
});
// 检查响应头中的新token
if (response.status === 200) {
const newToken = response.headers.get('authorization');
const newRefreshToken = response.headers.get('refre');
if (newToken) {
localStorage.setItem('wlxytk', newToken);
if (DEBUG) {
log(`更新token: ${newToken}`, true);
}
}
if (newRefreshToken) {
localStorage.setItem('rt', newRefreshToken);
if (DEBUG) {
log(`更新refreshToken: ${newRefreshToken}`, true);
}
}
}
if (!response.ok) {
log(`请求失败: ${response.status} ${response.statusText}`);
return null;
}
const data = await response.json();
if (DEBUG) {
log(`获取���课程列表响应: ${JSON.stringify(data)}`, true);
}
await new Promise(resolve => setTimeout(resolve, 10000));
return data;
} catch (error) {
log(`获取课程列表失败: ${error.message}`);
return null;
}
}
// 查找下一个未完成的科目
async function findNextUnfinishedSubject() {
const courseList = await fetchCourseList();
if (!courseList || !courseList.data || !courseList.data.records) {
log('获取课程列表失败或列表为空');
return null;
}
if (DEBUG) {
log(`开始查找未完成科目,共有 ${courseList.data.records.length} 个科目`, true);
}
// 查找第一个未完成的科目
const nextSubject = courseList.data.records.find(course => {
const isCompleted = course.creditsEarnedNew === course.credit;
if (DEBUG) {
log(`科目: ${course.courseware.coursewareName} - 已学时数: ${course.creditsEarnedNew}/${course.credit} - ${isCompleted ? '已完成' : '未完成'}`, true);
}
return !isCompleted;
});
return nextSubject;
}
// 跳转到指定科目
function navigateToSubject(courseware) {
const url = `https://www.fsa.gov.cn/videoChoose?newId=${courseware.id}`;
if (DEBUG) {
log(`准备跳转到新科目,URL: ${url}`, true);
log(`科目信息: ${JSON.stringify(courseware)}`, true);
}
window.location.href = url;
}
// 修改原有的switchToNextSubject函数
async function switchToNextSubject() {
log('所有课程已完成,正在查找下一个未完成的科目...');
const nextSubject = await findNextUnfinishedSubject();
if (nextSubject) {
log(`找到下一个未完成的科目: ${nextSubject.courseware.coursewareName}`);
log(`当前进度: ${nextSubject.creditsEarnedNew}/${nextSubject.credit}`);
navigateToSubject(nextSubject.courseware);
} else {
log('恭喜!所有科目都已完成!');
}
}
function getCurrentCourseInfo() {
const currentCourse = document.querySelector('.kc-list li h5[style*="color: rgb(166, 0, 0)"]');
if (!currentCourse) {
log('未找到当前课程');
return null;
}
const progressSpan = currentCourse.parentElement.querySelector('.kc-info span:last-child');
const progressText = progressSpan ? progressSpan.textContent : '进度:0.0%';
const progress = parseFloat(progressText.replace('进度:', ''));
if (DEBUG) {
log(`当前课程状态检查 - 标题: ${currentCourse.textContent.trim()} - 进度: ${progressText}`, true);
}
if (isFirstCheck) {
log('------------------------');
log('当前课程: ' + currentCourse.textContent.trim());
log('------------------------');
isFirstCheck = false;
}
if (progressText !== lastProgress) {
log('当前进度: ' + progressText);
lastProgress = progressText;
}
return {
currentCourse: currentCourse.parentElement,
progress: progress
};
}
function saveProgress() {
const saveButton = document.querySelector('button.el-button.btn span:first-child').closest('button');
if (saveButton) {
if (DEBUG) {
log('正在点击保存进度按钮...', true);
}
saveButton.click();
log('自动保存进度');
} else {
log('未找到保存进度按钮');
}
}
// 检查是否所有章节都已完成
function areAllChaptersCompleted() {
const chapters = document.querySelectorAll('.kc-list li');
if (DEBUG) {
log(`检查所有章节状态,共找到 ${chapters.length} 个章节`, true);
}
for (const chapter of chapters) {
const progressSpan = chapter.querySelector('.kc-info span:last-child');
const progressText = progressSpan ? progressSpan.textContent : '进度:0.0%';
const progress = parseFloat(progressText.replace('进度:', ''));
if (DEBUG) {
const chapterTitle = chapter.querySelector('h5').textContent.trim();
log(`章节: ${chapterTitle} - 进度: ${progressText}`, true);
}
if (progress < TARGET_PROGRESS) {
return false;
}
}
return true;
}
// 获取视频播放器
function getPlayer() {
const video = document.querySelector('video');
if (!video) {
if (DEBUG) {
log('未找到视频元素', true);
}
return null;
}
return video;
}
// 等待视频元素加载
function waitForVideo(maxAttempts = 10) {
return new Promise((resolve) => {
let attempts = 0;
const checkVideo = () => {
const video = getPlayer();
if (video) {
if (DEBUG) {
log('视频元素已就绪', true);
}
resolve(video);
return;
}
attempts++;
if (attempts >= maxAttempts) {
if (DEBUG) {
log(`视频加载超时,已尝试 ${attempts} 次`, true);
}
resolve(null);
return;
}
if (DEBUG) {
log(`等待视频加载,第 ${attempts} 次尝试...`, true);
}
setTimeout(checkVideo, 1000);
};
checkVideo();
});
}
// 播放视频
async function playVideo() {
const video = await waitForVideo();
if (!video) return false;
try {
if (video.paused) {
// 设置为静音并保持静音状态
video.muted = true;
await video.play();
if (DEBUG) {
log('视频已开始播放(音模式)', true);
}
return true;
}
} catch (error) {
if (DEBUG) {
log(`播放视频出错: ${error.message}`, true);
}
}
return false;
}
// ���停视频
function pauseVideo() {
const video = getPlayer();
if (!video) return false;
if (DEBUG) {
log(`当前视频状态: ${video.paused ? '已暂停' : '播放中'}`, true);
}
if (!video.paused) {
video.pause();
log('暂停播放视频');
return true;
}
return false;
}
// 切换播放状态
function toggleVideo() {
const video = getPlayer();
if (!video) return false;
if (video.paused) {
return playVideo();
} else {
return pauseVideo();
}
}
// 获取当前播放状态
function getVideoStatus() {
const video = getPlayer();
if (!video) return null;
return {
currentTime: video.currentTime,
duration: video.duration,
paused: video.paused,
playbackRate: video.playbackRate,
volume: video.volume
};
}
async function mainLoop() {
if (DEBUG) {
log('开始新一轮检查...', true);
}
// 先保存进度
saveProgress();
console.log(getVideoStatus());
if(getVideoStatus().paused == true){
playVideo();
}
// 2秒检查进度
setTimeout(async () => {
try {
const currentInfo = getCurrentCourseInfo();
if (!currentInfo) return;
if (currentInfo.progress >= TARGET_PROGRESS) {
if (DEBUG) {
log('当前章节完成,检查其他章节...', true);
}
// 检查是否所有章节都完成
if (areAllChaptersCompleted()) {
log('所有章节已完成,准备切换科目...');
await switchToNextSubject();
} else {
// 查找下一个未完成的章节
const chapters = document.querySelectorAll('.kc-list li');
for (const chapter of chapters) {
const progressSpan = chapter.querySelector('.kc-info span:last-child');
const progressText = progressSpan ? progressSpan.textContent : '进度:0.0%';
const progress = parseFloat(progressText.replace('进度:', ''));
if (progress < TARGET_PROGRESS) {
const chapterTitle = chapter.querySelector('h5').textContent.trim();
log('------------------------');
log('准备切换到章节: ' + chapterTitle);
log('当前进度: ' + progressText);
log('------------------------');
chapter.querySelector('h5').click();
isFirstCheck = true;
break;
}
}
}
}
} catch (error) {
log(`处理课程切换时发生错误: ${error.message}`);
}
}, 2000);
}
// 视频播放页面的主要逻辑
function handleVideoPage() {
if (DEBUG) {
log('初始化视频播放页面逻辑...', true);
}
// 5秒后开始第一次执行
setTimeout(() => {
getCurrentCourseInfo(); // 显示初始课程信息
mainLoop(); // 开始一次循环
// 根据调试模式设置不同的循环间隔
const interval = DEBUG ? DEBUG_INTERVAL : NORMAL_INTERVAL;
setInterval(mainLoop, interval);
}, 5000);
}
// 课程选择页面的主要逻辑
function handleVideoChoosePage() {
if (DEBUG) {
log('初始化课程选择页面逻辑...', true);
}
// 等待页面元素加载完成
setTimeout(() => {
// 使用精确的选择器查找第一条课程
const firstCourse = document.querySelector('#app > div.video-page-main > div.page-content > div > div.choose-body > div:nth-child(2) > span.choose-content');
if (firstCourse) {
if (DEBUG) {
log(`找到第一条课程: ${firstCourse.textContent}`, true);
log(`课程元素: ${firstCourse.outerHTML}`, true);
}
// 点击课程
firstCourse.click();
log('已选择第一条课程');
// 等待5秒后刷新页面
if (DEBUG) {
log('5秒后将刷新页面...', true);
}
setTimeout(() => {
window.location.reload();
}, 5000);
} else {
log('未找到课程选项');
if (DEBUG) {
// 输出页面结构以便调试
const pageContent = document.querySelector('.page-content');
log(`页面内容: ${pageContent ? pageContent.innerHTML : '未找到page-content'}`, true);
}
}
}, 1000);
}
// 根据页面URL执行不同的逻辑
function initializeBasedOnURL() {
const currentURL = window.location.href;
// 创建模拟控制台
createConsole();
// 创建配置面板
createConfigPanel();
// 显示初始提示
showInitialNotification();
if (DEBUG) {
log(`当前页面URL: ${currentURL}`, true);
}
if (currentURL.includes('/video?')) {
if (DEBUG) {
log('检测到视频播放页面', true);
}
handleVideoPage();
} else if (currentURL.includes('/videoChoose?')) {
if (DEBUG) {
log('检测到课程选择页面', true);
}
handleVideoChoosePage();
} else {
log('未知页面类型');
}
}
// 等待页面加载完成后初始化
window.addEventListener('load', initializeBasedOnURL);
// 添加配置面板
function createConfigPanel() {
const panel = document.createElement('div');
panel.innerHTML = `
<div id="config-panel" style="
position: fixed;
top: 50%;
right: 20px;
transform: translateY(-50%);
z-index: 9999;
background: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
font-size: 14px;
">
<div style="margin-bottom:15px;font-weight:bold;border-bottom:1px solid #eee;padding-bottom:5px;">
脚本设置
</div>
<div style="margin-bottom:15px;">
<label style="display:flex;align-items:center;cursor:pointer;">
<span id="debugIcon" style="margin-right:8px;width:20px;text-align:center;">
${DEBUG ? '✅' : '❌'}
</span>
<input
type="checkbox"
id="debugMode"
${DEBUG ? 'checked' : ''}
style="margin-right:8px;"
>
开启调试日志
</label>
</div>
<div style="margin-bottom:10px;">
<div style="margin-bottom:5px;">课程模式:</div>
<select
id="courseMode"
style="width:100%;padding:5px;border-radius:3px;border:1px solid #ddd;"
>
<option value="elective" ${currentCourseMode === 'elective' ? 'selected' : ''}>选修课程</option>
<option value="required" ${currentCourseMode === 'required' ? 'selected' : ''}>必修课程</option>
</select>
</div>
</div>
`;
document.body.appendChild(panel);
// 修改调试模式事件监听
document.getElementById('debugMode').addEventListener('change', e => {
DEBUG = e.target.checked;
localStorage.setItem('debugMode', DEBUG);
document.getElementById('debugIcon').textContent = DEBUG ? '✅' : '❌';
log(`调试模式已${DEBUG ? '开启' : '关闭'}`);
});
// 课程模式事件监听保持不变
document.getElementById('courseMode').addEventListener('change', e => {
currentCourseMode = e.target.value;
localStorage.setItem('courseMode', currentCourseMode);
log(`已切换到${currentCourseMode === 'required' ? '必修' : '选修'}模式`);
});
}
// 修改 fetchCourseList 函数中的 URL 获取
function getApiUrl() {
const urls = {
elective: 'https://www.fsa.gov.cn/api/study/my/elective/myElectives',
required: 'https://www.fsa.gov.cn/api/study/years/yearsCourseware/annualPortalCourseList'
};
return urls[currentCourseMode];
}
})();