// ==UserScript== // @name 优课联盟全自动刷课(目录连播+防暂停+二倍速静音) // @namespace http://tampermonkey.net/ // @version 1.1 // @description 自动完成视频课程:目录连播、跳过已完成、2倍速、静音、防移出、防页面隐藏 // @author YourName // @match *://*.uooc.net.cn/* // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ======================== 原有功能:防移出暂停 ======================== function handleTarget(node) { node.addEventListener('mouseout', e => { e.stopImmediatePropagation(); e.preventDefault(); }, true); if (node.onmouseout) { node.onmouseout = null; } } new MutationObserver(mutations => { const selectors = ['.video-wrapper', '.video-container', 'div[class*="video"]', 'div[id*="player"]']; selectors.forEach(selector => { document.querySelectorAll(selector).forEach(handleTarget); }); }).observe(document.body, { childList: true, subtree: true }); setInterval(() => { document.querySelectorAll('video, .video-player').forEach(video => { if (video.__protected) return; video.__protected = true; video.addEventListener('pause', e => { if (e.type === 'pause' && !video.paused) { video.play().catch(() => {}); } }); }); }, 500); // ======================== 原有功能:覆盖可见性属性 ======================== Object.defineProperty(document, 'visibilityState', { get: () => 'visible' }); Object.defineProperty(document, 'hidden', { get: () => false }); const originalAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function(type, listener, options) { if (type === 'visibilitychange') return; originalAddEventListener.call(this, type, listener, options); }; // ======================== 原有功能:强制静音+二倍速+自动播放 ======================== function enhanceVideoControl(video) { if (video.dataset.enhanced) return; video._originalPause = video.pause; video.pause = () => console.log('[防暂停] 已拦截暂停操作'); video._originalPlay = video.play; video.play = function() { this.muted = true; this.playbackRate = 2; return this._originalPlay(); }; video.muted = true; video.playbackRate = 2; video.dataset.enhanced = 'true'; if (video.paused) video.play().catch(e => {}); } new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { node.querySelectorAll('video').forEach(enhanceVideoControl); } }); }); }).observe(document.documentElement, { childList: true, subtree: true }); document.querySelectorAll('video').forEach(enhanceVideoControl); setInterval(() => { document.querySelectorAll('video').forEach(video => { if (video.playbackRate !== 2) video.playbackRate = 2; if (!video.muted) video.muted = true; if (video.paused && !video.ended) video.play().catch(e => {}); }); }, 1000); // ======================== 新增功能:基于右侧目录的自动连播 ======================== // 展开章节(如果需要) function expandChapterIfNeeded(chapterElement) { // 检查当前章节是否折叠(icon-xiangxia 表示折叠状态) const toggleIcon = chapterElement.querySelector('.icon-xiangxia'); if (toggleIcon) { // 模拟点击章节标题展开 const titleDiv = chapterElement.querySelector('.basic.chapter'); if (titleDiv) { titleDiv.click(); console.log('[自动连播] 展开章节'); // 等待DOM更新 return new Promise(resolve => setTimeout(resolve, 500)); } } return Promise.resolve(); } // 获取所有未完成的视频任务(按DOM顺序) function getAllUnfinishedVideos() { // 所有资源项 (.basic) 且包含视频图标 (icon-video) const videoItems = Array.from(document.querySelectorAll('.resourcelist .basic')).filter(item => { const hasVideoIcon = item.querySelector('.icon-video') !== null; const isFinished = item.classList.contains('complete'); return hasVideoIcon && !isFinished; }); return videoItems; } // 查找当前正在播放的视频在目录中对应的项 function getCurrentVideoItem() { // 方法1:通过 active 类(当前高亮项) let activeItem = document.querySelector('.learn-main-right .basic.active'); if (activeItem && activeItem.querySelector('.icon-video')) { return activeItem; } // 方法2:通过视频标题匹配(降级方案) const videoElem = document.querySelector('video'); if (!videoElem) return null; const videoTitle = videoElem.getAttribute('src') || ''; // 尝试从右侧列表中匹配包含相同文件名的项(模糊匹配) const items = document.querySelectorAll('.resourcelist .basic'); for (let item of items) { const nameSpan = item.querySelector('.tag-source-name'); if (nameSpan && nameSpan.textContent.includes('视频')) { // 实际无法精确匹配,所以优先使用方法1 return null; } } return activeItem; } // 找到并点击下一个未完成的视频 async function findAndClickNextVideo() { console.log('[自动连播] 视频结束,准备查找下一个...'); // 获取所有未完成视频(按DOM顺序) const unfinishedVideos = getAllUnfinishedVideos(); if (unfinishedVideos.length === 0) { console.log('[自动连播] 没有未完成的视频任务,课程可能已完成'); return; } // 当前激活的视频项(正在播放的) const currentItem = getCurrentVideoItem(); let nextItem = null; if (currentItem) { // 找到当前项在未完成列表中的位置,取下一个 const currentIndex = unfinishedVideos.findIndex(item => item === currentItem); if (currentIndex !== -1 && currentIndex + 1 < unfinishedVideos.length) { nextItem = unfinishedVideos[currentIndex + 1]; } else if (currentIndex === -1) { // 当前项未标记为未完成(可能已完成或未激活),取第一个未完成 nextItem = unfinishedVideos[0]; } } else { // 无法定位当前项,直接取第一个未完成 nextItem = unfinishedVideos[0]; } if (!nextItem) { console.log('[自动连播] 未找到下一个未完成的视频'); return; } // 确保下一项所在的章节已展开 const parentSectionLi = nextItem.closest('li[id]'); // 章节li if (parentSectionLi) { const chapterHeader = parentSectionLi.querySelector('.basic.chapter, .basic[ui-sref*="main.chapter.section"]'); if (chapterHeader && !chapterHeader.classList.contains('open')) { await expandChapterIfNeeded(chapterHeader.parentElement || parentSectionLi); } } // 滚动到视图并点击 nextItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => { nextItem.click(); console.log('[自动连播] 已点击下一个视频:', nextItem); }, 300); } // 绑定视频结束事件(确保每个新视频都绑定) function bindVideoEnded(video) { if (video._hasEndedListener) return; video._hasEndedListener = true; video.addEventListener('ended', () => { console.log('[自动连播] 检测到视频播放完成'); // 延迟执行,确保后端记录进度 setTimeout(findAndClickNextVideo, 2000); }); } // 监听新添加的视频,绑定 ended 事件 const observerVideo = new MutationObserver(() => { document.querySelectorAll('video').forEach(video => { if (!video._hasEndedListener) { bindVideoEnded(video); } }); }); observerVideo.observe(document.body, { childList: true, subtree: true }); // 绑定现有视频 document.querySelectorAll('video').forEach(bindVideoEnded); console.log('[自动连播] 脚本已启动,等待视频播放结束...'); })();