// ==UserScript== // @name 人才呀(博思)视频自动二倍速+自动下一集 // @namespace cx-free-helper // @version 4.7.0 // @description 人才呀(博思)网课视频助手:自动 2 倍速播放、自动跳过片尾、播放完毕自动切换下一集;提升起播可靠性、换集识别与运行安全。 // @author C-Y-T-258 // @license MIT // @supportURL https://greasyfork.org/zh-CN/scripts/585608 // @match http://rencaiya.vip/college/Video/* // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; console.log('[视频助手] 脚本已加载 v4.7.0(播完切换+安全版)'); // ========== 运行模式 ========== const RUN_MODE = { SPEED_2X: 'speed_2x', SKIP_TO_END: 'skip_to_end' }; // ========== 配置 ========== const CONFIG = { targetSpeed: 2.0, minPlayTime: 3, switchDelay: 2000, checkInterval: 500, skipToEndThreshold: 5, pageLoadWait: 3000 }; // ========== 状态 ========== let isRunning = false; let hasSwitched = false; let videoStartTime = 0; let isListOpen = false; let currentMode = RUN_MODE.SPEED_2X; let switchAttempts = 0; let lastVideoTitle = ''; let isPageLoading = false; let mainCheckInterval = null; let videoFinished = false; // 4.6.0:提到模块级,失败时可复位 let consecutiveFailures = 0; // 4.6.0:连续切换失败计数(上限 3) let isSwitching = false; // 4.6.0:切换进行中,防并发重复切换 let finishGraceUntil = 0; // 4.6.0:换集后宽限期,期间不判「播完」 let lastHref = ''; // 4.7.0:URL 变化作为换集信号 let mutedFallbackUsed = false; // 4.7.0:是否已用静音兜底起播 let warningScanTick = 0; // 4.7.0:预警文本扫描节流计数 // ========== 获取当前页面标题 ========== function getCurrentPageTitle() { const titleEl = document.querySelector('.ant-page-header-heading-title'); return titleEl ? titleEl.textContent.trim() : ''; } // ========== 获取视频元素 ========== function getVideo() { const video = document.querySelector('video'); return (video && video.duration > 0) ? video : null; } // ========== 点击"章节"按钮展开列表 ========== async function openChapterList() { if (isListOpen) return true; const existingPeriods = document.querySelectorAll('.period'); if (existingPeriods.length > 0) { isListOpen = true; return true; } const chapterBtn = document.querySelector('.chapter img[src*="chapter.png"]'); if (chapterBtn) { chapterBtn.click(); await new Promise(r => setTimeout(r, 1500)); isListOpen = true; return true; } const chapterText = document.querySelector('.chapter'); if (chapterText) { chapterText.click(); await new Promise(r => setTimeout(r, 1500)); isListOpen = true; return true; } return false; } // ========== 关闭章节列表 ========== function closeChapterList() { const chapterBtn = document.querySelector('.chapter img[src*="chapter.png"]'); if (chapterBtn && isListOpen) { chapterBtn.click(); isListOpen = false; } } // ========== 检测任务点是否已完成 ========== function isTaskCompleted(periodElement) { const progressImg = periodElement.querySelector('img.studyProgress'); if (progressImg) { const src = progressImg.src || ''; if (src.includes('full.png')) return true; if (src.includes('empty.png')) return false; if (src.includes('halfFull.png')) return false; } return false; } // ========== 判断是否是评测/任务项 ========== function isTestTask(periodElement) { // 4.6.0:补「练习」类课次(图标 videoCode.png),此前被当成视频课程点击 → 落到无 video 页 const taskIcon = periodElement.querySelector('img[src*="videoTask.png"], img[src*="videoCode.png"]'); if (taskIcon) return true; const text = periodElement.textContent || ''; if (text.includes('评测') || text.includes('考试') || text.includes('测试') || text.includes('测评')) { return true; } // 只认「练习:」标题前缀,避免「类型转换练习」这类课程名被误判 if (/^\s*练习\s*[::]/.test(getPeriodTitle(periodElement))) { return true; } return false; } // ========== 获取课时名称 ========== function getPeriodTitle(periodElement) { const titleSpan = periodElement.querySelector('.periodH span'); return titleSpan ? titleSpan.textContent.trim() : periodElement.textContent?.trim() || ''; } // ========== 获取可点击的元素 ========== function getClickableElement(periodElement) { const titleSpan = periodElement.querySelector('.periodH span'); if (titleSpan) return titleSpan; const spans = periodElement.querySelectorAll('span'); for (let span of spans) { const text = span.textContent.trim(); if (text && text.length > 5 && (text.includes('课程') || text.includes('评测'))) { return span; } } return periodElement; } // ========== 获取视频列表 ========== function getVideoList() { const videos = []; const periodElements = document.querySelectorAll('.period'); if (periodElements.length === 0) { return videos; } periodElements.forEach((period, index) => { const title = getPeriodTitle(period); if (!title || title.length < 2) return; if (title === '章节') return; if (title.includes('资源反馈')) return; const isTest = isTestTask(period); const isCourse = title.includes('课程') || title.match(/\d+-\d+/) || title.match(/^课程[::]/); const isCompleted = isTaskCompleted(period); if (isCourse || isTest) { videos.push({ index: index, element: period, clickableElement: getClickableElement(period), text: title, isTest: isTest, isCourse: isCourse, isCompleted: isCompleted }); } }); return videos; } // ========== 4.6.0:用站点自带的当前项标记定位(其余课次是 .periodH,当前项是 .periodI)========== function findCurrentPeriodElement() { const periods = document.querySelectorAll('.period'); for (let i = 0; i < periods.length; i++) { if (periods[i].querySelector('.periodI')) return periods[i]; } // 兜底:行内含「正在学」文本 for (let i = 0; i < periods.length; i++) { if ((periods[i].textContent || '').includes('正在学')) return periods[i]; } return null; } // ========== 查找当前视频在列表中的索引 ========== function findCurrentVideoIndex(videos) { // 策略0(4.6.0 新增,最可靠):DOM 当前项标记 → 按元素引用匹配 const currentPeriod = findCurrentPeriodElement(); if (currentPeriod) { for (let i = 0; i < videos.length; i++) { if (videos[i].element === currentPeriod) { console.log('[视频助手] 通过当前项标记定位到索引:', i, videos[i].text); return i; } } } const pageTitle = getCurrentPageTitle(); const pageMatch = pageTitle.match(/(\d+)-(\d+)/); if (pageMatch) { for (let i = 0; i < videos.length; i++) { const videoMatch = videos[i].text.match(/(\d+)-(\d+)/); if (videoMatch && pageMatch[1] === videoMatch[1] && pageMatch[2] === videoMatch[2]) { return i; } } } for (let i = 0; i < videos.length; i++) { const videoTitle = videos[i].text; if (pageTitle && videoTitle) { const pageSub = pageTitle.replace(/课程[::]?\s*/, '').substring(0, 20); const videoSub = videoTitle.replace(/课程[::]?\s*/, '').substring(0, 20); if (pageSub === videoSub || pageTitle.includes(videoTitle.substring(0, 15)) || videoTitle.includes(pageTitle.substring(0, 15))) { return i; } } } for (let i = 0; i < videos.length; i++) { if (videos[i].isCourse && !videos[i].isCompleted) { return i; } } for (let i = 0; i < videos.length; i++) { if (videos[i].isCourse) { return i; } } return -1; } // ========== 检查是否是测评/空页面 ========== function checkTestOrEmptyPage() { const pageTitle = getCurrentPageTitle(); const video = getVideo(); if (pageTitle === '#' || pageTitle === '' || !pageTitle) { return { shouldSkip: true }; } if (!video) { const pageContent = document.body.innerText || ''; if (pageContent.includes('测评') || pageContent.includes('考试') || pageContent.includes('测试') || pageTitle.includes('测评')) { return { shouldSkip: true }; } return { shouldSkip: true }; } if (pageTitle.includes('测评') || pageTitle.includes('考试') || pageTitle.includes('测试')) { return { shouldSkip: true }; } return { shouldSkip: false }; } // ========== 拖动进度条到底部 ========== function skipToEnd() { const video = getVideo(); if (!video || !video.duration) { return false; } const targetTime = Math.max(0, video.duration - CONFIG.skipToEndThreshold); video.currentTime = targetTime; console.log('[视频助手] 进度条已拖动到:', targetTime.toFixed(1), '秒'); return true; } // ========== 点击视频项 ========== async function clickVideoItem(videoItem) { console.log('[视频助手] 准备点击:', videoItem.text); notify('即将播放: ' + videoItem.text.substring(0, 25)); hasSwitched = true; isPageLoading = true; switchAttempts = 0; lastVideoTitle = getCurrentPageTitle(); setTimeout(() => { const clickTarget = videoItem.clickableElement || videoItem.element; clickTarget.click(); console.log('[视频助手] 已点击:', videoItem.text); setTimeout(() => { closeChapterList(); }, 800); setTimeout(() => { const titleNow = getCurrentPageTitle(); hasSwitched = false; isPageLoading = false; isListOpen = false; videoStartTime = Date.now(); switchAttempts = 0; // 4.6.0:校验点击是否真的生效,未生效则允许重试(避免永久卡死) if (titleNow === lastVideoTitle) { consecutiveFailures++; videoFinished = false; console.log('[视频助手] 点击后标题未变化,判定切换失败'); if (consecutiveFailures > 3) { isRunning = false; notify('自动运行已停止(连续切换失败 3 次)'); } } else { consecutiveFailures = 0; videoFinished = false; } }, CONFIG.pageLoadWait); }, CONFIG.switchDelay); return true; } // ========== 通过点击切换到下一个视频 ========== async function switchToNextByClick() { if (hasSwitched || isPageLoading) { return false; } const opened = await openChapterList(); if (!opened) { return false; } await new Promise(r => setTimeout(r, 2000)); let videos = getVideoList(); if (videos.length === 0) { await new Promise(r => setTimeout(r, 2000)); videos = getVideoList(); if (videos.length === 0) { return false; } } const currentIndex = findCurrentVideoIndex(videos); if (currentIndex === -1) { for (let i = 0; i < videos.length; i++) { if (videos[i].isCourse && !videos[i].isCompleted && !videos[i].isTest) { return await clickVideoItem(videos[i]); } } return false; } let nextIndex = currentIndex + 1; while (nextIndex < videos.length) { const next = videos[nextIndex]; if (next.isTest || next.isCompleted || !next.isCourse) { nextIndex++; continue; } return await clickVideoItem(next); } for (let i = 0; i < videos.length; i++) { const video = videos[i]; if (video.isCourse && !video.isCompleted && !video.isTest) { return await clickVideoItem(video); } } notify('🎉 所有课程已完成!'); isRunning = false; return false; } // ========== 切换到下一个视频 ========== async function switchToNext() { if (hasSwitched || isPageLoading || isSwitching) return false; switchAttempts++; if (switchAttempts > 50) { isRunning = false; notify('自动运行已停止(切换次数过多)'); return false; } console.log('[视频助手] 开始切换下一集...'); isSwitching = true; let clicked = false; try { clicked = await switchToNextByClick(); } catch (e) { // 4.6.0:主循环调用不 await,这里兜住异常,避免未处理的 Promise rejection console.log('[视频助手] 切换过程异常:', e && e.message ? e.message : e); clicked = false; } finally { isSwitching = false; } if (clicked) { // 是否真的切成功由 clickVideoItem 的 3s 标题校验判定;此处不能清零失败计数 return true; } // 4.6.0 失败处理:复位完成标志让主循环可重试;失败不计入熔断,改为连续失败计数 videoFinished = false; switchAttempts = Math.max(0, switchAttempts - 1); consecutiveFailures++; if (consecutiveFailures > 3) { isRunning = false; notify('自动运行已停止(连续切换失败 3 次)'); return false; } notify('切换失败,将自动重试(' + consecutiveFailures + '/3)'); return false; } // ========== 设置倍速 ========== function setSpeed() { const video = getVideo(); if (!video) return; if (video.paused) { ensurePlay(video); } video.playbackRate = CONFIG.targetSpeed; console.log('[视频助手] 倍速设置为:', CONFIG.targetSpeed); } // ========== 显示通知 ========== function notify(msg) { const old = document.getElementById('vh-notify'); if (old) old.remove(); const div = document.createElement('div'); div.id = 'vh-notify'; div.style.cssText = ` position: fixed; top: 20px; right: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 25px; border-radius: 8px; font-size: 14px; font-weight: bold; z-index: 999999; box-shadow: 0 4px 15px rgba(0,0,0,0.3); max-width: 300px; word-wrap: break-word; `; div.textContent = msg; document.body.appendChild(div); setTimeout(() => div.remove(), 3000); } // ========== 更新模式按钮样式 ========== function updateModeButtons() { const speedBtn = document.getElementById('vh-mode-speed'); const skipBtn = document.getElementById('vh-mode-skip'); const modeText = document.getElementById('vh-current-mode'); if (speedBtn && skipBtn) { if (currentMode === RUN_MODE.SPEED_2X) { speedBtn.style.background = '#4CAF50'; speedBtn.style.color = 'white'; skipBtn.style.background = '#555'; skipBtn.style.color = '#ccc'; } else { speedBtn.style.background = '#555'; speedBtn.style.color = '#ccc'; skipBtn.style.background = '#FF5722'; skipBtn.style.color = 'white'; } } if (modeText) { modeText.textContent = currentMode === RUN_MODE.SPEED_2X ? '二倍速' : '进度条跳过'; } } // ========== 切换模式 ========== function switchMode(mode) { currentMode = mode; updateModeButtons(); if (mode === RUN_MODE.SPEED_2X) { notify('已切换到:二倍速模式'); setSpeed(); } else { notify('已切换到:进度条跳过模式'); const video = getVideo(); if (video) video.playbackRate = 1.0; } } // ========== 创建控制面板 ========== function createPanel() { const panel = document.createElement('div'); panel.id = 'vh-panel'; panel.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: rgba(0,0,0,0.9); color: white; padding: 15px; border-radius: 10px; font-size: 12px; z-index: 999999; min-width: 260px; `; panel.innerHTML = `
📹 视频助手 v4.7.0
当前: -
倍速: 1.0x
状态: 等待中
当前模式: 二倍速
✓ 播放完毕切换   ✓ 自动二倍速   ✓ 智能跳过
`; document.body.appendChild(panel); const currentEl = document.getElementById('vh-current'); if (currentEl) { const title = getCurrentPageTitle(); currentEl.textContent = title ? title.substring(0, 20) + (title.length > 20 ? '...' : '') : '-'; } document.getElementById('vh-mode-speed').addEventListener('click', () => { switchMode(RUN_MODE.SPEED_2X); }); document.getElementById('vh-mode-skip').addEventListener('click', () => { switchMode(RUN_MODE.SKIP_TO_END); }); document.getElementById('vh-apply').addEventListener('click', () => { if (currentMode === RUN_MODE.SPEED_2X) { setSpeed(); notify('已设置二倍速'); } else { if (skipToEnd()) { setTimeout(() => { if (!hasSwitched) switchToNext(); }, 2000); } } }); document.getElementById('vh-next').addEventListener('click', () => { hasSwitched = false; isListOpen = false; isPageLoading = false; switchAttempts = 0; switchToNext(); }); document.getElementById('vh-autorun').addEventListener('change', (e) => { isRunning = e.target.checked; if (isRunning) { notify('自动运行已开启'); hasSwitched = false; isPageLoading = false; switchAttempts = 0; videoStartTime = Date.now(); const video = getVideo(); if (video) { if (video.paused) { ensurePlay(video); } if (currentMode === RUN_MODE.SPEED_2X) { setSpeed(); } } } else { notify('自动运行已暂停'); } }); setInterval(() => { const video = getVideo(); const statusEl = document.getElementById('vh-status'); const speedEl = document.getElementById('vh-speed'); const currentEl = document.getElementById('vh-current'); if (video && statusEl && speedEl) { const time = video.currentTime.toFixed(0); const duration = video.duration.toFixed(0); const paused = video.paused ? '暂停' : '播放中'; statusEl.textContent = `${paused} | ${time}/${duration}s`; speedEl.textContent = video.playbackRate.toFixed(1) + 'x'; } if (currentEl) { const title = getCurrentPageTitle(); currentEl.textContent = title ? title.substring(0, 20) + (title.length > 20 ? '...' : '') : '-'; } }, 1000); } // ========== 4.7.0:可靠起播(自动播放被拦 → 静音重试) ========== function ensurePlay(video) { if (!video || !video.paused) return; const p = video.play(); if (p && p.catch) { p.catch(() => { if (!mutedFallbackUsed) { mutedFallbackUsed = true; video.muted = true; console.log('[视频助手] 自动播放被拦,已静音重试'); try { ensurePlay(video); } catch (e) {} } }); } } // ========== 4.7.0:ended 事件作即时信号(与 500ms 轮询互为冗余) ========== function bindEndedSignal(video) { if (!video || video.__vhEndedBound) return; video.__vhEndedBound = true; video.addEventListener('ended', () => { console.log('[视频助手] 收到 ended 事件'); if (!isRunning || hasSwitched || isPageLoading || isSwitching) return; if (Date.now() < finishGraceUntil) return; videoFinished = false; // 交给主循环下一 tick 统一判定,避免重复切换 }); } // ========== 4.7.0:平台预警文本检测(命中即停机,保护账号) ========== const WARNING_KEYS = ['预警提示', '检测到异常', '异常学习', '请勿使用插件', '脚本已失效']; function checkPlatformWarning() { const t = (document.body && document.body.innerText) ? document.body.innerText : ''; for (let i = 0; i < WARNING_KEYS.length; i++) { if (t.indexOf(WARNING_KEYS[i]) >= 0) return WARNING_KEYS[i]; } return null; } // ========== 处理当前视频(核心逻辑)========== function processCurrentVideo() { const video = getVideo(); if (!video) { console.log('[视频助手] 未找到视频'); return; } // 根据模式执行操作 if (currentMode === RUN_MODE.SPEED_2X) { // 二倍速模式:确保倍速设置 if (video.playbackRate !== CONFIG.targetSpeed) { video.playbackRate = CONFIG.targetSpeed; console.log('[视频助手] 设置二倍速'); } } else { // 进度条跳过模式:拖动到结尾 const remaining = video.duration - video.currentTime; if (remaining > CONFIG.skipToEndThreshold * 2) { skipToEnd(); } } } // ========== 检查视频是否播放完毕 ========== function isVideoFinished(video) { if (!video || !video.duration || !isFinite(video.duration)) return false; // 1) 原生 ended 最可靠,不要求 paused(4.6.0) if (video.ended) return true; const remaining = video.duration - video.currentTime; const progress = video.currentTime / video.duration; // 2) 已到结尾(容差 0.5s):站点播完不 pause 也能判定 if (remaining <= 0.5) return true; // 3) 进度接近 100%(播放器对 duration 显示截断时兜底) if (progress >= 0.995) return true; // 4) 原「接近结尾 + 已暂停」条件保留为更严格的兜底 return (remaining <= CONFIG.skipToEndThreshold && video.paused) || (progress >= 0.95 && video.paused); } // ========== 主循环(每500ms执行一次)========== function startMainLoop() { if (mainCheckInterval) { clearInterval(mainCheckInterval); } let lastTitle = ''; lastHref = location.href; videoFinished = false; consecutiveFailures = 0; mainCheckInterval = setInterval(() => { const currentTitle = getCurrentPageTitle(); const currentHref = location.href; const urlChanged = currentHref !== lastHref && lastHref !== ''; // 检测页面变化(视频切换):标题变化或 URL 变化(4.7.0 起 URL 也算) if ((currentTitle !== lastTitle && currentTitle !== '') || urlChanged) { console.log('[视频助手] 检测到页面变化:', currentTitle, urlChanged ? '(URL)' : ''); lastTitle = currentTitle; lastHref = currentHref; videoStartTime = Date.now(); hasSwitched = false; isPageLoading = false; isListOpen = false; videoFinished = false; consecutiveFailures = 0; finishGraceUntil = Date.now() + 1500; // 4.6.0:新视频未就绪前不判播完,避免连跳 // 新视频加载后,立即执行一次模式操作 setTimeout(() => { processCurrentVideo(); }, 1000); } // 处理测评/空页面 const pageStatus = checkTestOrEmptyPage(); if (pageStatus.shouldSkip && isRunning && !hasSwitched && !isPageLoading) { console.log('[视频助手] 检测到需要跳过的页面'); notify('自动跳过当前页面'); switchToNext(); return; } // 4.7.0:绑定 ended 即时信号 + 周期性扫描平台预警 const anyVideo = getVideo(); if (anyVideo) bindEndedSignal(anyVideo); warningScanTick++; if (isRunning && warningScanTick % 10 === 0) { const hit = checkPlatformWarning(); if (hit) { isRunning = false; console.warn('[视频助手] 检测到平台提示:', hit); notify('检测到平台提示「' + hit + '」,已停止自动运行'); } } // 自动运行逻辑 if (isRunning && !hasSwitched && !isPageLoading) { const video = getVideo(); if (video && video.currentTime && video.duration) { // 持续确保倍速/跳过 processCurrentVideo(); // 检查视频是否播放完毕 if (Date.now() >= finishGraceUntil && isVideoFinished(video)) { if (!videoFinished) { console.log('[视频助手] 视频播放完毕,准备切换'); videoFinished = true; switchToNext(); } } else { // 视频未播放完毕,继续播放 videoFinished = false; // 暂停时恢复播放(但只在视频未结束时) if (video.paused) { const remaining = video.duration - video.currentTime; if (remaining > CONFIG.skipToEndThreshold) { ensurePlay(video); } } } } } }, CONFIG.checkInterval); } // ========== 启动 ========== function init() { console.log('[视频助手] 初始化...'); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); return; } createPanel(); updateModeButtons(); // 4.7.0:静音兜底后,用户首次交互即恢复声音(各只触发一次) const unmuteOnce = () => { if (!mutedFallbackUsed) return; const v = getVideo(); if (v) v.muted = false; }; document.addEventListener('click', unmuteOnce, { once: true }); document.addEventListener('keydown', unmuteOnce, { once: true }); // 启动主循环 startMainLoop(); notify('视频助手 v4.7.0 已启动!'); } init(); })();