// ==UserScript== // @name 趣卫教育视频自动播放 // @namespace http://tampermonkey.net/ // @version 12.1.0 // @description 仅供个人学习交流,使用风险自负 // @tag 趣卫课堂 学习辅助 // @author unyoah12345 // @match *://study.qwjiaoyu.com/play/course/* // @grant GM_xmlhttpRequest // @connect www.keyt.cn // @antifeature payment // @license All Rights Reserved // @run-at document-idle // ==/UserScript== (function () { 'use strict'; (function forceMutedAutoplay() { const proto = HTMLMediaElement.prototype; const origPlay = proto.play; if (typeof origPlay !== 'function') return; proto.play = function () { try { const noGesture = !(navigator.userActivation && navigator.userActivation.hasBeenActive); if (noGesture && this.muted !== true) this.muted = true; } catch (e) { } return origPlay.apply(this, arguments); }; })(); const CARD_API = 'https://www.keyt.cn/kami/nuyoah123/check.php'; const CARD_APP = 'quweiketang'; const CARD_CACHE = 86400 * 1000; const CARD_EXIT = ['expired', 'banned', 'device_mismatch', 'system_locked', 'online_limit_reached']; function cardLsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } } function cardLsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} } function cardHash(str) { let h = 5381; for (let i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) | 0; return 'web_' + (h >>> 0).toString(16); } function getDeviceId() { const saved = cardLsGet('qw_card_dev'); if (saved) return saved; const parts = []; try { parts.push('s:' + screen.width + 'x' + screen.height + 'x' + (screen.colorDepth || 24)); } catch (e) {} try { parts.push('l:' + (navigator.language || 'zh-CN')); } catch (e) {} try { parts.push('p:' + (navigator.platform || '')); } catch (e) {} const id = cardHash(parts.join('|')); cardLsSet('qw_card_dev', id); return id; } function cardCall(card, cb) { const url = CARD_API + '?card=' + encodeURIComponent(card) + '&mac=' + encodeURIComponent(getDeviceId()) + '&app=' + encodeURIComponent(CARD_APP); GM_xmlhttpRequest({ method: 'GET', url: url, onload: function (res) { const t = res.responseText || ''; const p = t.indexOf('|sign='); cb((p >= 0 ? t.substring(0, p) : t).split('|')); }, onerror: function () { cb(['error', 'network']); } }); } function cardCachedOk() { if (cardLsGet('qw_card_ok') !== 'true') return false; const t = parseInt(cardLsGet('qw_card_ok_time')) || 0; return (Date.now() - t) <= CARD_CACHE; } function cardOverlay() { return new Promise((resolve) => { const box = document.createElement('div'); box.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.92);z-index:2147483647;display:flex;align-items:center;justify-content:center;font-family:sans-serif;'; const inner = document.createElement('div'); inner.style.cssText = 'background:#1a202c;padding:36px;border-radius:18px;width:380px;max-width:90%;text-align:center;color:#fff;box-shadow:0 20px 60px rgba(0,0,0,0.5);'; inner.innerHTML = '
🔐
' + '

请输入卡密

' + '

验证通过后脚本自动运行

' + '
' + '' + '' + '

卡密问题联系微信:quweijiaoyu123456

