├── image
├── demo.gif
└── developer.png
├── startocr.py
├── README.md
├── startocr.bat
└── NJU-enhance.js
/image/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hronrad/LMS-enhancement/HEAD/image/demo.gif
--------------------------------------------------------------------------------
/image/developer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hronrad/LMS-enhancement/HEAD/image/developer.png
--------------------------------------------------------------------------------
/startocr.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, request, jsonify
2 | from flask_cors import CORS
3 | import ddddocr
4 | import base64
5 |
6 | app = Flask(__name__)
7 | CORS(app) # 允许所有来源跨域
8 |
9 | # 初始化ddddocr
10 | ocr = ddddocr.DdddOcr(show_ad=False)
11 |
12 | @app.route('/ocr', methods=['POST'])
13 | def ocr_api():
14 | try:
15 | data = request.get_json()
16 | if not data or 'image' not in data:
17 | return jsonify({'error': '缺少图像数据'}), 400
18 |
19 | img_b64 = data.get('image', '')
20 | img_bytes = base64.b64decode(img_b64)
21 | text = ocr.classification(img_bytes)
22 |
23 | # 清理结果,只保留字母和数字
24 | clean_text = ''.join(c for c in text if c.isalnum())
25 |
26 | print(f"识别结果: '{text}' -> '{clean_text}'")
27 | return jsonify({'text': clean_text})
28 |
29 | except Exception as e:
30 | print(f"OCR识别错误: {e}")
31 | return jsonify({'error': str(e)}), 500
32 |
33 | @app.route('/health', methods=['GET'])
34 | def health():
35 | return jsonify({'status': 'OK'})
36 |
37 | if __name__ == '__main__':
38 | print("启动ddddocr验证码识别服务...")
39 | print("服务地址: http://127.0.0.1:5000")
40 | app.run(host='127.0.0.1', port=5000, debug=False)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 南大LMS智慧教育平台|MOOC增强
2 |
3 | [](https://www.gnu.org/licenses/gpl-3.0)
4 | [](https://www.tampermonkey.net/)
5 | [](https://www.tampermonkey.net/)
6 | [](https://www.tampermonkey.net/)
7 |
8 | 请使用篡改猴|油猴插件 Tampermonkey 来进行安装
9 |
10 | 最新功能(v 0.30):可通过配置ocr服务自动识别登录验证码;可自动下载课件。
11 |
12 | ## 一键安装
13 |
14 | ### 第一步
15 | **请先安装 [Tampermonkey](https://www.tampermonkey.net/) 浏览器扩展**
16 |
17 | 各浏览器具体链接:
18 |
19 | [](https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo)
20 | [](https://addons.mozilla.org/zh-CN/firefox/addon/tampermonkey/)
21 | [](https://microsoftedge.microsoft.com/addons/detail/%E7%AF%A1%E6%94%B9%E7%8C%B4/iikmkjmpaadaobahmlepeloendndfphd)
22 | [](https://apps.apple.com/us/app/tampermonkey/id1482490089)
23 | [](https://addons.opera.com/zh-cn/extensions/details/tampermonkey-beta/)
24 |
25 | ### 第二步
26 | 第一次安装需要打开浏览器的开发者模式。
27 |
28 | Edge浏览器: 地址栏输入edge://extensions/并回车,打开开发人员模式
29 |
30 |
31 | Chrome浏览器: 地址栏输入chrome://extensions/并回车,打开开发人员模式
32 |
33 | 其他浏览器同理。
34 |
35 | ### 第三步
36 | 点击下面的按钮直接安装最新版本:
37 |
38 | [](https://greasyfork.org/zh-CN/scripts/546406-%E5%8D%97%E5%A4%A7lms%E6%99%BA%E6%85%A7%E6%95%99%E8%82%B2%E5%B9%B3%E5%8F%B0-mooc%E5%A2%9E%E5%BC%BA/)
39 |
40 | ## 自动更新
41 |
42 | 脚本已配置自动更新功能
43 |
44 | ## 配置OCR验证码识别
45 | 如果需要自动识别登录验证码,请按以下步骤操作(如已有相应API接口可直接在第二步填入):
46 |
47 | ### 第一步
48 | 下载startocr.bat文件并保存在本地任意目录,双击运行,等待命令行窗口提示`启动ddddocr验证码识别服务...\\
49 | 服务地址: http://127.0.0.1:5000`即运行成功,无需关闭该 cmd 窗口。
50 |
51 | 该脚本为傻瓜式一键操作脚本,如该批处理脚本无法正常运行,请手动运行`startocr.py`文件,确保已安装 Python3 和 ddddocr 库。
52 |
53 | ### 第二步
54 | 在登录页面 authserver.nju.edu.cn 的右侧菜单开启自动识别,并填入API地址`http://127.0.0.1:5000/ocr`。
55 |
56 | ## 功能简介
57 | 1. ▶️解除自动暂停,可后台播放
58 | 2. 🚫解除禁用右键,不可跳转进度条等限制
59 | 3. ⏩自动加速视频课程完成进度
60 | 4. ⏭️播放完成后自动播放下一个视频,跳过无视频页面
61 | 5. 🚀侧边菜单可控制多种倍速0.1x/1x/3x/16x
62 | 6. 🚀可自动识别登录验证码(需配置OCR服务)
63 |
64 | ## 使用演示
65 | 其他功能自动开启,如需倍速请按下图从右侧菜单选择所需倍速
66 | 
67 |
68 | ## 温馨提示
69 | - 当您在对应网站看到页面右侧蓝色隐藏倍速按钮时,代表脚本已经成功安装并生效。
70 |
71 | - 如遇页面卡顿/功能失效等问题,以及“系统繁忙”提示时,请立即通过 `ctrl + shift + R` 刷新缓存,以及 `F5` 强制刷新。
72 |
73 | - 建议遵循适度使用和非必要不使用的准则。使用过程中您可随时选择关闭该脚本。由使用此脚本导致的任何问题请自行承担风险。
74 |
75 | - 欢迎反馈问题和建议!
76 |
77 | ## TODO List
78 |
79 | 📋 **功能扩展计划**
80 |
81 | - ☐
82 |
83 |
84 | **⭐ 如果这个脚本对您有帮助,请给个 Star 支持一下!(\*╹▽╹\*)**
85 |
86 | (P.S.引流:欢迎关注B站 Hronrad)
--------------------------------------------------------------------------------
/startocr.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | chcp 65001 >nul
3 | title OCR验证码识别服务 - 一键安装并启动
4 | color 0A
5 |
6 | echo ========================================
7 | echo OCR验证码识别服务
8 | echo 全自动安装和启动
9 | echo ========================================
10 | echo.
11 |
12 | REM ============================================
13 | REM 第一步:检查Python环境
14 | REM ============================================
15 | echo [步骤 1/5] 检查Python环境...
16 | python --version >nul 2>&1
17 | if %errorlevel% neq 0 (
18 | color 0C
19 | echo.
20 | echo ========================================
21 | echo [错误] 未检测到Python环境!
22 | echo ========================================
23 | echo.
24 | echo 请先安装 Python 3.7 或更高版本
25 | echo 下载地址: https://www.python.org/downloads/
26 | echo.
27 | echo 安装时请务必勾选:
28 | echo [✓] Add Python to PATH
29 | echo.
30 | echo 安装完成后,请重新运行本脚本
31 | echo ========================================
32 | echo.
33 | pause
34 | exit /b 1
35 | )
36 |
37 | python --version
38 | echo [✓] Python环境正常
39 | echo.
40 |
41 | REM ============================================
42 | REM 第二步:检查pip工具
43 | REM ============================================
44 | echo [步骤 2/5] 检查pip工具...
45 | python -m pip --version >nul 2>&1
46 | if %errorlevel% neq 0 (
47 | echo [!] pip不可用,正在尝试修复...
48 | python -m ensurepip --default-pip
49 | if %errorlevel% neq 0 (
50 | color 0C
51 | echo [错误] 无法修复pip,请手动重装Python
52 | pause
53 | exit /b 1
54 | )
55 | )
56 | echo [✓] pip工具正常
57 | echo.
58 |
59 | REM ============================================
60 | REM 第三步:检查依赖库是否已安装
61 | REM ============================================
62 | echo [步骤 3/5] 检查依赖库...
63 | python -c "import flask, flask_cors, ddddocr" >nul 2>&1
64 | if %errorlevel% neq 0 (
65 | echo [!] 检测到依赖未安装,开始自动安装...
66 | echo.
67 |
68 | REM 升级pip
69 | echo [3.1] 升级pip工具...
70 | python -m pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple >nul 2>&1
71 | echo [✓] pip已升级
72 | echo.
73 |
74 | REM 安装依赖
75 | echo [3.2] 安装依赖库(使用清华镜像源)...
76 | echo 这可能需要几分钟,请耐心等待...
77 | echo.
78 |
79 | echo 正在安装 flask...
80 | python -m pip install flask==2.3.2 -i https://pypi.tuna.tsinghua.edu.cn/simple
81 | if %errorlevel% neq 0 (
82 | echo [!] 清华源失败,尝试官方源...
83 | python -m pip install flask==2.3.2
84 | if %errorlevel% neq 0 (
85 | color 0C
86 | echo [错误] flask 安装失败
87 | pause
88 | exit /b 1
89 | )
90 | )
91 | echo [✓] flask 安装完成
92 |
93 | echo 正在安装 flask-cors...
94 | python -m pip install flask-cors==4.0.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
95 | if %errorlevel% neq 0 (
96 | python -m pip install flask-cors==4.0.0
97 | if %errorlevel% neq 0 (
98 | color 0C
99 | echo [错误] flask-cors 安装失败
100 | pause
101 | exit /b 1
102 | )
103 | )
104 | echo [✓] flask-cors 安装完成
105 |
106 | echo 正在安装 ddddocr(OCR识别引擎)...
107 | python -m pip install ddddocr==1.4.11 -i https://pypi.tuna.tsinghua.edu.cn/simple
108 | if %errorlevel% neq 0 (
109 | python -m pip install ddddocr==1.4.11
110 | if %errorlevel% neq 0 (
111 | color 0C
112 | echo [错误] ddddocr 安装失败
113 | pause
114 | exit /b 1
115 | )
116 | )
117 | echo [✓] ddddocr 安装完成
118 |
119 | echo.
120 | echo ========================================
121 | echo [✓] 所有依赖安装成功!
122 | echo ========================================
123 | echo.
124 | ) else (
125 | echo [✓] 所有依赖已安装
126 | echo.
127 | )
128 |
129 | REM ============================================
130 | REM 第四步:最终检查
131 | REM ============================================
132 | echo [步骤 4/5] 最终环境检查...
133 | python -c "import flask, flask_cors, ddddocr" >nul 2>&1
134 | if %errorlevel% neq 0 (
135 | color 0C
136 | echo [错误] 依赖库验证失败
137 | pause
138 | exit /b 1
139 | )
140 | echo [✓] 环境检查完成,一切就绪!
141 | echo.
142 |
143 | REM ============================================
144 | REM 第五步:启动服务
145 | REM ============================================
146 | echo [步骤 5/5] 启动OCR识别服务...
147 | echo.
148 | color 0B
149 | echo ========================================
150 | echo 服务启动中...
151 | echo ========================================
152 | echo.
153 | echo 服务地址: http://127.0.0.1:5000
154 | echo 健康检查: http://127.0.0.1:5000/health
155 | echo.
156 | echo 提示:
157 | echo * 保持此窗口打开,服务才能运行
158 | echo * 按 Ctrl+C 可以停止服务
159 | echo * 关闭窗口也会停止服务
160 | echo ========================================
161 | echo.
162 |
163 | REM 检查startocr.py是否存在
164 | if not exist "startocr.py" (
165 | color 0C
166 | echo [错误] 找不到 startocr.py 文件
167 | echo 请确保此脚本与 startocr.py 在同一目录
168 | pause
169 | exit /b 1
170 | )
171 |
172 | REM 启动服务
173 | python startocr.py
174 |
175 | REM 服务停止后的处理
176 | echo.
177 | echo ========================================
178 | echo 服务已停止
179 | echo ========================================
180 | echo.
181 | pause
182 |
--------------------------------------------------------------------------------
/NJU-enhance.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name 南大LMS智慧教育平台|MOOC增强
3 | // @namespace http://tampermonkey.net/
4 | // @version 0.30
5 | // @description 南大LMS平台与MOOC平台加速进度/自动下一个/智能停止/无视频自动跳转/视频倍速控制/解除播放限制 + 验证码自动识别 + 自动下载课件
6 | // @author Hronrad
7 | // @license GPL-3.0-only
8 | // @match https://lms.nju.edu.cn/*
9 | // @match https://www.icourse163.org/*
10 | // @match https://icourse163.org/*
11 | // @match https://authserver.nju.edu.cn/authserver/login*
12 | // @grant none
13 | // ==/UserScript==
14 |
15 | (function() {
16 | 'use strict';
17 |
18 | let isUserPaused = false;
19 | let lastUserAction = 0;
20 | let processedRequests = new Set();
21 | let isVirtualRequest = false;
22 | let allVideosCompleted = false;
23 | let scriptPaused = false;
24 | let noVideoCheckCount = 0;
25 | const MAX_NO_VIDEO_CHECKS = 5;
26 | let currentSpeed = 1;
27 | let processedVideos = new Set();
28 | let contentReady = false;
29 | let pageLoadTime = Date.now();
30 |
31 | const SPEED_STORAGE_KEY = `lms-video-speed-${location.hostname}`;
32 |
33 | const isICourse163 = location.hostname.includes('icourse163.org');
34 | const isAuthServer = location.hostname.includes('authserver.nju.edu.cn');
35 |
36 | function checkContentReady() {
37 | const hasMainContent = document.querySelector('[ng-view]') ||
38 | document.querySelector('.main-content') ||
39 | document.querySelector('#main') ||
40 | document.querySelector('.content-area');
41 |
42 | const hasAngular = window.angular && document.querySelector('[ng-app]');
43 | const timeElapsed = Date.now() - pageLoadTime > 2000;
44 |
45 | const ready = (hasMainContent || hasAngular) && timeElapsed;
46 |
47 | return ready;
48 | }
49 |
50 | function waitForContentReady(callback, maxWait = 15000) {
51 | const startTime = Date.now();
52 |
53 | function check() {
54 | if (checkContentReady()) {
55 | contentReady = true;
56 | callback();
57 | } else if (Date.now() - startTime < maxWait) {
58 | setTimeout(check, 1000);
59 | } else {
60 | contentReady = true;
61 | callback();
62 | }
63 | }
64 |
65 | check();
66 | }
67 |
68 | function handlePageChange() {
69 | scriptPaused = false;
70 | allVideosCompleted = false;
71 | noVideoCheckCount = 0;
72 | contentReady = false;
73 | pageLoadTime = Date.now();
74 |
75 | waitForContentReady(() => {});
76 | }
77 |
78 | function setupPageChangeListener() {
79 | let currentUrl = location.href;
80 | let currentHash = location.hash;
81 |
82 | const observer = new MutationObserver(() => {
83 | if (location.href !== currentUrl || location.hash !== currentHash) {
84 | currentUrl = location.href;
85 | currentHash = location.hash;
86 | handlePageChange();
87 | }
88 | });
89 |
90 | observer.observe(document.body, {
91 | childList: true,
92 | subtree: true
93 | });
94 |
95 | window.addEventListener('hashchange', handlePageChange);
96 | window.addEventListener('popstate', handlePageChange);
97 | }
98 |
99 | function loadSavedSpeed() {
100 | try {
101 | const savedSpeed = localStorage.getItem(SPEED_STORAGE_KEY);
102 | if (savedSpeed) {
103 | const speed = parseFloat(savedSpeed);
104 | if ([0.1, 1, 3, 16].includes(speed)) {
105 | currentSpeed = speed;
106 | }
107 | }
108 | } catch (e) {}
109 | }
110 |
111 | function saveSpeed(speed) {
112 | try {
113 | localStorage.setItem(SPEED_STORAGE_KEY, speed.toString());
114 | window.dispatchEvent(new CustomEvent('lms-speed-changed', {
115 | detail: { speed, timestamp: Date.now() }
116 | }));
117 | } catch (e) {}
118 | }
119 |
120 | function syncSpeedAcrossTabs() {
121 | window.addEventListener('lms-speed-changed', (e) => {
122 | if (e.detail.speed !== currentSpeed) {
123 | currentSpeed = e.detail.speed;
124 | applySpeedToVideos();
125 | updateSpeedButton();
126 | }
127 | });
128 |
129 | window.addEventListener('storage', (e) => {
130 | if (e.key === SPEED_STORAGE_KEY && e.newValue) {
131 | const newSpeed = parseFloat(e.newValue);
132 | if ([0.1, 1, 3, 16].includes(newSpeed) && newSpeed !== currentSpeed) {
133 | currentSpeed = newSpeed;
134 | applySpeedToVideos();
135 | updateSpeedButton();
136 | }
137 | }
138 | });
139 | }
140 |
141 | function applySpeedToVideos() {
142 | document.querySelectorAll('video').forEach(video => {
143 | if (video.playbackRate !== currentSpeed) {
144 | video.playbackRate = currentSpeed;
145 | }
146 | });
147 | }
148 |
149 | function updateSpeedButton() {
150 | const speedButton = document.getElementById('lms-speed-button');
151 | const speedMenu = document.getElementById('lms-speed-menu');
152 |
153 | if (speedButton) {
154 | speedButton.innerHTML = `${currentSpeed}x`;
155 | }
156 |
157 | if (speedMenu) {
158 | speedMenu.querySelectorAll('div').forEach((div, i) => {
159 | const itemSpeed = [0.1, 1, 3, 16][i];
160 | div.style.background = itemSpeed === currentSpeed ? '#e3f2fd' : 'white';
161 | div.style.fontWeight = itemSpeed === currentSpeed ? 'bold' : 'normal';
162 | });
163 | }
164 | }
165 |
166 | function removeVideoRestrictions() {
167 | const videos = document.querySelectorAll('video:not([data-restrictions-removed])');
168 |
169 | videos.forEach(video => {
170 | video.setAttribute('data-restrictions-removed', 'true');
171 | video.setAttribute('allow-foward-seeking', 'true');
172 | video.setAttribute('data-allow-download', 'true');
173 | video.setAttribute('allow-right-click', 'true');
174 | video.removeAttribute('forward-seeking-warning');
175 | video.controls = true;
176 | video.oncontextmenu = null;
177 | });
178 | }
179 |
180 | function removePageRestrictions() {
181 | document.oncontextmenu = null;
182 | document.onselectstart = null;
183 | document.ondragstart = null;
184 | document.onkeydown = null;
185 | }
186 |
187 | function monitorRestrictions() {
188 | const observer = new MutationObserver((mutations) => {
189 | let needsUpdate = false;
190 |
191 | mutations.forEach((mutation) => {
192 | if (mutation.type === 'childList') {
193 | mutation.addedNodes.forEach((node) => {
194 | if (node.nodeType === 1 && (node.tagName === 'VIDEO' || node.querySelector('video'))) {
195 | needsUpdate = true;
196 | }
197 | });
198 | }
199 | });
200 |
201 | if (needsUpdate) {
202 | setTimeout(removeVideoRestrictions, 200);
203 | }
204 | });
205 |
206 | observer.observe(document.body, {
207 | childList: true,
208 | subtree: true
209 | });
210 | }
211 |
212 | function createSpeedControlUI() {
213 | if (document.getElementById('lms-speed-container')) return;
214 |
215 | const container = document.createElement('div');
216 | container.id = 'lms-speed-container';
217 | container.style.cssText = `
218 | position: fixed;
219 | top: 50%;
220 | right: -45px;
221 | transform: translateY(-50%);
222 | z-index: 10000;
223 | transition: right 0.3s ease;
224 | display: flex;
225 | flex-direction: column;
226 | align-items: flex-end;
227 | `;
228 |
229 | const speedButton = document.createElement('button');
230 | speedButton.id = 'lms-speed-button';
231 | speedButton.innerHTML = `${currentSpeed}x`;
232 | speedButton.style.cssText = `
233 | width: 60px;
234 | height: 35px;
235 | background: #007bff;
236 | color: white;
237 | border: none;
238 | border-radius: 8px 0 0 8px;
239 | font-size: 14px;
240 | cursor: pointer;
241 | box-shadow: 0 4px 12px rgba(0,123,255,0.3);
242 | transition: all 0.3s ease;
243 | margin-bottom: 5px;
244 | `;
245 |
246 | const speedMenu = document.createElement('div');
247 | speedMenu.id = 'lms-speed-menu';
248 | speedMenu.style.cssText = `
249 | background: white;
250 | border: 1px solid #ddd;
251 | border-radius: 8px 0 0 8px;
252 | box-shadow: 0 8px 24px rgba(0,0,0,0.15);
253 | min-width: 80px;
254 | overflow: hidden;
255 | opacity: 0;
256 | transform: translateX(10px);
257 | transition: all 0.3s ease;
258 | pointer-events: none;
259 | `;
260 |
261 | [0.1, 1, 3, 16].forEach(speed => {
262 | const item = document.createElement('div');
263 | item.textContent = `${speed}x`;
264 | item.style.cssText = `
265 | padding: 10px 16px;
266 | cursor: pointer;
267 | transition: background 0.2s ease;
268 | font-size: 13px;
269 | text-align: center;
270 | ${speed === currentSpeed ? 'background: #e3f2fd; font-weight: bold;' : ''}
271 | `;
272 | item.onmouseenter = () => item.style.background = speed === currentSpeed ? '#bbdefb' : '#f5f5f5';
273 | item.onmouseleave = () => item.style.background = speed === currentSpeed ? '#e3f2fd' : 'white';
274 | item.onclick = () => {
275 | setVideoSpeed(speed);
276 | speedButton.innerHTML = `${speed}x`;
277 | updateMenuSelection(speedMenu, speed);
278 | };
279 | speedMenu.appendChild(item);
280 | });
281 |
282 | // 添加设置选项(分割线)
283 | const divider = document.createElement('div');
284 | divider.style.cssText = 'height: 1px; background: #ddd; margin: 5px 0;';
285 | speedMenu.appendChild(divider);
286 |
287 | const settingsItem = document.createElement('div');
288 | settingsItem.textContent = '⚙️ 设置';
289 | settingsItem.style.cssText = `
290 | padding: 10px 16px;
291 | cursor: pointer;
292 | transition: background 0.2s ease;
293 | font-size: 13px;
294 | text-align: center;
295 | `;
296 | settingsItem.onmouseenter = () => settingsItem.style.background = '#f5f5f5';
297 | settingsItem.onmouseleave = () => settingsItem.style.background = 'white';
298 | settingsItem.onclick = () => showSettingsPanel();
299 | speedMenu.appendChild(settingsItem);
300 |
301 | function updateMenuSelection(menu, selectedSpeed) {
302 | menu.querySelectorAll('div').forEach((div, i) => {
303 | const itemSpeed = [0.1, 1, 3, 16][i];
304 | div.style.background = itemSpeed === selectedSpeed ? '#e3f2fd' : 'white';
305 | div.style.fontWeight = itemSpeed === selectedSpeed ? 'bold' : 'normal';
306 | });
307 | }
308 |
309 | container.appendChild(speedButton);
310 | container.appendChild(speedMenu);
311 |
312 | let isExpanded = false;
313 | let hideTimeout;
314 |
315 | function showControls() {
316 | clearTimeout(hideTimeout);
317 | isExpanded = true;
318 | container.style.right = '0px';
319 | speedButton.style.background = '#0056b3';
320 | speedButton.style.transform = 'scale(1.05)';
321 | speedMenu.style.opacity = '1';
322 | speedMenu.style.transform = 'translateX(0)';
323 | speedMenu.style.pointerEvents = 'auto';
324 | }
325 |
326 | function hideControls() {
327 | hideTimeout = setTimeout(() => {
328 | isExpanded = false;
329 | container.style.right = '-45px';
330 | speedButton.style.background = '#007bff';
331 | speedButton.style.transform = 'scale(1)';
332 | speedMenu.style.opacity = '0';
333 | speedMenu.style.transform = 'translateX(10px)';
334 | speedMenu.style.pointerEvents = 'none';
335 | }, 300);
336 | }
337 |
338 | container.onmouseenter = showControls;
339 | container.onmouseleave = hideControls;
340 |
341 | speedButton.onclick = (e) => {
342 | e.stopPropagation();
343 | if (isExpanded) {
344 | speedMenu.style.display = speedMenu.style.display === 'none' ? 'block' : 'none';
345 | }
346 | };
347 |
348 | document.addEventListener('click', (e) => {
349 | if (!container.contains(e.target)) {
350 | speedMenu.style.display = 'block';
351 | }
352 | });
353 |
354 | const hoverIndicator = document.createElement('div');
355 | hoverIndicator.style.cssText = `
356 | position: absolute;
357 | right: 0;
358 | top: 50%;
359 | transform: translateY(-50%);
360 | width: 3px;
361 | height: 30px;
362 | background: linear-gradient(45deg, #007bff, #0056b3);
363 | border-radius: 3px 0 0 3px;
364 | opacity: 0.7;
365 | animation: pulse 2s infinite;
366 | `;
367 |
368 | const style = document.createElement('style');
369 | style.textContent = `
370 | @keyframes pulse {
371 | 0%, 100% { opacity: 0.7; }
372 | 50% { opacity: 0.3; }
373 | }
374 | `;
375 | document.head.appendChild(style);
376 |
377 | container.appendChild(hoverIndicator);
378 | document.body.appendChild(container);
379 | }
380 |
381 | function setVideoSpeed(speed) {
382 | currentSpeed = speed;
383 | saveSpeed(speed);
384 | applySpeedToVideos();
385 | updateSpeedButton();
386 | }
387 |
388 | function initICourse163() {
389 | loadSavedSpeed();
390 | syncSpeedAcrossTabs();
391 | removeVideoRestrictions();
392 | removePageRestrictions();
393 | monitorRestrictions();
394 | createSpeedControlUI();
395 |
396 | setInterval(() => {
397 | applySpeedToVideos();
398 | }, 2000);
399 | }
400 |
401 | if (isICourse163) {
402 | if (document.readyState === 'loading') {
403 | document.addEventListener('DOMContentLoaded', () => setTimeout(initICourse163, 500));
404 | } else {
405 | setTimeout(initICourse163, 500);
406 | }
407 | return;
408 | }
409 |
410 | loadSavedSpeed();
411 | syncSpeedAcrossTabs();
412 |
413 | Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
414 | Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
415 | document.addEventListener('visibilitychange', (e) => e.stopImmediatePropagation(), true);
416 |
417 | const originalOpen = XMLHttpRequest.prototype.open;
418 | XMLHttpRequest.prototype.open = function(method, url, ...args) {
419 | this._method = method;
420 | this._url = url;
421 | this._isVirtual = isVirtualRequest;
422 | return originalOpen.call(this, method, url, ...args);
423 | };
424 |
425 | const originalSend = XMLHttpRequest.prototype.send;
426 | XMLHttpRequest.prototype.send = function(data) {
427 | const url = this._url || '';
428 |
429 | if (scriptPaused) {
430 | return originalSend.call(this, data);
431 | }
432 |
433 | if (!this._isVirtual &&
434 | (url.includes('/statistics/api/online-videos') ||
435 | url.includes('/api/course/activities-read/')) &&
436 | this._method === 'POST' && data) {
437 |
438 | try {
439 | const jsonData = JSON.parse(data);
440 | const requestKey = `${url}-${JSON.stringify(jsonData)}`;
441 |
442 | if (!processedRequests.has(requestKey)) {
443 | processedRequests.add(requestKey);
444 | createVirtualSessions(url, jsonData);
445 | setTimeout(() => processedRequests.delete(requestKey), 10000);
446 | }
447 | } catch (e) {}
448 | }
449 |
450 | return originalSend.call(this, data);
451 | };
452 |
453 | function createVirtualSessions(url, originalData) {
454 | if (scriptPaused) return;
455 |
456 | const sessionCount = 10;
457 | const maxDuration = 30;
458 | const originalDuration = (originalData.end || 0) - (originalData.start || 0);
459 | const isLargeDuration = originalDuration > maxDuration;
460 |
461 | for (let i = 1; i < sessionCount; i++) {
462 | setTimeout(() => {
463 | if (scriptPaused) return;
464 |
465 | const virtualData = JSON.parse(JSON.stringify(originalData));
466 |
467 | if (isLargeDuration) {
468 | const segmentDuration = Math.min(maxDuration, Math.floor(originalDuration / sessionCount) + 5);
469 | const baseStart = originalData.start || 0;
470 |
471 | virtualData.start = baseStart + (i - 1) * segmentDuration + Math.floor(Math.random() * 3);
472 | virtualData.end = virtualData.start + segmentDuration + Math.floor(Math.random() * 3);
473 |
474 | if (virtualData.end > originalData.end) {
475 | virtualData.end = originalData.end;
476 | }
477 |
478 | if (virtualData.start >= virtualData.end) {
479 | virtualData.start = virtualData.end - Math.min(5, segmentDuration);
480 | }
481 | } else {
482 | if (virtualData.start !== undefined) {
483 | virtualData.start += Math.floor(Math.random() * 3);
484 | }
485 | if (virtualData.end !== undefined) {
486 | virtualData.end += Math.floor(Math.random() * 3);
487 | }
488 | }
489 |
490 | const duration = (virtualData.end || 0) - (virtualData.start || 0);
491 | if (duration <= 0 || duration > maxDuration * 2) {
492 | return;
493 | }
494 |
495 | fetch(url, {
496 | method: 'POST',
497 | headers: {
498 | 'Content-Type': 'application/json',
499 | 'X-Requested-With': 'XMLHttpRequest'
500 | },
501 | body: JSON.stringify(virtualData),
502 | credentials: 'same-origin'
503 | }).then(response => {}).catch(error => {});
504 |
505 | }, i * 400 + Math.random() * 300);
506 | }
507 | }
508 |
509 | function detectUserAction(e) {
510 | const target = e.target;
511 |
512 | if (target.closest('.vjs-play-control') ||
513 | target.closest('.vjs-big-play-button') ||
514 | target.closest('button') ||
515 | target.tagName === 'BUTTON') {
516 |
517 | lastUserAction = Date.now();
518 |
519 | setTimeout(() => {
520 | document.querySelectorAll('video').forEach(video => {
521 | if (video.paused) {
522 | isUserPaused = true;
523 | }
524 | });
525 | }, 100);
526 | }
527 | }
528 |
529 | document.addEventListener('click', detectUserAction, true);
530 | document.addEventListener('keydown', (e) => {
531 | if (e.code === 'Space') {
532 | lastUserAction = Date.now();
533 | }
534 | }, true);
535 |
536 | function hasNextButton() {
537 | try {
538 | const angular = window.angular;
539 | if (angular) {
540 | const scope = angular.element(document.body).scope();
541 | if ((scope && scope.navigation && scope.navigation.nextItem) ||
542 | (scope && scope.nextActivity)) {
543 | return true;
544 | }
545 | }
546 | } catch (e) {}
547 |
548 | const nextSelectors = [
549 | 'button[ng-click*="changeActivity(nextActivity)"]',
550 | 'button[ng-if="nextActivity"]',
551 | 'a[ng-click*="goToNextTopic()"]',
552 | 'a.next[ng-if*="!isLastTopic()"]',
553 | 'span.icon-student-circle[ng-click*="navigation.goNext"]',
554 | 'button[ng-click*="goNext"]',
555 | 'a.next[ng-click="goToNextTopic()"]',
556 | 'button.button[ng-click*="changeActivity(nextActivity)"]'
557 | ];
558 |
559 | for (const selector of nextSelectors) {
560 | const nextButton = document.querySelector(selector);
561 | if (nextButton && nextButton.offsetParent !== null) {
562 | return true;
563 | }
564 | }
565 |
566 | try {
567 | const nextTopicLink = document.querySelector('a.next[ng-click="goToNextTopic()"]');
568 | if (nextTopicLink) {
569 | const scope = window.angular.element(nextTopicLink).scope();
570 | if (scope && typeof scope.isLastTopic === 'function') {
571 | if (!scope.isLastTopic() && nextTopicLink.offsetParent !== null) {
572 | return true;
573 | }
574 | }
575 | }
576 |
577 | const nextActivityBtn = document.querySelector('button[ng-click*="changeActivity(nextActivity)"]');
578 | if (nextActivityBtn) {
579 | const scope = window.angular.element(nextActivityBtn).scope();
580 | if (scope && scope.nextActivity && nextActivityBtn.offsetParent !== null) {
581 | return true;
582 | }
583 | }
584 | } catch (e) {}
585 |
586 | const elements = document.querySelectorAll('button, a');
587 | for (const el of elements) {
588 | if (el.textContent.includes('下一个') && el.offsetParent !== null) {
589 | return true;
590 | }
591 | }
592 |
593 | return false;
594 | }
595 |
596 | function hasVideos() {
597 | return document.querySelectorAll('video').length > 0;
598 | }
599 |
600 | function checkAllVideosCompleted() {
601 | const videos = document.querySelectorAll('video');
602 | if (videos.length === 0) return false;
603 |
604 | return Array.from(videos).every(video => {
605 | const isEnded = video.ended;
606 | const isDurationComplete = video.duration > 0 &&
607 | Math.abs(video.currentTime - video.duration) < 1;
608 | const isNearComplete = video.duration > 0 &&
609 | video.currentTime / video.duration >= 0.98;
610 |
611 | return isEnded || isDurationComplete || isNearComplete;
612 | });
613 | }
614 |
615 | function checkNoVideoAutoNext() {
616 | if (scriptPaused) return;
617 |
618 | if (!contentReady) {
619 | return;
620 | }
621 |
622 | if (!hasVideos()) {
623 | if (hasNextButton()) {
624 | noVideoCheckCount++;
625 | if (noVideoCheckCount >= MAX_NO_VIDEO_CHECKS) {
626 | noVideoCheckCount = 0;
627 | autoClickNext();
628 | }
629 | } else {
630 | pauseScript();
631 | }
632 | } else {
633 | noVideoCheckCount = 0;
634 | }
635 | }
636 |
637 | function pauseScript() {
638 | if (scriptPaused) return;
639 |
640 | scriptPaused = true;
641 | allVideosCompleted = true;
642 |
643 | document.querySelectorAll('video').forEach(video => {
644 | if (!video.paused) {
645 | video.pause();
646 | }
647 | });
648 | }
649 |
650 | function keepVideoPlaying() {
651 | if (scriptPaused) return;
652 |
653 | document.querySelectorAll('video').forEach(video => {
654 | if (video.paused) {
655 | const timeSinceUserAction = Date.now() - lastUserAction;
656 |
657 | if (isUserPaused && timeSinceUserAction < 3000) {
658 | return;
659 | }
660 |
661 | if (video.readyState >= 2) {
662 | video.play().then(() => {
663 | isUserPaused = false;
664 | }).catch(() => {});
665 | }
666 | } else {
667 | if (isUserPaused && Date.now() - lastUserAction > 2000) {
668 | isUserPaused = false;
669 | }
670 | }
671 | });
672 | }
673 |
674 | function performVirtualUserAction() {
675 | if (scriptPaused) return;
676 |
677 | const videos = document.querySelectorAll('video');
678 | const playButtons = document.querySelectorAll('.vjs-play-control');
679 |
680 | if (videos.length > 0 && !isUserPaused) {
681 | videos.forEach((video, index) => {
682 | if (!video.paused) {
683 | if (playButtons[index]) {
684 | playButtons[index].dispatchEvent(new MouseEvent('click', {
685 | bubbles: true,
686 | cancelable: true,
687 | view: window
688 | }));
689 | } else {
690 | video.pause();
691 | }
692 |
693 | setTimeout(() => {
694 | if (scriptPaused) return;
695 |
696 | if (playButtons[index]) {
697 | playButtons[index].dispatchEvent(new MouseEvent('click', {
698 | bubbles: true,
699 | cancelable: true,
700 | view: window
701 | }));
702 | } else {
703 | video.play().catch(() => {});
704 | }
705 | }, 100);
706 | }
707 | });
708 | }
709 | }
710 |
711 | function setupVideoCompletionHandler() {
712 | const videos = document.querySelectorAll('video:not([data-completion-handler])');
713 |
714 | videos.forEach(video => {
715 | video.setAttribute('data-completion-handler', 'true');
716 | video.playbackRate = currentSpeed;
717 |
718 | video.addEventListener('ended', function() {
719 | setTimeout(() => {
720 | if (checkAllVideosCompleted()) {
721 | if (hasNextButton()) {
722 | autoClickNext();
723 | } else {
724 | pauseScript();
725 | }
726 | } else {
727 | autoClickNext();
728 | }
729 | }, 2000);
730 | });
731 | });
732 | }
733 |
734 | function autoClickNext() {
735 | if (scriptPaused) return;
736 |
737 | try {
738 | const angular = window.angular;
739 | if (angular) {
740 | const scope = angular.element(document.body).scope();
741 |
742 | if (scope && scope.nextActivity && scope.changeActivity) {
743 | scope.changeActivity(scope.nextActivity);
744 | scope.$apply();
745 | return;
746 | }
747 |
748 | if (scope && scope.goToNextTopic) {
749 | scope.goToNextTopic();
750 | scope.$apply();
751 | return;
752 | }
753 |
754 | if (scope && scope.navigation && scope.navigation.goNext) {
755 | scope.navigation.goNext();
756 | scope.$apply();
757 | return;
758 | }
759 | }
760 | } catch (e) {}
761 |
762 | const nextSelectors = [
763 | 'button[ng-click*="changeActivity(nextActivity)"]',
764 | 'button[ng-if="nextActivity"]',
765 | 'a[ng-click*="goToNextTopic()"]',
766 | 'a.next[ng-if*="!isLastTopic()"]',
767 | 'button[ng-click*="goNext"]',
768 | 'a.next[ng-click="goToNextTopic()"]',
769 | 'button.button[ng-click*="changeActivity(nextActivity)"]'
770 | ];
771 |
772 | for (const selector of nextSelectors) {
773 | const nextButton = document.querySelector(selector);
774 | if (nextButton && nextButton.offsetParent !== null) {
775 | if (nextButton.hasAttribute('ng-click') && window.angular) {
776 | try {
777 | const scope = window.angular.element(nextButton).scope();
778 | if (scope) {
779 | scope.$eval(nextButton.getAttribute('ng-click'));
780 | scope.$apply();
781 | return;
782 | }
783 | } catch (e) {}
784 | }
785 |
786 | nextButton.click();
787 | return;
788 | }
789 | }
790 |
791 | const allElements = document.querySelectorAll('button, a, span[ng-click]');
792 | for (const element of allElements) {
793 | const text = element.textContent.trim();
794 | const ngClick = element.getAttribute('ng-click') || '';
795 |
796 | if ((text.includes('下一个') || ngClick.includes('changeActivity') ||
797 | ngClick.includes('goToNextTopic') || ngClick.includes('goNext')) &&
798 | element.offsetParent !== null) {
799 |
800 | if (ngClick && window.angular) {
801 | try {
802 | const scope = window.angular.element(element).scope();
803 | if (scope) {
804 | scope.$eval(ngClick);
805 | scope.$apply();
806 | return;
807 | }
808 | } catch (e) {}
809 | }
810 |
811 | element.click();
812 | return;
813 | }
814 | }
815 |
816 | pauseScript();
817 | }
818 |
819 | setInterval(keepVideoPlaying, 2000);
820 | setInterval(performVirtualUserAction, 1000);
821 | setInterval(() => {
822 | setupVideoCompletionHandler();
823 | applySpeedToVideos();
824 | }, 3000);
825 | setInterval(checkNoVideoAutoNext, 6000);
826 |
827 | function init() {
828 | keepVideoPlaying();
829 | setupVideoCompletionHandler();
830 | createSpeedControlUI();
831 | removeVideoRestrictions();
832 | removePageRestrictions();
833 | monitorRestrictions();
834 | applySpeedToVideos();
835 | setupPageChangeListener();
836 |
837 | waitForContentReady(() => {
838 | setTimeout(checkNoVideoAutoNext, 3000);
839 | });
840 | }
841 |
842 | if (document.readyState === 'loading') {
843 | document.addEventListener('DOMContentLoaded', () => {
844 | setTimeout(init, 1000);
845 | });
846 | } else {
847 | setTimeout(init, 1000);
848 | }
849 |
850 | // ======= 全局设置管理 =======
851 | const GlobalSettings = {
852 | config: {
853 | captchaAuto: true,
854 | captchaApi: 'http://127.0.0.1:5000/ocr'
855 | },
856 | load() {
857 | try {
858 | const saved = localStorage.getItem('lms-enhance-settings');
859 | if (saved) Object.assign(this.config, JSON.parse(saved));
860 | } catch (e) {}
861 | },
862 | save() {
863 | try {
864 | localStorage.setItem('lms-enhance-settings', JSON.stringify(this.config));
865 | } catch (e) {}
866 | }
867 | };
868 | GlobalSettings.load();
869 |
870 | function showSettingsPanel() {
871 | let panel = document.getElementById('lms-settings-panel');
872 | if (panel) {
873 | panel.style.display = 'flex';
874 | return;
875 | }
876 |
877 | panel = document.createElement('div');
878 | panel.id = 'lms-settings-panel';
879 | panel.style.cssText = `
880 | display: flex;
881 | position: fixed;
882 | top: 0;
883 | left: 0;
884 | width: 100%;
885 | height: 100%;
886 | background: rgba(0,0,0,0.5);
887 | z-index: 20000;
888 | align-items: center;
889 | justify-content: center;
890 | `;
891 |
892 | const content = document.createElement('div');
893 | content.style.cssText = `
894 | background: white;
895 | border-radius: 12px;
896 | padding: 24px;
897 | min-width: 400px;
898 | max-width: 500px;
899 | box-shadow: 0 8px 32px rgba(0,0,0,0.3);
900 | `;
901 |
902 | content.innerHTML = `
903 |