您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
南大LMS平台与MOOC平台加速进度 + 自动下一个 + 智能停止 + 无视频自动跳转 + 视频倍速控制 + 解除播放限制
// ==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); } })();