// ==UserScript== // @name 学习通视频自动播放助手滁院专属,小出生福利嗷嗷嗷,(仅悬浮视频版,后续解题。。。有时间再说) // @namespace https://github.com/your-github-username/chaoxing-helper // @version 2.2 // @description 自动播放学习通视频、自动切换章节,悬浮窗实时显示进度、章节和预计总时长,适配所有学习通课程。 // @author VA1 // @match https://mooc1.chaoxing.com/mooc-ans/mycourse/studentstudy* // @match https://mooc1.chaoxing.com/ananas/modules/work/index.html* // @match https://mooc1.chaoxing.com/mooc-ans/api/work* // @icon https://www.google.com/s2/favicons?sz=64&domain=chaoxing.com // @license MIT // @grant none // @downloadURL https://update.greasyfork.icu/scripts/552238/%E5%AD%A6%E4%B9%A0%E9%80%9A%E8%A7%86%E9%A2%91%E8%87%AA%E5%8A%A8%E6%92%AD%E6%94%BE%E5%8A%A9%E6%89%8B%E6%BB%81%E9%99%A2%E4%B8%93%E5%B1%9E%EF%BC%8C%E5%B0%8F%E5%87%BA%E7%94%9F%E7%A6%8F%E5%88%A9%E5%97%B7%E5%97%B7%E5%97%B7%EF%BC%8C%EF%BC%88%E4%BB%85%E6%82%AC%E6%B5%AE%E8%A7%86%E9%A2%91%E7%89%88%EF%BC%8C%E5%90%8E%E7%BB%AD%E8%A7%A3%E9%A2%98%E3%80%82%E3%80%82%E3%80%82%E6%9C%89%E6%97%B6%E9%97%B4%E5%86%8D%E8%AF%B4%EF%BC%89.user.js // @updateURL https://update.greasyfork.icu/scripts/552238/%E5%AD%A6%E4%B9%A0%E9%80%9A%E8%A7%86%E9%A2%91%E8%87%AA%E5%8A%A8%E6%92%AD%E6%94%BE%E5%8A%A9%E6%89%8B%E6%BB%81%E9%99%A2%E4%B8%93%E5%B1%9E%EF%BC%8C%E5%B0%8F%E5%87%BA%E7%94%9F%E7%A6%8F%E5%88%A9%E5%97%B7%E5%97%B7%E5%97%B7%EF%BC%8C%EF%BC%88%E4%BB%85%E6%82%AC%E6%B5%AE%E8%A7%86%E9%A2%91%E7%89%88%EF%BC%8C%E5%90%8E%E7%BB%AD%E8%A7%A3%E9%A2%98%E3%80%82%E3%80%82%E3%80%82%E6%9C%89%E6%97%B6%E9%97%B4%E5%86%8D%E8%AF%B4%EF%BC%89.meta.js // ==/UserScript== (function () { 'use strict'; // ---------------------- 配置参数 ---------------------- const CONFIG = { targetPlaybackRate: 1.0, // 固定播放倍速(1.0为原速) floatPanel: { defaultWidth: 450, // 悬浮窗默认宽度 defaultHeight: 250, // 悬浮窗默认高度 minWidth: 300, // 悬浮窗最小宽度 minHeight: 180, // 悬浮窗最小高度 }, timing: { updateInterval: 1000, // 悬浮窗内容更新间隔(毫秒) initDelay: 2000, // 页面加载后初始化延迟(毫秒) nextChapterDelay: 3000 // 切换章节后重新初始化延迟(毫秒) } }; // ---------------------- 全局状态 ---------------------- let videoStatus = "初始化中..."; // 脚本运行状态 let currentVideo = null; // 当前视频元素 let floatPanel = null; // 悬浮窗对象(包含panel和content) let isUserInteracted = false; // 用户是否已与页面交互(解决自动播放限制) let totalChapters = 0; // 课程总章节数 let currentChapterIndex = 0; // 当前章节索引(从0开始) let estimatedTotalTime = "计算中..."; // 预计总学习时长 // ---------------------- 工具函数 ---------------------- /** * 格式化秒数为 分:秒 或 小时:分:秒 * @param {number} seconds - 总秒数 * @returns {string} 格式化后的时间字符串 */ function formatTime(seconds) { const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); return hours > 0 ? `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` : `${mins}:${secs.toString().padStart(2, '0')}`; } /** * 递归查找多层iframe中的视频元素 * @param {NodeList} iframes - iframe节点列表 * @returns {HTMLVideoElement|null} 找到的视频元素,无则返回null */ function findVideoInNestedIframes(iframes) { for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; // 检查当前iframe内的视频 let video = iframeDoc.querySelector('video'); if (video) return video; // 递归检查嵌套的iframe const nestedIframes = iframeDoc.querySelectorAll('iframe'); if (nestedIframes.length) { video = findVideoInNestedIframes(nestedIframes); if (video) return video; } } catch (e) { console.log('跨域iframe,跳过访问:', e.message); } } return null; } // ---------------------- 悬浮窗相关 ---------------------- /** * 创建可拖动、缩放的悬浮窗(仅主页面执行) * @returns {object|null} 包含panel和content的对象,非主页面返回null */ function createDraggableFloatPanel() { if (!isMainPage()) return null; // 移除已存在的悬浮窗,避免重复创建 const existingPanel = document.getElementById('chaoxing-video-helper-panel'); if (existingPanel) existingPanel.remove(); // 创建悬浮窗容器 const panel = document.createElement('div'); panel.id = 'chaoxing-video-helper-panel'; panel.style.cssText = ` position: fixed; top: 20px; left: 20px; background: rgba(0, 0, 0, 0.8); color: white; padding: 0; border-radius: 6px; font-size: 14px; z-index: 999999; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); width: ${CONFIG.floatPanel.defaultWidth}px; height: ${CONFIG.floatPanel.defaultHeight}px; user-select: none; overflow: hidden; resize: none; `; // 标题栏(拖动区域 + 缩放按钮) const header = document.createElement('div'); header.style.cssText = ` padding: 8px 12px; background: rgba(0, 0, 0, 0.5); cursor: move; border-top-left-radius: 6px; border-top-right-radius: 6px; font-weight: bold; display: flex; justify-content: space-between; align-items: center; `; header.innerHTML = ` 垃圾学习通视频助手(我写的这个就是一个垃圾脚本,rubbish)
`; // 内容区域(分上下两部分展示信息) const content = document.createElement('div'); content.style.cssText = ` padding: 10px 12px; height: calc(100% - 40px); overflow: auto; `; // 右下角调整大小的拖拽柄 const resizeHandle = document.createElement('div'); resizeHandle.style.cssText = ` position: absolute; bottom: 0; right: 0; width: 15px; height: 15px; background: rgba(255, 255, 255, 0.5); cursor: nwse-resize; border-bottom-right-radius: 6px; `; panel.appendChild(header); panel.appendChild(content); panel.appendChild(resizeHandle); document.body.appendChild(panel); // 缩放按钮功能 header.querySelectorAll('.scale-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const scale = parseFloat(btn.dataset.scale); panel.style.width = `${CONFIG.floatPanel.defaultWidth * scale}px`; panel.style.height = `${CONFIG.floatPanel.defaultHeight * scale}px`; panel.style.fontSize = `${14 * scale}px`; }); }); // 拖拽移动逻辑 let isDragging = false; let startPos = { x: 0, y: 0, top: 0, left: 0 }; header.addEventListener('mousedown', (e) => { if (e.target.classList.contains('scale-btn')) return; isDragging = true; startPos = { x: e.clientX, y: e.clientY, top: panel.offsetTop, left: panel.offsetLeft, }; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; panel.style.top = `${startPos.top + (e.clientY - startPos.y)}px`; panel.style.left = `${startPos.left + (e.clientX - startPos.x)}px`; }, { passive: false }); document.addEventListener('mouseup', () => { isDragging = false; }); // 调整大小逻辑 let isResizing = false; let startSize = { width: 0, height: 0, x: 0, y: 0 }; resizeHandle.addEventListener('mousedown', (e) => { isResizing = true; startSize = { width: panel.offsetWidth, height: panel.offsetHeight, x: e.clientX, y: e.clientY, }; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isResizing) return; const newWidth = Math.max(CONFIG.floatPanel.minWidth, startSize.width + (e.clientX - startSize.x)); const newHeight = Math.max(CONFIG.floatPanel.minHeight, startSize.height + (e.clientY - startSize.y)); panel.style.width = `${newWidth}px`; panel.style.height = `${newHeight}px`; }, { passive: false }); document.addEventListener('mouseup', () => { isResizing = false; }); return { panel, content }; } /** * 更新悬浮窗内容(课程、进度、状态等) */ function updateFloatPanelContent() { if (!floatPanel || !floatPanel.content) return; const { content } = floatPanel; // 初始化内容结构(仅首次执行) if (!content.dataset.initialized) { content.innerHTML = `
`; content.dataset.initialized = 'true'; } const topSection = content.querySelector('.top-section'); const bottomSection = content.querySelector('.bottom-section'); // 获取课程名称 let courseName = "未知课程"; const courseTitleSelectors = ['.course-title', '.chapter-title', 'h1', '.work-title', '.ans-title']; for (const selector of courseTitleSelectors) { const elem = document.querySelector(selector); if (elem && elem.textContent.trim()) { courseName = elem.textContent.trim().slice(0, 25); // 限制长度避免换行 break; } } // 章节信息 const chapterInfoText = totalChapters ? `章节:${currentChapterIndex + 1}/${totalChapters}` : "章节:加载中..."; // 计算预计总时长(当前章节时长 × 总章节数) if (currentVideo && totalChapters > 0) { const chapterDuration = currentVideo.duration || 0; const totalSeconds = Math.floor(chapterDuration * totalChapters); estimatedTotalTime = formatTime(totalSeconds); } // 上方:基础信息(课程、章节、倍速) topSection.innerHTML = `
课程:${courseName}
${chapterInfoText}
倍速:${currentVideo ? currentVideo.playbackRate + "x(固定)" : "1x(固定)"}
`; // 下方:详细进度与状态 if (!currentVideo) { bottomSection.innerHTML = `
当前进度:未加载视频
预计总时长:${estimatedTotalTime}
脚本状态:${videoStatus}
`; } else { const currentTime = formatTime(currentVideo.currentTime); const totalTime = formatTime(currentVideo.duration || 0); const progress = currentVideo.duration ? Math.floor((currentVideo.currentTime / currentVideo.duration) * 100) : 0; bottomSection.innerHTML = `
当前进度:${currentTime} / ${totalTime}(${progress}%)
本章剩余:${formatTime((currentVideo.duration || 0) - currentVideo.currentTime)}
预计总时长:${estimatedTotalTime}
脚本状态:${videoStatus}
`; } } /** * 更新脚本状态并刷新悬浮窗 * @param {string} status - 新状态文本 */ function updateStatus(status) { videoStatus = status; updateFloatPanelContent(); } // ---------------------- 章节与视频逻辑 ---------------------- /** * 获取章节信息(当前章节索引 + 总章节数) * @returns {object} { currentIndex: 当前章节索引, total: 总章节数 } */ function getChapterInformation() { // 学习通左侧章节列表的常见选择器(可根据页面更新扩展) const chapterSelectors = ['.chapter-item', '.ans-job-icon', '.ncells', '.chapter-unit']; let total = 0; let currentIndex = 0; for (const selector of chapterSelectors) { const chapterElements = document.querySelectorAll(selector); if (chapterElements.length) { total = chapterElements.length; // 查找“当前激活”的章节(含active类或播放标识) for (let i = 0; i < chapterElements.length; i++) { if (chapterElements[i].classList.contains('active') || chapterElements[i].classList.contains('current') || chapterElements[i].querySelector('.iconfont.icon-playing')) { currentIndex = i; break; } } break; // 找到后停止遍历选择器 } } console.log(`章节信息:第 ${currentIndex + 1}/${total} 章`); return { currentIndex, total }; } /** * 自动切换到下一章 * @param {object} chapterInfo - 章节信息(currentIndex, total) * @returns {boolean} 是否成功切换 */ function switchToNextChapter(chapterInfo) { if (chapterInfo.currentIndex + 1 >= chapterInfo.total) { updateStatus("所有章节已播放完毕!"); return false; } updateStatus("当前章节播放完毕,正在进入下一章..."); // 下一章按钮的常见选择器(可根据页面更新扩展) const nextBtnSelectors = ['.nextChapter', '.ans-next', '.chapter-next', '.icon-arrright', '.next-unit']; let nextBtn = null; for (const selector of nextBtnSelectors) { nextBtn = document.querySelector(selector); if (nextBtn) break; } if (nextBtn) { nextBtn.click(); console.log('已点击下一章按钮'); setTimeout(initVideoPlayback, CONFIG.timing.nextChapterDelay); return true; } else { updateStatus("未找到下一章按钮,请手动切换"); return false; } } /** * 初始化视频播放逻辑(查找视频、设置播放、监听结束事件) */ function initVideoPlayback() { const chapterInfo = getChapterInformation(); totalChapters = chapterInfo.total; currentChapterIndex = chapterInfo.currentIndex; // 查找视频元素(优先当前页面,再递归查找iframe内) let video = document.querySelector('video'); if (!video) { const iframes = document.querySelectorAll('iframe'); video = findVideoInNestedIframes(iframes); } if (!video) { updateStatus("未找到视频元素,2秒后重试..."); setTimeout(initVideoPlayback, 2000); return; } currentVideo = video; // 监听视频结束事件,自动切换下一章 video.addEventListener('ended', () => { updateStatus("当前视频播放完毕"); switchToNextChapter(chapterInfo); }, { once: true }); // 尝试播放视频(需用户先与页面交互一次) const attemptPlayVideo = () => { if (!video.paused) return; video.play().then(() => { updateStatus("视频自动播放中"); video.playbackRate = CONFIG.targetPlaybackRate; }).catch(err => { // 尝试点击页面内的播放按钮 const playButtons = document.querySelectorAll( '.vjs-big-play-button, .playButton, .startPlay, .ans-video-btn, .icon-play' ); playButtons.forEach(btn => { if (btn.offsetParent !== null) btn.click(); }); updateStatus("已自动点击播放按钮"); }); }; if (isUserInteracted) { attemptPlayVideo(); } else { updateStatus("等待用户首次点击页面(任意位置)..."); const handleInteraction = () => { isUserInteracted = true; updateStatus("用户已交互,开始自动播放"); attemptPlayVideo(); document.removeEventListener('click', handleInteraction); document.removeEventListener('touchstart', handleInteraction); }; document.addEventListener('click', handleInteraction); document.addEventListener('touchstart', handleInteraction); } updateFloatPanelContent(); } // ---------------------- 页面初始化 ---------------------- /** * 判断是否为学习通“主课程页面” * @returns {boolean} true = 主页面,false = iframe或其他页面 */ function isMainPage() { return window.location.href.includes('studentstudy'); } // 页面加载完成后初始化(延迟确保资源加载) window.addEventListener('load', () => { if (isMainPage()) { setTimeout(() => { console.log('学习通视频助手:页面加载完成,开始初始化'); floatPanel = createDraggableFloatPanel(); if (floatPanel) { updateFloatPanelContent(); initVideoPlayback(); // 定时更新悬浮窗内容(保持实时) setInterval(updateFloatPanelContent, CONFIG.timing.updateInterval); } }, CONFIG.timing.initDelay); } }); })();