// ==UserScript==
// @name 南大LMS智慧教育平台|MOOC增强
// @namespace http://tampermonkey.net/
// @version 0.20
// @description 南大LMS平台与MOOC平台加速进度 + 自动下一个 + 智能停止 + 无视频自动跳转 + 视频倍速控制 + 解除播放限制
// @author Hronrad
// @license GPL-3.0-only
// @match https://lms.nju.edu.cn/*
// @match https://www.icourse163.org/*
// @match https://icourse163.org/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
let isUserPaused = false;
let lastUserAction = 0;
let processedRequests = new Set();
let isVirtualRequest = false;
let allVideosCompleted = false;
let scriptPaused = false;
let noVideoCheckCount = 0;
const MAX_NO_VIDEO_CHECKS = 5;
let currentSpeed = 1;
let processedVideos = new Set();
let contentReady = false;
let pageLoadTime = Date.now();
const SPEED_STORAGE_KEY = `lms-video-speed-${location.hostname}`;
const isICourse163 = location.hostname.includes('icourse163.org');
function checkContentReady() {
const hasMainContent = document.querySelector('[ng-view]') ||
document.querySelector('.main-content') ||
document.querySelector('#main') ||
document.querySelector('.content-area');
const hasAngular = window.angular && document.querySelector('[ng-app]');
const timeElapsed = Date.now() - pageLoadTime > 2000;
const ready = (hasMainContent || hasAngular) && timeElapsed;
return ready;
}
function waitForContentReady(callback, maxWait = 15000) {
const startTime = Date.now();
function check() {
if (checkContentReady()) {
contentReady = true;
callback();
} else if (Date.now() - startTime < maxWait) {
setTimeout(check, 1000);
} else {
contentReady = true;
callback();
}
}
check();
}
function handlePageChange() {
scriptPaused = false;
allVideosCompleted = false;
noVideoCheckCount = 0;
contentReady = false;
pageLoadTime = Date.now();
waitForContentReady(() => {});
}
function setupPageChangeListener() {
let currentUrl = location.href;
let currentHash = location.hash;
const observer = new MutationObserver(() => {
if (location.href !== currentUrl || location.hash !== currentHash) {
currentUrl = location.href;
currentHash = location.hash;
handlePageChange();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
window.addEventListener('hashchange', handlePageChange);
window.addEventListener('popstate', handlePageChange);
}
function loadSavedSpeed() {
try {
const savedSpeed = localStorage.getItem(SPEED_STORAGE_KEY);
if (savedSpeed) {
const speed = parseFloat(savedSpeed);
if ([0.1, 1, 3, 16].includes(speed)) {
currentSpeed = speed;
}
}
} catch (e) {}
}
function saveSpeed(speed) {
try {
localStorage.setItem(SPEED_STORAGE_KEY, speed.toString());
window.dispatchEvent(new CustomEvent('lms-speed-changed', {
detail: { speed, timestamp: Date.now() }
}));
} catch (e) {}
}
function syncSpeedAcrossTabs() {
window.addEventListener('lms-speed-changed', (e) => {
if (e.detail.speed !== currentSpeed) {
currentSpeed = e.detail.speed;
applySpeedToVideos();
updateSpeedButton();
}
});
window.addEventListener('storage', (e) => {
if (e.key === SPEED_STORAGE_KEY && e.newValue) {
const newSpeed = parseFloat(e.newValue);
if ([0.1, 1, 3, 16].includes(newSpeed) && newSpeed !== currentSpeed) {
currentSpeed = newSpeed;
applySpeedToVideos();
updateSpeedButton();
}
}
});
}
function applySpeedToVideos() {
document.querySelectorAll('video').forEach(video => {
if (video.playbackRate !== currentSpeed) {
video.playbackRate = currentSpeed;
}
});
}
function updateSpeedButton() {
const speedButton = document.getElementById('lms-speed-button');
const speedMenu = document.getElementById('lms-speed-menu');
if (speedButton) {
speedButton.innerHTML = `${currentSpeed}x`;
}
if (speedMenu) {
speedMenu.querySelectorAll('div').forEach((div, i) => {
const itemSpeed = [0.1, 1, 3, 16][i];
div.style.background = itemSpeed === currentSpeed ? '#e3f2fd' : 'white';
div.style.fontWeight = itemSpeed === currentSpeed ? 'bold' : 'normal';
});
}
}
function removeVideoRestrictions() {
const videos = document.querySelectorAll('video:not([data-restrictions-removed])');
videos.forEach(video => {
video.setAttribute('data-restrictions-removed', 'true');
video.setAttribute('allow-foward-seeking', 'true');
video.setAttribute('data-allow-download', 'true');
video.setAttribute('allow-right-click', 'true');
video.removeAttribute('forward-seeking-warning');
video.controls = true;
video.oncontextmenu = null;
});
}
function removePageRestrictions() {
document.oncontextmenu = null;
document.onselectstart = null;
document.ondragstart = null;
document.onkeydown = null;
}
function monitorRestrictions() {
const observer = new MutationObserver((mutations) => {
let needsUpdate = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && (node.tagName === 'VIDEO' || node.querySelector('video'))) {
needsUpdate = true;
}
});
}
});
if (needsUpdate) {
setTimeout(removeVideoRestrictions, 200);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
function createSpeedControlUI() {
if (document.getElementById('lms-speed-container')) return;
const container = document.createElement('div');
container.id = 'lms-speed-container';
container.style.cssText = `
position: fixed;
top: 50%;
right: -45px;
transform: translateY(-50%);
z-index: 10000;
transition: right 0.3s ease;
display: flex;
flex-direction: column;
align-items: flex-end;
`;
const speedButton = document.createElement('button');
speedButton.id = 'lms-speed-button';
speedButton.innerHTML = `${currentSpeed}x`;
speedButton.style.cssText = `
width: 60px;
height: 35px;
background: #007bff;
color: white;
border: none;
border-radius: 8px 0 0 8px;
font-size: 14px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,123,255,0.3);
transition: all 0.3s ease;
margin-bottom: 5px;
`;
const speedMenu = document.createElement('div');
speedMenu.id = 'lms-speed-menu';
speedMenu.style.cssText = `
background: white;
border: 1px solid #ddd;
border-radius: 8px 0 0 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
min-width: 80px;
overflow: hidden;
opacity: 0;
transform: translateX(10px);
transition: all 0.3s ease;
pointer-events: none;
`;
[0.1, 1, 3, 16].forEach(speed => {
const item = document.createElement('div');
item.textContent = `${speed}x`;
item.style.cssText = `
padding: 10px 16px;
cursor: pointer;
transition: background 0.2s ease;
font-size: 13px;
text-align: center;
${speed === currentSpeed ? 'background: #e3f2fd; font-weight: bold;' : ''}
`;
item.onmouseenter = () => item.style.background = speed === currentSpeed ? '#bbdefb' : '#f5f5f5';
item.onmouseleave = () => item.style.background = speed === currentSpeed ? '#e3f2fd' : 'white';
item.onclick = () => {
setVideoSpeed(speed);
speedButton.innerHTML = `${speed}x`;
updateMenuSelection(speedMenu, speed);
};
speedMenu.appendChild(item);
});
function updateMenuSelection(menu, selectedSpeed) {
menu.querySelectorAll('div').forEach((div, i) => {
const itemSpeed = [0.1, 1, 3, 16][i];
div.style.background = itemSpeed === selectedSpeed ? '#e3f2fd' : 'white';
div.style.fontWeight = itemSpeed === selectedSpeed ? 'bold' : 'normal';
});
}
container.appendChild(speedButton);
container.appendChild(speedMenu);
let isExpanded = false;
let hideTimeout;
function showControls() {
clearTimeout(hideTimeout);
isExpanded = true;
container.style.right = '0px';
speedButton.style.background = '#0056b3';
speedButton.style.transform = 'scale(1.05)';
speedMenu.style.opacity = '1';
speedMenu.style.transform = 'translateX(0)';
speedMenu.style.pointerEvents = 'auto';
}
function hideControls() {
hideTimeout = setTimeout(() => {
isExpanded = false;
container.style.right = '-45px';
speedButton.style.background = '#007bff';
speedButton.style.transform = 'scale(1)';
speedMenu.style.opacity = '0';
speedMenu.style.transform = 'translateX(10px)';
speedMenu.style.pointerEvents = 'none';
}, 300);
}
container.onmouseenter = showControls;
container.onmouseleave = hideControls;
speedButton.onclick = (e) => {
e.stopPropagation();
if (isExpanded) {
speedMenu.style.display = speedMenu.style.display === 'none' ? 'block' : 'none';
}
};
document.addEventListener('click', (e) => {
if (!container.contains(e.target)) {
speedMenu.style.display = 'block';
}
});
const hoverIndicator = document.createElement('div');
hoverIndicator.style.cssText = `
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 30px;
background: linear-gradient(45deg, #007bff, #0056b3);
border-radius: 3px 0 0 3px;
opacity: 0.7;
animation: pulse 2s infinite;
`;
const style = document.createElement('style');
style.textContent = `
@keyframes pulse {
0%, 100% { opacity: 0.7; }
50% { opacity: 0.3; }
}
`;
document.head.appendChild(style);
container.appendChild(hoverIndicator);
document.body.appendChild(container);
}
function setVideoSpeed(speed) {
currentSpeed = speed;
saveSpeed(speed);
applySpeedToVideos();
updateSpeedButton();
}
function initICourse163() {
loadSavedSpeed();
syncSpeedAcrossTabs();
removeVideoRestrictions();
removePageRestrictions();
monitorRestrictions();
createSpeedControlUI();
setInterval(() => {
applySpeedToVideos();
}, 2000);
}
if (isICourse163) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(initICourse163, 500));
} else {
setTimeout(initICourse163, 500);
}
return;
}
loadSavedSpeed();
syncSpeedAcrossTabs();
Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
document.addEventListener('visibilitychange', (e) => e.stopImmediatePropagation(), true);
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, ...args) {
this._method = method;
this._url = url;
this._isVirtual = isVirtualRequest;
return originalOpen.call(this, method, url, ...args);
};
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(data) {
const url = this._url || '';
if (scriptPaused) {
return originalSend.call(this, data);
}
if (!this._isVirtual &&
(url.includes('/statistics/api/online-videos') ||
url.includes('/api/course/activities-read/')) &&
this._method === 'POST' && data) {
try {
const jsonData = JSON.parse(data);
const requestKey = `${url}-${JSON.stringify(jsonData)}`;
if (!processedRequests.has(requestKey)) {
processedRequests.add(requestKey);
createVirtualSessions(url, jsonData);
setTimeout(() => processedRequests.delete(requestKey), 10000);
}
} catch (e) {}
}
return originalSend.call(this, data);
};
function createVirtualSessions(url, originalData) {
if (scriptPaused) return;
const sessionCount = 10;
const maxDuration = 30;
const originalDuration = (originalData.end || 0) - (originalData.start || 0);
const isLargeDuration = originalDuration > maxDuration;
for (let i = 1; i < sessionCount; i++) {
setTimeout(() => {
if (scriptPaused) return;
const virtualData = JSON.parse(JSON.stringify(originalData));
if (isLargeDuration) {
const segmentDuration = Math.min(maxDuration, Math.floor(originalDuration / sessionCount) + 5);
const baseStart = originalData.start || 0;
virtualData.start = baseStart + (i - 1) * segmentDuration + Math.floor(Math.random() * 3);
virtualData.end = virtualData.start + segmentDuration + Math.floor(Math.random() * 3);
if (virtualData.end > originalData.end) {
virtualData.end = originalData.end;
}
if (virtualData.start >= virtualData.end) {
virtualData.start = virtualData.end - Math.min(5, segmentDuration);
}
} else {
if (virtualData.start !== undefined) {
virtualData.start += Math.floor(Math.random() * 3);
}
if (virtualData.end !== undefined) {
virtualData.end += Math.floor(Math.random() * 3);
}
}
const duration = (virtualData.end || 0) - (virtualData.start || 0);
if (duration <= 0 || duration > maxDuration * 2) {
return;
}
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(virtualData),
credentials: 'same-origin'
}).then(response => {}).catch(error => {});
}, i * 400 + Math.random() * 300);
}
}
function detectUserAction(e) {
const target = e.target;
if (target.closest('.vjs-play-control') ||
target.closest('.vjs-big-play-button') ||
target.closest('button') ||
target.tagName === 'BUTTON') {
lastUserAction = Date.now();
setTimeout(() => {
document.querySelectorAll('video').forEach(video => {
if (video.paused) {
isUserPaused = true;
}
});
}, 100);
}
}
document.addEventListener('click', detectUserAction, true);
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
lastUserAction = Date.now();
}
}, true);
function hasNextButton() {
try {
const angular = window.angular;
if (angular) {
const scope = angular.element(document.body).scope();
if ((scope && scope.navigation && scope.navigation.nextItem) ||
(scope && scope.nextActivity)) {
return true;
}
}
} catch (e) {}
const nextSelectors = [
'button[ng-click*="changeActivity(nextActivity)"]',
'button[ng-if="nextActivity"]',
'a[ng-click*="goToNextTopic()"]',
'a.next[ng-if*="!isLastTopic()"]',
'span.icon-student-circle[ng-click*="navigation.goNext"]',
'button[ng-click*="goNext"]',
'a.next[ng-click="goToNextTopic()"]',
'button.button[ng-click*="changeActivity(nextActivity)"]'
];
for (const selector of nextSelectors) {
const nextButton = document.querySelector(selector);
if (nextButton && nextButton.offsetParent !== null) {
return true;
}
}
try {
const nextTopicLink = document.querySelector('a.next[ng-click="goToNextTopic()"]');
if (nextTopicLink) {
const scope = window.angular.element(nextTopicLink).scope();
if (scope && typeof scope.isLastTopic === 'function') {
if (!scope.isLastTopic() && nextTopicLink.offsetParent !== null) {
return true;
}
}
}
const nextActivityBtn = document.querySelector('button[ng-click*="changeActivity(nextActivity)"]');
if (nextActivityBtn) {
const scope = window.angular.element(nextActivityBtn).scope();
if (scope && scope.nextActivity && nextActivityBtn.offsetParent !== null) {
return true;
}
}
} catch (e) {}
const elements = document.querySelectorAll('button, a');
for (const el of elements) {
if (el.textContent.includes('下一个') && el.offsetParent !== null) {
return true;
}
}
return false;
}
function hasVideos() {
return document.querySelectorAll('video').length > 0;
}
function checkAllVideosCompleted() {
const videos = document.querySelectorAll('video');
if (videos.length === 0) return false;
return Array.from(videos).every(video => {
const isEnded = video.ended;
const isDurationComplete = video.duration > 0 &&
Math.abs(video.currentTime - video.duration) < 1;
const isNearComplete = video.duration > 0 &&
video.currentTime / video.duration >= 0.98;
return isEnded || isDurationComplete || isNearComplete;
});
}
function checkNoVideoAutoNext() {
if (scriptPaused) return;
if (!contentReady) {
return;
}
if (!hasVideos()) {
if (hasNextButton()) {
noVideoCheckCount++;
if (noVideoCheckCount >= MAX_NO_VIDEO_CHECKS) {
noVideoCheckCount = 0;
autoClickNext();
}
} else {
pauseScript();
}
} else {
noVideoCheckCount = 0;
}
}
function pauseScript() {
if (scriptPaused) return;
scriptPaused = true;
allVideosCompleted = true;
document.querySelectorAll('video').forEach(video => {
if (!video.paused) {
video.pause();
}
});
}
function keepVideoPlaying() {
if (scriptPaused) return;
document.querySelectorAll('video').forEach(video => {
if (video.paused) {
const timeSinceUserAction = Date.now() - lastUserAction;
if (isUserPaused && timeSinceUserAction < 3000) {
return;
}
if (video.readyState >= 2) {
video.play().then(() => {
isUserPaused = false;
}).catch(() => {});
}
} else {
if (isUserPaused && Date.now() - lastUserAction > 2000) {
isUserPaused = false;
}
}
});
}
function performVirtualUserAction() {
if (scriptPaused) return;
const videos = document.querySelectorAll('video');
const playButtons = document.querySelectorAll('.vjs-play-control');
if (videos.length > 0 && !isUserPaused) {
videos.forEach((video, index) => {
if (!video.paused) {
if (playButtons[index]) {
playButtons[index].dispatchEvent(new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
}));
} else {
video.pause();
}
setTimeout(() => {
if (scriptPaused) return;
if (playButtons[index]) {
playButtons[index].dispatchEvent(new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
}));
} else {
video.play().catch(() => {});
}
}, 100);
}
});
}
}
function setupVideoCompletionHandler() {
const videos = document.querySelectorAll('video:not([data-completion-handler])');
videos.forEach(video => {
video.setAttribute('data-completion-handler', 'true');
video.playbackRate = currentSpeed;
video.addEventListener('ended', function() {
setTimeout(() => {
if (checkAllVideosCompleted()) {
if (hasNextButton()) {
autoClickNext();
} else {
pauseScript();
}
} else {
autoClickNext();
}
}, 2000);
});
});
}
function autoClickNext() {
if (scriptPaused) return;
try {
const angular = window.angular;
if (angular) {
const scope = angular.element(document.body).scope();
if (scope && scope.nextActivity && scope.changeActivity) {
scope.changeActivity(scope.nextActivity);
scope.$apply();
return;
}
if (scope && scope.goToNextTopic) {
scope.goToNextTopic();
scope.$apply();
return;
}
if (scope && scope.navigation && scope.navigation.goNext) {
scope.navigation.goNext();
scope.$apply();
return;
}
}
} catch (e) {}
const nextSelectors = [
'button[ng-click*="changeActivity(nextActivity)"]',
'button[ng-if="nextActivity"]',
'a[ng-click*="goToNextTopic()"]',
'a.next[ng-if*="!isLastTopic()"]',
'button[ng-click*="goNext"]',
'a.next[ng-click="goToNextTopic()"]',
'button.button[ng-click*="changeActivity(nextActivity)"]'
];
for (const selector of nextSelectors) {
const nextButton = document.querySelector(selector);
if (nextButton && nextButton.offsetParent !== null) {
if (nextButton.hasAttribute('ng-click') && window.angular) {
try {
const scope = window.angular.element(nextButton).scope();
if (scope) {
scope.$eval(nextButton.getAttribute('ng-click'));
scope.$apply();
return;
}
} catch (e) {}
}
nextButton.click();
return;
}
}
const allElements = document.querySelectorAll('button, a, span[ng-click]');
for (const element of allElements) {
const text = element.textContent.trim();
const ngClick = element.getAttribute('ng-click') || '';
if ((text.includes('下一个') || ngClick.includes('changeActivity') ||
ngClick.includes('goToNextTopic') || ngClick.includes('goNext')) &&
element.offsetParent !== null) {
if (ngClick && window.angular) {
try {
const scope = window.angular.element(element).scope();
if (scope) {
scope.$eval(ngClick);
scope.$apply();
return;
}
} catch (e) {}
}
element.click();
return;
}
}
pauseScript();
}
setInterval(keepVideoPlaying, 2000);
setInterval(performVirtualUserAction, 1000);
setInterval(() => {
setupVideoCompletionHandler();
applySpeedToVideos();
}, 3000);
setInterval(checkNoVideoAutoNext, 6000);
function init() {
keepVideoPlaying();
setupVideoCompletionHandler();
createSpeedControlUI();
removeVideoRestrictions();
removePageRestrictions();
monitorRestrictions();
applySpeedToVideos();
setupPageChangeListener();
waitForContentReady(() => {
setTimeout(checkNoVideoAutoNext, 3000);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(init, 1000);
});
} else {
setTimeout(init, 1000);
}
})();