'; box.appendChild(inner); document.body.appendChild(box); const inp = inner.querySelector('#qwCardInp'); const btn = inner.querySelector('#qwCardBtn'); const err = inner.querySelector('#qwCardErr'); const ERR = { missing_params: '参数不完整', app_not_found: '应用不存在', invalid_card: '卡密无效,请检查', expired: '卡密已过期', banned: '卡密已被禁用', device_mismatch: '设备不匹配', online_limit_reached: '在线设备数已满', too_frequent: '请求过于频繁,请稍后再试', system_locked: '系统已锁死,请联系管理员', network: '网络错误,请稍后重试' }; function doVerify() { const card = inp.value.trim(); if (!card) { err.textContent = '请输入卡密'; return; } err.textContent = ''; btn.disabled = true; btn.textContent = '⏳ 验证中...'; cardCall(card, (parts) => { if (parts[0] === 'ok') { cardLsSet('qw_card_ok', 'true'); cardLsSet('qw_card_ok_time', String(Date.now())); cardLsSet('qw_card', card); box.remove(); resolve(true); } else { err.textContent = ERR[parts[1] || ''] || '验证失败'; btn.disabled = false; btn.textContent = '✅ 验证卡密'; } }); } btn.addEventListener('click', doVerify); inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') doVerify(); }); inp.focus(); }); } function startCardHeartbeat() { function beat() { const card = cardLsGet('qw_card'); if (!card) return; cardCall(card, (parts) => { if (parts[0] === 'error' && parts[1] !== 'too_frequent') { cardLsSet('qw_card_ok', 'false'); alert('卡密已失效,请重新输入。问题联系微信:quweijiaoyu123456'); location.reload(); } }); } setTimeout(() => { beat(); setInterval(beat, 120000); }, 60000); } async function ensureCardReady() { if (cardCachedOk()) { startCardHeartbeat(); return true; } const saved = cardLsGet('qw_card'); if (saved) { const ok = await new Promise((res) => { cardCall(saved, (parts) => { if (parts[0] === 'ok') { cardLsSet('qw_card_ok', 'true'); cardLsSet('qw_card_ok_time', String(Date.now())); res(true); } else { cardLsSet('qw_card_ok', 'false'); res(false); } }); }); if (ok) { startCardHeartbeat(); return true; } } await cardOverlay(); startCardHeartbeat(); return true; } const CONFIG = { tree: { container: '.n-tree, .chapter-container', allNodeWrappers: '.n-tree-node-wrapper', chapterNode: '.n-tree-node:not(.n-tree-node--leaf):has(.n-tree-node-switcher)', videoNode: '.n-tree-node:has(.n-tree-node-indent--is-leaf):not(:has(.n-tree-node-switcher))', chapterSwitcher: '.n-tree-node-switcher', nodeText: '.n-tree-node-content__text', nodeContent: '.n-tree-node-content', }, status: { selectors: [ '.n-tree-node-content__suffix .n-button__content', '.n-tree-node-content__suffix span', '.n-tree-node-content__suffix', ], completedText: '已学完', learningText: '学习中', }, player: { videoSelector: 'video', playerContainer: '.art-video-player', progressBar: '.art-progress-played', nativeSpeedSync: false, nativeSpeedSelector: '.art-setting-item-right-tooltip', }, playback: { defaultSpeed: 2.0, defaultMuted: true, speedOptions: [0.5, 0.8, 1.0, 1.3, 1.5, 2.0], maxSpeed: 2.0, speedStorageKey: 'qwVideoAuto_speed', muteStorageKey: 'qwVideoAuto_muted', stateApplyRetry: 3, stateApplyInterval: 700, stateLockDuration: 4600, }, flow: { maxWaitTime: 15000, initDelay: 1500, maxTraverseAttempts: 6, traversalInterval: 1200, expandRetry: 2, expandWait: 1200, switchClickDelay: 600, applyStateDelay: 800, checkInterval: 3000, syncInterval: 1000, urlCheckInterval: 5000, openChapterTabText: '章节', openTabWait: 1500, autoStartMaxRetries: 3, }, }; const state = { chapterList: [], selectedChapterIds: new Set(), mode: 'idle', currentPlayingChapterIndex: 0, currentPlayingVideoId: null, syncLocked: false, switchPending: false, monitorTimer: null, syncTimer: null, urlTimer: null, mutationObserver: null, lastVideoElement: null, lastUrl: location.href, dropdownCloseBound: false, treeClickBound: false, }; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); function log(...args) { console.log(`[视频自动播放 ${new Date().toLocaleTimeString()}]`, ...args); } function error(...args) { console.error(`[视频自动播放 ${new Date().toLocaleTimeString()}] 错误:`, ...args); } function waitForElement(selector, timeout = CONFIG.flow.maxWaitTime) { return new Promise((resolve) => { const start = Date.now(); const timer = setInterval(() => { const el = document.querySelector(selector); if (el) { clearInterval(timer); resolve(el); return; } if (Date.now() - start >= timeout) { clearInterval(timer); resolve(null); } }, 400); }); } function supportsHasSelector() { try { document.querySelector(':has(div)'); return true; } catch (e) { return false; } } function showToast(message, type = 'info') { const panel = document.getElementById('videoAutoPlayPanel'); if (!panel) return; let toast = document.getElementById('autoplayToast'); if (!toast) { toast = document.createElement('div'); toast.id = 'autoplayToast'; toast.style.cssText = 'margin-top:8px;padding:8px 10px;border-radius:4px;font-size:12px;line-height:1.5;word-break:break-all;color:#333;background:#f6f8fa;border:1px solid #e1e4e8;max-height:90px;overflow-y:auto;'; panel.appendChild(toast); } const colors = { info: '#0366d6', success: '#1a7f37', warn: '#9a6700', error: '#cf222e' }; toast.style.borderLeft = `3px solid ${colors[type] || colors.info}`; toast.textContent = message; clearTimeout(toast._hideTimer); toast._hideTimer = setTimeout(() => { toast.textContent = ''; }, 6000); } function getVideoElement() { const container = document.querySelector(CONFIG.player.playerContainer); if (container) { const v = container.querySelector(CONFIG.player.videoSelector); if (v) return v; } const all = document.querySelectorAll(CONFIG.player.videoSelector); return all.length === 1 ? all[0] : null; } function savePlaybackState(speed, muted) { localStorage.setItem(CONFIG.playback.speedStorageKey, speed); localStorage.setItem(CONFIG.playback.muteStorageKey, muted); log('保存播放状态:', `${speed}x`, muted ? '静音' : '正常'); } function loadPlaybackState() { const savedMuted = localStorage.getItem(CONFIG.playback.muteStorageKey); return { speed: parseFloat(localStorage.getItem(CONFIG.playback.speedStorageKey)) || CONFIG.playback.defaultSpeed, muted: savedMuted === null ? CONFIG.playback.defaultMuted : savedMuted === 'true', }; } function setPlaybackSpeed(speed) { const video = getVideoElement(); if (!video) return false; const target = Math.min(speed, CONFIG.playback.maxSpeed); if (Math.abs(video.playbackRate - target) < 0.01) return true; video.playbackRate = target; if (Math.abs(video.playbackRate - target) < 0.01) { log('倍速设置成功:', `${target}x`); return true; } log('倍速设置失败,当前实际:', `${video.playbackRate}x`); return false; } function setMutedState(muted) { const video = getVideoElement(); if (!video) return false; if (video.muted === muted) return true; video.muted = muted; if (video.muted === muted) { log(muted ? '静音成功' : '取消静音成功'); return true; } return false; } function updatePlaybackControlsUI(speed, muted) { const sel = document.getElementById('speedSelector'); const btn = document.getElementById('muteButton'); if (sel && parseFloat(sel.value) !== speed) sel.value = speed; if (btn) btn.textContent = muted ? '取消静音' : '静音'; } function parseRateFromText(text) { const m = String(text).match(/(\d+(?:\.\d+)?)/); return m ? parseFloat(m[1]) : NaN; } function syncSpeedToNative(speed) { if (!CONFIG.player.nativeSpeedSync) return; const control = document.querySelector(CONFIG.player.nativeSpeedSelector); if (!control) { log('未找到原生倍速控件(选择器待核对):', CONFIG.player.nativeSpeedSelector); return; } let target = null, best = Infinity; control.querySelectorAll('div, span, option, li').forEach(el => { const rate = parseRateFromText(el.textContent); if (!isNaN(rate)) { const diff = Math.abs(rate - speed); if (diff < best) { best = diff; target = el; } } }); if (target && best < 0.05) { target.click(); log('已同步原生倍速:', `${speed}x`); } else log('未找到匹配的原生倍速选项:', `${speed}x`); } function syncFromNativeControls() { if (state.syncLocked || state.switchPending) return; const video = getVideoElement(); if (!video) return; const saved = loadPlaybackState(); const speedDrift = Math.abs(video.playbackRate - saved.speed) > 0.1; const muteDiff = video.muted !== saved.muted; if (speedDrift || muteDiff) { savePlaybackState(video.playbackRate, video.muted); updatePlaybackControlsUI(video.playbackRate, video.muted); } } function applySavedStateForVideoSwitch(savedState) { state.syncLocked = true; log('已锁定状态同步,防止新视频重置倍速/静音'); let retries = 0; const retryTimer = setInterval(() => { setPlaybackSpeed(savedState.speed); setMutedState(savedState.muted); syncSpeedToNative(savedState.speed); retries++; if (retries >= CONFIG.playback.stateApplyRetry) { clearInterval(retryTimer); setTimeout(() => { setPlaybackSpeed(savedState.speed); setMutedState(savedState.muted); syncSpeedToNative(savedState.speed); tryPlayVideo(); log('最终强制应用状态:', `${savedState.speed}x`, savedState.muted ? '静音' : '正常'); state.syncLocked = false; state.switchPending = false; log('同步锁定已解除'); }, 1000); } }, CONFIG.playback.stateApplyInterval); setTimeout(() => { if (state.syncLocked || state.switchPending) { state.syncLocked = false; state.switchPending = false; log('超时自动解除同步锁定'); } }, CONFIG.playback.stateLockDuration); } function readStatusFromNode(node) { for (const sel of CONFIG.status.selectors) { const el = node.querySelector(sel); if (el) { const text = el.textContent.trim(); if (text === CONFIG.status.completedText) return { status: 'completed', text }; if (text === CONFIG.status.learningText) return { status: 'learning', text }; return { status: 'unlearned', text: text || '' }; } } return { status: 'unknown', text: '' }; } function traverseChaptersAndVideos() { const wrappers = document.querySelectorAll(CONFIG.tree.allNodeWrappers); const chapters = []; let current = null; let missingStatus = 0; wrappers.forEach(wrapper => { const chapterNode = wrapper.querySelector(CONFIG.tree.chapterNode); const videoNode = wrapper.querySelector(CONFIG.tree.videoNode); if (chapterNode) { const titleEl = chapterNode.querySelector(CONFIG.tree.nodeText); current = { id: `chapter_${chapters.length}`, title: titleEl ? titleEl.textContent.trim() : `章节${chapters.length + 1}`, node: chapterNode, wrapper, videos: [], }; chapters.push(current); } else if (videoNode && current) { const titleEl = videoNode.querySelector(CONFIG.tree.nodeText); const content = videoNode.querySelector(CONFIG.tree.nodeContent) || videoNode; const { status, text } = readStatusFromNode(videoNode); if (status === 'unknown' && !videoNode.querySelector(CONFIG.status.selectors[0])) missingStatus++; current.videos.push({ id: `video_${current.id}_${current.videos.length}`, title: titleEl ? titleEl.textContent.trim() : `视频${current.videos.length + 1}`, node: videoNode, wrapper, content, status, statusText: text, chapterId: current.id, }); } }); if (missingStatus > 0) { log(`警告:${missingStatus} 个视频节点未找到状态元素,已学视频可能被重播(见验证清单第 2 项)`); } return chapters; } function refreshTreeRefs() { const fresh = traverseChaptersAndVideos(); if (fresh.length === 0) return; state.chapterList = fresh; if (state.currentPlayingVideoId && !state.chapterList.some(c => c.videos.some(v => v.id === state.currentPlayingVideoId))) { state.currentPlayingVideoId = null; } } function detectCurrentPlayingVideo() { for (const ch of state.chapterList) { for (const v of ch.videos) { if (v.node.classList.contains('n-tree-node--selected')) { state.currentPlayingVideoId = v.id; log('检测到当前正在播放的视频:', v.title); return; } } } } function getSelectedChapters() { return state.chapterList.filter(c => state.selectedChapterIds.has(c.id)); } function findVideoById(id) { for (const ch of state.chapterList) { for (const v of ch.videos) { if (v.id === id) return v; } } return null; } function findChapterIndexInSelection(chapterId) { const idx = getSelectedChapters().findIndex(c => c.id === chapterId); return idx >= 0 ? idx : state.currentPlayingChapterIndex; } function isVideoCompleted() { const video = getVideoElement(); if (video && !isNaN(video.duration) && video.duration > 0) { if (video.ended) { log('当前视频播放完毕(时长 ', `${video.duration.toFixed(1)}s)`); return true; } if (video.currentTime >= video.duration - 0.3) { log('当前视频播放完毕(接近结尾 ', `${video.currentTime.toFixed(1)}/${video.duration.toFixed(1)}s)`); return true; } } const bar = document.querySelector(CONFIG.player.progressBar); if (bar) { const pct = parseFloat(bar.style.width || '0%'); if (pct >= 99.9) { log('当前视频播放完毕(进度 ', `${pct.toFixed(1)}%)`); return true; } } return false; } function findGlobalNextVideo() { const all = []; state.chapterList.forEach(ch => ch.videos.forEach(v => all.push(v))); const learning = all.find(v => v.status === 'learning'); if (learning && learning.id !== state.currentPlayingVideoId) return learning; if (state.currentPlayingVideoId) { const idx = all.findIndex(v => v.id === state.currentPlayingVideoId); if (idx >= 0) { for (let i = idx + 1; i < all.length; i++) { if (all[i].status !== 'completed') return all[i]; } } } for (const v of all) { if (v.status !== 'completed') return v; } return null; } function findNextVideoInCurrentPlayingChapter() { const selected = getSelectedChapters(); if (selected.length === 0) return null; if (state.currentPlayingChapterIndex >= selected.length) return null; const chapter = selected[state.currentPlayingChapterIndex]; const currentIdx = chapter.videos.findIndex(v => v.id === state.currentPlayingVideoId); const start = currentIdx >= 0 ? currentIdx + 1 : 0; for (let i = start; i < chapter.videos.length; i++) { if (chapter.videos[i].status !== 'completed') return chapter.videos[i]; } return playNextSelectedChapter(); } function playNextSelectedChapter() { const selected = getSelectedChapters(); if (selected.length === 0) return null; state.currentPlayingChapterIndex++; if (state.currentPlayingChapterIndex >= selected.length) { state.mode = 'idle'; showToast(`选中的 ${selected.length} 个章节播放完成`, 'success'); log('选中章节全部播放完成,已停止自动连播(不再续播未选章节)'); return null; } const chapter = selected[state.currentPlayingChapterIndex]; return playChapterFirstUnlearnedVideo(chapter); } function playChapterFirstUnlearnedVideo(chapter) { const first = chapter.videos.find(v => v.status !== 'completed'); if (first) { log(`开始播放章节「${chapter.title}」的未学视频:`, first.title); switchToVideo(first); return null; } log(`章节「${chapter.title}」已学完,跳过`); return playNextSelectedChapter(); } function switchToVideo(video) { refreshTreeRefs(); const fresh = findVideoById(video.id); if (!fresh || !fresh.content) { error('切换视频失败:找不到节点(树结构可能变化)', video && video.id); return false; } try { fresh.wrapper.scrollIntoView({ behavior: 'smooth', block: 'center' }); const saved = loadPlaybackState(); setTimeout(() => { let node = fresh.content; if (!document.contains(node)) { log('切换前检测到节点已更新,重新定位...'); refreshTreeRefs(); const again = findVideoById(fresh.id); node = again && again.content; if (!node || !document.contains(node)) { error('重试后仍无法定位视频节点,请刷新页面'); return; } } selfInitiatedClick = true; node.click(); selfInitiatedClick = false; state.currentPlayingVideoId = fresh.id; state.currentPlayingChapterIndex = findChapterIndexInSelection(fresh.chapterId); state.syncLocked = true; state.switchPending = true; log('已切换视频:', fresh.title); recoverPlaybackAfterSwitch(); setTimeout(() => applySavedStateForVideoSwitch(saved), CONFIG.flow.applyStateDelay); }, CONFIG.flow.switchClickDelay); return true; } catch (err) { error('切换视频时出错', err); return false; } } function checkAndAdvance() { if (state.mode === 'idle') return; if (!isVideoCompleted()) return; let next = null; if (state.mode === 'selected') { next = findNextVideoInCurrentPlayingChapter(); } else { next = findGlobalNextVideo(); } if (next) { switchToVideo(next); } else if (state.mode === 'global') { stopVideoMonitoring(); showToast('所有未学视频已播放完毕', 'success'); } } function startVideoMonitoring() { if (state.monitorTimer) clearInterval(state.monitorTimer); state.monitorTimer = setInterval(() => { attachVideoListeners(); refreshTreeRefs(); updateCountInfo(); autoStartIfNoPlayer(); keepVideoPlaying(); checkAndAdvance(); }, CONFIG.flow.checkInterval); log('视频监控已启动(每', `${CONFIG.flow.checkInterval}ms 检查一次)`); } function stopVideoMonitoring() { if (state.monitorTimer) { clearInterval(state.monitorTimer); state.monitorTimer = null; } log('视频监控已停止'); } function startSyncTimer() { if (state.syncTimer) clearInterval(state.syncTimer); state.syncTimer = setInterval(syncFromNativeControls, CONFIG.flow.syncInterval); log('状态同步定时器已启动(每', `${CONFIG.flow.syncInterval}ms 一次)`); } function attachVideoListeners() { const video = getVideoElement(); if (!video || video === state.lastVideoElement) return; if (state.lastVideoElement) { state.lastVideoElement.removeEventListener('ended', onVideoEnded); state.lastVideoElement.removeEventListener('ratechange', onRateChange); state.lastVideoElement.removeEventListener('volumechange', onVolumeChange); } state.lastVideoElement = video; video.addEventListener('ended', onVideoEnded); video.addEventListener('ratechange', onRateChange); video.addEventListener('volumechange', onVolumeChange); log('已绑定视频事件监听'); } function onVideoEnded() { setTimeout(() => { refreshTreeRefs(); checkAndAdvance(); }, 2500); } function keepVideoPlaying() { const video = getVideoElement(); if (!video) return; if (state.syncLocked || state.switchPending) return; if (video.paused && !video.ended && !isNaN(video.duration) && video.currentTime < video.duration - 0.3) { const p = video.play(); if (p && p.catch) p.catch(() => {}); } } function onRateChange() { if (state.syncLocked) { const saved = loadPlaybackState(); setPlaybackSpeed(saved.speed); return; } syncFromNativeControls(); } function onVolumeChange() { if (state.syncLocked) { const saved = loadPlaybackState(); setMutedState(saved.muted); return; } syncFromNativeControls(); } let autoStartRetries = 0; let selfInitiatedClick = false; function isTreeVisible() { const el = document.querySelector(CONFIG.tree.container); return !!el && el.offsetHeight > 0 && el.offsetWidth > 0; } function openChapterPanel() { const text = CONFIG.flow.openChapterTabText; const candidates = [...document.querySelectorAll('p, div, span, li, button, a')]; const label = candidates.find(el => el.childElementCount === 0 && el.textContent.trim() === text); const target = (label && label.closest('div[class*="cursor-pointer"], [role="button"], button')) || label; if (target) { target.click(); log('已自动点击右侧"', text, '"标签'); return true; } log('未找到右侧"', text, '"标签,请手动点开'); return false; } function autoStartIfNoPlayer() { const playerContainer = document.querySelector(CONFIG.player.playerContainer); const video = getVideoElement(); if (playerContainer && video) { autoStartRetries = 0; return false; } if (autoStartRetries >= CONFIG.flow.autoStartMaxRetries) return false; const next = findGlobalNextVideo(); if (!next) return false; autoStartRetries++; log('播放器未就绪,自动点播下一个未学视频(第', autoStartRetries, '次):', next.title); switchToVideo(next); return true; } function tryPlayVideo() { const video = getVideoElement(); if (!video || !video.paused) return; const p = video.play(); if (p && typeof p.catch === 'function') { p.catch(() => { showToast('浏览器拦截了自动播放,请点击页面任意位置开始', 'warn'); log('自动播放被浏览器拦截,等待用户交互'); }); } } function bindAutoplayUnblock() { const handler = () => { const video = getVideoElement(); if (video && video.paused) { video.play().then(() => log('用户交互后继续播放')).catch(() => {}); } }; document.addEventListener('click', handler, { once: true }); document.addEventListener('keydown', handler, { once: true }); } function recoverPlaybackAfterSwitch() { const saved = loadPlaybackState(); let tries = 0; const timer = setInterval(() => { tries++; const video = getVideoElement(); if (video) { if (video.muted !== saved.muted) video.muted = saved.muted; if (video.paused) { const p = video.play(); if (p && p.catch) p.catch(() => log('恢复播放被浏览器拦截(第', tries, '次)')); } else { log('视频已开始播放(第', tries, '次确认)'); clearInterval(timer); return; } } if (tries >= 15) { clearInterval(timer); showToast('自动播放未成功,请点一下页面任意位置', 'warn'); } }, 400); } function renderCheckboxDropdown() { const container = document.getElementById('chapterCheckboxDropdown'); if (!container) return; container.style.position = 'relative'; container.innerHTML = ''; const inputWrapper = document.createElement('div'); inputWrapper.id = 'dropdownInputWrapper'; inputWrapper.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border:1px solid #ccc;border-radius:4px;background:#fff;cursor:pointer;box-sizing:border-box;'; const display = document.createElement('div'); display.id = 'selectedChaptersDisplay'; display.style.cssText = 'flex:1;text-align:left;font-size:13px;color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'; display.textContent = '请选择章节(点击展开多选)'; const arrow = document.createElement('span'); arrow.id = 'dropdownToggleBtn'; arrow.textContent = '▼'; arrow.style.cssText = 'width:20px;height:20px;display:flex;align-items:center;justify-content:center;color:#666;transition:transform .2s;font-size:12px;'; inputWrapper.append(display, arrow); const panel = document.createElement('div'); panel.id = 'dropdownPanel'; panel.style.cssText = 'position:absolute;top:100%;left:0;right:0;z-index:100000;margin-top:4px;border:1px solid #ccc;border-radius:4px;background:#fff;max-height:250px;overflow-y:auto;box-shadow:0 2px 10px rgba(0,0,0,.1);box-sizing:border-box;display:none;'; if (state.chapterList.length === 0) { const empty = document.createElement('div'); empty.textContent = '无可用章节'; empty.style.cssText = 'padding:10px;text-align:center;color:#999;font-size:13px;'; panel.appendChild(empty); } else { state.chapterList.forEach(chapter => { const hasVideo = chapter.videos.length > 0; const item = document.createElement('div'); item.style.cssText = `padding:8px 12px;display:flex;align-items:center;cursor:${hasVideo ? 'pointer' : 'not-allowed'};transition:background .2s;`; if (!hasVideo) item.style.opacity = '0.5'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.id = `chapter_cb_${chapter.id}`; cb.value = chapter.id; cb.checked = state.selectedChapterIds.has(chapter.id); cb.disabled = !hasVideo; cb.style.cssText = 'margin-right:8px;cursor:pointer;width:14px;height:14px;'; cb.addEventListener('change', () => handleCheckboxChange(chapter.id, cb.checked)); const label = document.createElement('label'); label.htmlFor = cb.id; label.style.cssText = `flex:1;text-align:left;font-size:13px;color:#333;cursor:${hasVideo ? 'pointer' : 'not-allowed'};`; const titleSpan = document.createElement('span'); titleSpan.textContent = chapter.title; const metaSpan = document.createElement('span'); metaSpan.style.cssText = 'color:#999;font-size:12px;'; const done = chapter.videos.filter(v => v.status === 'completed').length; metaSpan.textContent = `(共${chapter.videos.length}个视频,已学${done}个)`; label.append(titleSpan, metaSpan); if (!hasVideo) { const warn = document.createElement('span'); warn.style.cssText = 'color:#ff6666;font-size:12px;'; warn.textContent = '[无视频]'; label.appendChild(warn); } item.append(cb, label); panel.appendChild(item); }); } container.append(inputWrapper, panel); inputWrapper.addEventListener('click', (e) => { e.stopPropagation(); toggleDropdown(); }); panel.addEventListener('click', (e) => e.stopPropagation()); if (!state.dropdownCloseBound) { document.addEventListener('click', closeDropdown); state.dropdownCloseBound = true; } log('下拉框渲染完成'); } function toggleDropdown() { const panel = document.getElementById('dropdownPanel'); const btn = document.getElementById('dropdownToggleBtn'); if (!panel || !btn) return; const isOpen = panel.style.display === 'block'; panel.style.display = isOpen ? 'none' : 'block'; btn.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(180deg)'; } function closeDropdown() { const panel = document.getElementById('dropdownPanel'); const btn = document.getElementById('dropdownToggleBtn'); if (!panel || !btn) return; panel.style.display = 'none'; btn.style.transform = 'rotate(0deg)'; } function handleCheckboxChange(chapterId, checked) { if (checked) state.selectedChapterIds.add(chapterId); else state.selectedChapterIds.delete(chapterId); updateSelectedDisplay(); } function updateSelectedDisplay() { const display = document.getElementById('selectedChaptersDisplay'); if (!display) return; const selected = getSelectedChapters(); if (selected.length === 0) { display.textContent = '请选择章节(点击展开多选)'; return; } const names = selected.map(c => c.title); display.textContent = names.length <= 3 ? `已选:${names.join('、')}` : `已选:${names.slice(0, 3).join('、')}...(共${names.length}个)`; } function startPlaySelectedChapters() { refreshTreeRefs(); const selected = getSelectedChapters(); if (selected.length === 0) { showToast('请先选择要播放的章节(点击下拉框展开多选)', 'warn'); return; } state.mode = 'selected'; state.currentPlayingChapterIndex = 0; startVideoMonitoring(); startSyncTimer(); playChapterFirstUnlearnedVideo(selected[0]); } function manualSwitchNext() { refreshTreeRefs(); const selected = getSelectedChapters(); let next = null; if (selected.length > 0) { state.mode = 'selected'; next = findNextVideoInCurrentPlayingChapter(); } else { state.mode = 'global'; next = findGlobalNextVideo(); } if (next) switchToVideo(next); else showToast('没有更多可播放的视频了', 'warn'); } function restartAll() { refreshTreeRefs(); updateCountInfo(); renderCheckboxDropdown(); startVideoMonitoring(); startSyncTimer(); const selected = getSelectedChapters(); state.mode = selected.length > 0 ? 'selected' : 'global'; showToast('已重启监控与状态同步', 'success'); } function addControlPanel() { if (document.getElementById('videoAutoPlayPanel')) return; const saved = loadPlaybackState(); const panel = document.createElement('div'); panel.id = 'videoAutoPlayPanel'; panel.style.cssText = 'position:fixed;top:20px;right:20px;z-index:9999;background:#fff;padding:15px 12px;border:1px solid #ddd;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:Arial,sans-serif;font-size:14px;width:280px;cursor:default;box-sizing:border-box;'; const speedOptionsHtml = CONFIG.playback.speedOptions.map(s => `` ).join(''); panel.innerHTML = `
视频控制中心 按住拖动
已识别:0个章节 | 0个视频
`; document.body.appendChild(panel); panel.querySelector('#speedSelector').addEventListener('change', (e) => { const speed = parseFloat(e.target.value); if (setPlaybackSpeed(speed)) { const video = getVideoElement(); savePlaybackState(speed, video ? video.muted : false); syncSpeedToNative(speed); } }); panel.querySelector('#muteButton').addEventListener('click', () => { const video = getVideoElement(); if (!video) { showToast('未找到视频元素', 'warn'); return; } const newMuted = !video.muted; if (setMutedState(newMuted)) { savePlaybackState(video.playbackRate, newMuted); panel.querySelector('#muteButton').textContent = newMuted ? '取消静音' : '静音'; } }); panel.querySelector('#switchNextBtn').addEventListener('click', manualSwitchNext); panel.querySelector('#restartMonitorBtn').addEventListener('click', restartAll); panel.querySelector('#startChapterPlayBtn').addEventListener('click', startPlaySelectedChapters); initPanelDrag(panel); log('控制面板创建完成'); } function initPanelDrag(panel) { const handle = panel.querySelector('#panelDragHandle'); if (!handle) return; let dragging = false, ox = 0, oy = 0; handle.addEventListener('mousedown', (e) => { dragging = true; ox = e.clientX - panel.getBoundingClientRect().left; oy = e.clientY - panel.getBoundingClientRect().top; panel.style.cursor = 'grabbing'; handle.style.cursor = 'grabbing'; e.stopPropagation(); }); document.addEventListener('mousemove', (e) => { if (!dragging) return; const left = Math.max(0, Math.min(e.clientX - ox, window.innerWidth - panel.offsetWidth)); const top = Math.max(0, Math.min(e.clientY - oy, window.innerHeight - panel.offsetHeight)); panel.style.left = left + 'px'; panel.style.top = top + 'px'; }); document.addEventListener('mouseup', () => { if (dragging) { dragging = false; panel.style.cursor = 'default'; handle.style.cursor = 'grab'; } }); handle.addEventListener('selectstart', (e) => e.preventDefault()); } function bindTreeClickTracking() { const container = document.querySelector(CONFIG.tree.container); if (!container || state.treeClickBound) return; state.treeClickBound = true; container.addEventListener('click', (e) => { if (selfInitiatedClick) return; const row = e.target && e.target.closest ? e.target.closest('.n-tree-node-wrapper') : null; if (!row) return; const videoNode = row.querySelector(CONFIG.tree.videoNode); if (!videoNode) return; const textEl = videoNode.querySelector(CONFIG.tree.nodeText); const title = textEl ? textEl.textContent.trim() : ''; for (const ch of state.chapterList) { const v = ch.videos.find(x => x.title === title); if (v) { state.currentPlayingVideoId = v.id; state.currentPlayingChapterIndex = findChapterIndexInSelection(v.chapterId); log('检测到手动点击视频:', title); return; } } }, true); } function updateCountInfo() { const el = document.getElementById('videoCountInfo'); if (!el) return; const total = state.chapterList.reduce((s, c) => s + c.videos.length, 0); const done = state.chapterList.reduce((s, c) => s + c.videos.filter(v => v.status === 'completed').length, 0); el.textContent = `已识别:${state.chapterList.length}个章节 | ${total}个视频(已学${done}个)`; } async function expandAllChapters() { log('开始展开章节...'); for (let attempt = 0; attempt < CONFIG.flow.expandRetry; attempt++) { const switchers = document.querySelectorAll(CONFIG.tree.chapterSwitcher); let clicked = 0; switchers.forEach(btn => { if (btn instanceof HTMLElement && !btn.classList.contains('n-tree-node-switcher--expanded')) { btn.click(); clicked++; } }); if (clicked === 0) break; log('本轮展开章节按钮:', clicked); await sleep(CONFIG.flow.expandWait); } } async function initChapterAndVideoList() { let lastCount = -1; let stableRounds = 0; for (let i = 0; i < CONFIG.flow.maxTraverseAttempts; i++) { const chapters = traverseChaptersAndVideos(); const hasVideo = chapters.some(c => c.videos.length > 0); if (chapters.length > 0 && chapters.length === lastCount) { stableRounds++; if (hasVideo || stableRounds >= 2) { state.chapterList = chapters; log('遍历收敛:', chapters.length, '个章节'); renderCheckboxDropdown(); return; } } else { stableRounds = 0; } lastCount = chapters.length; await sleep(CONFIG.flow.traversalInterval); } state.chapterList = traverseChaptersAndVideos(); renderCheckboxDropdown(); log('达到最大遍历次数,使用当前结果:', state.chapterList.length, '个章节'); } function startMutationWatch() { const container = document.querySelector(CONFIG.tree.container); if (!container || state.mutationObserver) return; let timer = null; state.mutationObserver = new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(() => { refreshTreeRefs(); updateCountInfo(); }, 500); }); state.mutationObserver.observe(container, { childList: true, subtree: true }); log('树结构变更监听已启动'); } function startUrlWatcher() { if (state.urlTimer) clearInterval(state.urlTimer); state.urlTimer = setInterval(() => { if (location.href !== state.lastUrl) { state.lastUrl = location.href; log('检测到页面跳转,重新初始化...'); reinitScript(); } }, CONFIG.flow.urlCheckInterval); } function reinitScript() { stopVideoMonitoring(); if (state.syncTimer) { clearInterval(state.syncTimer); state.syncTimer = null; } state.selectedChapterIds.clear(); state.currentPlayingVideoId = null; state.mode = 'idle'; initScript(); } async function initScript() { try { if (!supportsHasSelector()) { showToast('当前浏览器不支持 :has() 选择器,请使用 Chrome/Edge 105+ 或 Firefox 121+', 'error'); return; } let container = isTreeVisible() ? document.querySelector(CONFIG.tree.container) : null; if (!container) { log('课程目录未展开,自动点开右侧"章节"标签...'); openChapterPanel(); await sleep(CONFIG.flow.openTabWait); container = await waitForElement(CONFIG.tree.container, CONFIG.flow.maxWaitTime); } if (!container) { error('未找到课程目录容器:', CONFIG.tree.container); showToast('未找到课程目录,请手动点开右侧"章节"后刷新', 'error'); return; } addControlPanel(); bindTreeClickTracking(); bindAutoplayUnblock(); await expandAllChapters(); await initChapterAndVideoList(); if (state.chapterList.length === 0) { showToast('未识别到章节/视频,请刷新页面重试', 'error'); return; } updateCountInfo(); detectCurrentPlayingVideo(); state.mode = 'global'; startVideoMonitoring(); startSyncTimer(); startUrlWatcher(); startMutationWatch(); autoStartIfNoPlayer(); log('初始化成功:', state.chapterList.length, '个章节'); } catch (err) { error('初始化失败', err); showToast('初始化出错,请查看控制台(F12→Console)', 'error'); } } window.addEventListener('load', async () => { log('页面加载完成,等待卡密验证...'); await ensureCardReady(); log('卡密已通过,', `${CONFIG.flow.initDelay}ms 后开始初始化...`); setTimeout(initScript, CONFIG.flow.initDelay); }); window.addEventListener('beforeunload', () => { if (state.monitorTimer) clearInterval(state.monitorTimer); if (state.syncTimer) clearInterval(state.syncTimer); if (state.urlTimer) clearInterval(state.urlTimer); if (state.mutationObserver) state.mutationObserver.disconnect(); if (state.lastVideoElement) { state.lastVideoElement.removeEventListener('ended', onVideoEnded); state.lastVideoElement.removeEventListener('ratechange', onRateChange); state.lastVideoElement.removeEventListener('volumechange', onVolumeChange); } }); })();