// ==UserScript== // @name 云南开放大学-暴力自动阅读小小站 // @namespace http://scriptcat.org/ // @version 2.7.0 // @description 支持视频自动播放 + PDF简单自动停留阅读等,验证平台是否“只需打开停留即可完成”,可支持倍速播放。过程中可能会有作业+考试+章节+期末考试+下线考试等资料都可以反馈小小站1077472126 // @match https://teach.ynou.edu.cn/* // @grant none // @run-at document-idle // @grant GM_getResourceText // @grant GM_getValue // @grant GM_info // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-start // @antifeature ads 课程解答 // @license MIT // ==/UserScript== (function () { 'use strict'; const PANEL_ID = '__course_list_panel_v270__'; const HIGHLIGHT_CLASS = '__course_list_highlight_v270__'; const STORAGE_KEY = '__clp_v270_progress__'; const STORAGE_CONFIG_KEY = '__clp_v270_config__'; let courseItems = []; let filteredItems = []; let statusText = '就绪'; let monitorTimer = null; let selectionObserver = null; let panelCollapsed = false; let lastSelectedId = ''; let lastVideoSrc = ''; let lastPdfSrc = ''; let currentVideoBound = null; let currentBoundDoc = null; let autoNextEnabled = true; let autoLocateCurrent = true; let keywordCache = ''; let isAutoAdvancing = false; let lastEndedToken = ''; let lastNearEndToken = ''; let playbackRate = 1.0; let progressStore = {}; let rangeStart = ''; let rangeEnd = ''; let resumeEnabled = true; let pdfSimpleEnabled = true; let pdfReadSeconds = 30; let pdfTimerToken = ''; let pdfTimerStartedAt = 0; let pdfTimerLeft = 0; let pdfTimerHandle = null; let saveProgressThrottle = 0; let activeResumeToken = ''; let lastScanAt = 0; let dragState = { dragging: false, startX: 0, startY: 0, startLeft: 0, startTop: 0 }; function getText(el) { if (!el) return ''; const direct = (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim(); if (direct) return direct; const attrs = [ el.getAttribute?.('title'), el.getAttribute?.('aria-label'), el.getAttribute?.('alt'), el.getAttribute?.('data-title'), el.getAttribute?.('data-name') ].filter(Boolean); return attrs.join(' ').replace(/\s+/g, ' ').trim(); } function getAnchorDisplayText(a, index = 0) { if (!a) return `第${index + 1}项`; const candidates = [ a.getAttribute('title'), a.getAttribute('aria-label'), a.getAttribute('data-title'), a.getAttribute('data-name'), getText(a) ]; const li = a.closest('li'); const parent = a.parentElement; if (li) { candidates.push(li.getAttribute?.('title')); candidates.push(getText(li)); } if (parent) { candidates.push(parent.getAttribute?.('title')); candidates.push(getText(parent)); } let text = candidates .filter(Boolean) .map(x => String(x).replace(/\s+/g, ' ').trim()) .find(x => x && x.length > 0); if (!text) text = `第${index + 1}项`; if (text.length > 120) text = text.slice(0, 120).trim(); return text; } function escapeHtml(str) { return String(str || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); } function toInt(v) { const n = parseInt(v, 10); return Number.isFinite(n) ? n : 0; } function hasNumberPrefix(text) { return /^\d+[/.\-、()()]?\s*/.test(String(text || '').trim()); } function loadJson(key, fallback) { try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : fallback; } catch { return fallback; } } function saveJson(key, data) { try { localStorage.setItem(key, JSON.stringify(data)); } catch (e) { console.warn('localStorage 保存失败:', e); } } function loadConfig() { const cfg = loadJson(STORAGE_CONFIG_KEY, {}); playbackRate = Number(cfg.playbackRate || 1.0); rangeStart = cfg.rangeStart ?? ''; rangeEnd = cfg.rangeEnd ?? ''; autoNextEnabled = cfg.autoNextEnabled ?? true; autoLocateCurrent = cfg.autoLocateCurrent ?? true; keywordCache = cfg.keywordCache ?? ''; resumeEnabled = cfg.resumeEnabled ?? true; panelCollapsed = cfg.panelCollapsed ?? false; pdfSimpleEnabled = cfg.pdfSimpleEnabled ?? true; pdfReadSeconds = Number(cfg.pdfReadSeconds || 30); } function saveConfig() { saveJson(STORAGE_CONFIG_KEY, { playbackRate, rangeStart, rangeEnd, autoNextEnabled, autoLocateCurrent, keywordCache, resumeEnabled, panelCollapsed, pdfSimpleEnabled, pdfReadSeconds }); } function loadProgressStore() { progressStore = loadJson(STORAGE_KEY, {}); } function saveProgressStore() { saveJson(STORAGE_KEY, progressStore); } function setStatus(text) { statusText = text; const el = document.getElementById('__clp_status__'); if (el) el.textContent = text; } function isVisible(el, doc = document) { if (!el) return false; try { const rect = el.getBoundingClientRect(); const style = (doc.defaultView || window).getComputedStyle(el); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 8 && rect.height > 8; } catch { return false; } } function removeOldHighlight() { document.querySelectorAll('.' + HIGHLIGHT_CLASS).forEach(el => { el.classList.remove(HIGHLIGHT_CLASS); }); } function highlightElement(el) { if (!el) return; removeOldHighlight(); el.classList.add(HIGHLIGHT_CLASS); try { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch {} } function isCatalogPage() { return !!document.querySelector('.r-tree.dTree2') || !!document.querySelector('a[onclick*="playFile"]') || !!document.querySelector('a[href*="playVideo"]') || !!document.querySelector('iframe[src*="pdfjs/web/viewer.html"]'); } function isPlayVideoPage() { return /\/play\/playVideo\.action/i.test(location.href); } function getPageMode() { if (isCatalogPage()) return 'catalog'; if (isPlayVideoPage()) return 'player'; return 'unknown'; } function getCurrentSelectedAnchor() { const selectors = [ '.r-tree.dTree2 a.selected', '.r-tree.dTree2 a.curSelectedNode', '.r-tree.dTree2 a.nodeSel', 'a.selected', 'li.current a', 'li.active a', 'a[aria-current="page"]' ]; for (const sel of selectors) { const el = document.querySelector(sel); if (el) return el; } return null; } function getVideoRootLi() { const spans = [...document.querySelectorAll('.r-tree.dTree2 li.parent_li > span.parent_tit, span.parent_tit')]; for (const span of spans) { const text = getText(span); if (text.includes('视频课堂') || text.includes('视频') || text.includes('章节辅导') || text.includes('课程资源')) { return span.closest('li.parent_li, li'); } } return null; } function uniqueBy(arr, keyFn) { const map = new Map(); arr.forEach(item => { const key = keyFn(item); if (!map.has(key)) map.set(key, item); }); return [...map.values()]; } function getSameOriginDocsLevel1() { const list = [{ doc: document, path: 'top', frame: null }]; const iframes = [...document.querySelectorAll('iframe, frame')]; for (let i = 0; i < iframes.length; i++) { const f = iframes[i]; try { const doc = f.contentDocument || f.contentWindow?.document; if (doc) { list.push({ doc, path: `frame${i}`, frame: f }); } } catch {} } return list; } function getVisibleVideoDetail() { const docs = getSameOriginDocsLevel1(); let fallback = null; for (const ctx of docs) { let videos = []; try { videos = [...ctx.doc.querySelectorAll('video')]; } catch {} const visible = videos.filter(v => isVisible(v, ctx.doc)); if (visible.length) { return { video: visible[0], doc: ctx.doc, frame: ctx.frame, source: ctx.path, visible: true }; } if (!fallback && videos.length) { fallback = { video: videos[0], doc: ctx.doc, frame: ctx.frame, source: ctx.path, visible: false }; } } return fallback || { video: null, doc: document, frame: null, source: 'none', visible: false }; } function getVisiblePdfDetail() { const candidates = [...document.querySelectorAll('iframe, embed, object')]; const visibleCandidates = candidates.filter(el => { const src = el.getAttribute('src') || el.getAttribute('data') || ''; return /pdfjs\/web\/viewer\.html|\.pdf(\?|$)/i.test(src) && isVisible(el); }); if (visibleCandidates.length) { const el = visibleCandidates[0]; return { element: el, src: el.getAttribute('src') || el.getAttribute('data') || '', tag: el.tagName.toLowerCase(), visible: true }; } const any = candidates.find(el => { const src = el.getAttribute('src') || el.getAttribute('data') || ''; return /pdfjs\/web\/viewer\.html|\.pdf(\?|$)/i.test(src); }); if (any) { return { element: any, src: any.getAttribute('src') || any.getAttribute('data') || '', tag: any.tagName.toLowerCase(), visible: false }; } return { element: null, src: '', tag: '', visible: false }; } function getVisibleVideo() { return getVisibleVideoDetail().video || null; } function getCurrentContentInfo() { const videoDetail = getVisibleVideoDetail(); if (videoDetail.video) { const video = videoDetail.video; const currentTime = Number(video.currentTime || 0); const duration = Number(video.duration || 0); const progress = duration > 0 ? (currentTime / duration) * 100 : 0; return { type: 'video', exists: true, src: video.currentSrc || video.src || '', paused: !!video.paused, muted: !!video.muted, currentTime, duration, progress, ended: !!video.ended, readyState: Number(video.readyState || 0), source: videoDetail.source || 'top' }; } const pdfDetail = getVisiblePdfDetail(); if (pdfDetail.element) { return { type: 'pdf', exists: true, src: pdfDetail.src || '', paused: false, muted: false, currentTime: 0, duration: 0, progress: 0, ended: false, readyState: 1, source: pdfDetail.tag || 'iframe' }; } return { type: 'none', exists: false, src: '', paused: true, muted: false, currentTime: 0, duration: 0, progress: 0, ended: false, readyState: 0, source: 'none' }; } async function waitForAnyVideo(timeout = 12000) { const start = Date.now(); while (Date.now() - start < timeout) { const detail = getVisibleVideoDetail(); if (detail.video) return detail; await wait(500); } return { video: null, doc: document, frame: null, source: 'timeout', visible: false }; } function formatTime(sec) { if (!Number.isFinite(sec)) return '--:--'; const s = Math.floor(sec % 60); const m = Math.floor((sec / 60) % 60); const h = Math.floor(sec / 3600); if (h > 0) return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } function getVideoInfo(video = null) { const detail = getVisibleVideoDetail(); const target = video || detail.video; if (!target) { return { exists: false, src: '', paused: true, muted: false, currentTime: 0, duration: 0, ended: false, readyState: 0, progress: 0, playbackRate, source: detail.source, inIframe: !!detail.frame }; } const currentTime = Number(target.currentTime || 0); const duration = Number(target.duration || 0); const progress = duration > 0 ? (currentTime / duration) * 100 : 0; return { exists: true, src: target.currentSrc || target.src || '', paused: !!target.paused, muted: !!target.muted, currentTime, duration, ended: !!target.ended, readyState: Number(target.readyState || 0), progress, playbackRate: Number(target.playbackRate || 1), source: detail.source || 'top', inIframe: !!detail.frame }; } function getCourseKey(item) { if (!item) return ''; return String(item.id || item.dataId || item.text || item.index || location.href); } function getCurrentSelectedItem() { const current = getCurrentSelectedAnchor(); if (!current) return null; return courseItems.find(item => item.anchor === current || item.id === current.id) || null; } function getCurrentPseudoItem() { const current = getCurrentSelectedItem(); if (current) return current; const title = document.title || '当前内容'; const params = new URLSearchParams(location.search); return { index: 1, text: title, shortText: title, id: location.pathname + location.search, dataId: params.get('courseId') || params.get('ksId') || '', anchor: null, element: null, resourceType: getCurrentContentInfo().type }; } function getCurrentCourseProgress() { const current = getCurrentSelectedItem() || getCurrentPseudoItem(); if (!current) return null; return progressStore[getCourseKey(current)] || null; } function detectAnchorResourceType(a, text = '') { const href = a.getAttribute('href') || ''; const onclick = a.getAttribute('onclick') || ''; const mix = `${href} ${onclick} ${text}`.toLowerCase(); if (/playfile|playvideo/.test(mix)) return 'video'; if (/pdfjs\/web\/viewer\.html|\.pdf(\?|$)/i.test(href)) return 'pdf'; if (/pdf|讲义|教材|文档|附件|参考答案/.test(mix) && href) return 'pdf'; return 'unknown'; } function buildCandidateAnchors() { const selectors = [ 'a[onclick*="playFile"]', 'a[href*="playVideo"]', 'a[href*="playFile"]', 'a[href*="pdfjs/web/viewer.html"]', 'a[href$=".pdf"]', 'a[href*=".pdf?"]', '.r-tree.dTree2 a', 'li a' ]; let all = []; selectors.forEach(sel => { try { all = all.concat([...document.querySelectorAll(sel)]); } catch {} }); return uniqueBy(all, a => `${a.id}|${a.getAttribute('href') || ''}|${a.getAttribute('onclick') || ''}|${getText(a)}|${a.getAttribute('title') || ''}`); } function findCourseItems() { const root = getVideoRootLi(); let anchors = []; let globalAnchors = buildCandidateAnchors(); if (root) { try { anchors = [...root.querySelectorAll('a[onclick*="playFile"], a[href*="playVideo"], a[href*="playFile"], a[href*="pdfjs/web/viewer.html"], a[href$=".pdf"], a[href*=".pdf?"], a')]; } catch (e) { console.warn('root query 失败:', e); } } if (!anchors.length) anchors = globalAnchors; anchors = uniqueBy(anchors, a => `${a.id}|${a.getAttribute('href') || ''}|${a.getAttribute('onclick') || ''}|${a.getAttribute('title') || ''}|${getText(a)}`); const mapped = anchors.map((a, idx) => { const text = getAnchorDisplayText(a, idx); const onclick = a.getAttribute('onclick') || ''; const href = a.getAttribute('href') || ''; const resourceType = detectAnchorResourceType(a, text); const score = (onclick.includes('playFile') ? 5 : 0) + (/playVideo|playFile/i.test(href) ? 4 : 0) + (/pdfjs\/web\/viewer\.html|\.pdf(\?|$)/i.test(href) ? 4 : 0) + (hasNumberPrefix(text) ? 3 : 0) + ((text.includes('视频') || text.includes('第') || text.includes('参考答案') || text.includes('讲义')) ? 1 : 0); return { index: idx + 1, text: text || `第${idx + 1}项`, shortText: text || `第${idx + 1}项`, id: a.id || `auto_${idx}`, dataId: a.dataset.id || '', onclick, href, visible: isVisible(a), active: a.classList.contains('selected') || a.classList.contains('active') || a.classList.contains('curSelectedNode') || a.classList.contains('nodeSel'), anchor: a, element: a.closest('li') || a, score, resourceType }; }).filter(item => { if (item.resourceType === 'video') return true; if (item.resourceType === 'pdf') return true; if (item.score >= 4) return true; if (item.text && hasNumberPrefix(item.text)) return true; return false; }); const finalItems = uniqueBy(mapped, item => `${item.text}|${item.onclick}|${item.href}`).map((item, idx) => ({ ...item, index: idx + 1, progressInfo: progressStore[getCourseKey(item)] || null })); console.log('[findCourseItems] rootFound=', !!root); console.log('[findCourseItems] rootAnchors=', anchors.length); console.log('[findCourseItems] globalAnchors=', globalAnchors.length); console.log('[findCourseItems] finalItems=', finalItems.length, finalItems.slice(0, 10)); return finalItems; } function findItemByIndex(index) { return courseItems.find(x => x.index === index); } function getRangeNormalized() { const start = toInt(rangeStart); const end = toInt(rangeEnd); return { start: start > 0 ? start : 0, end: end > 0 ? end : 0 }; } function inPlayRange(item) { if (!item) return false; const { start, end } = getRangeNormalized(); if (start > 0 && item.index < start) return false; if (end > 0 && item.index > end) return false; return true; } function getNextItemFromCurrent() { const current = getCurrentSelectedItem(); if (!current) return null; const { start, end } = getRangeNormalized(); if (start > 0 && current.index < start) { return findItemByIndex(start) || null; } const next = courseItems.find(x => x.index === current.index + 1) || null; if (!next) return null; if (end > 0 && next.index > end) return null; return next; } function updateOptionUI() { const autoNext = document.getElementById('__clp_auto_next__'); const autoLocate = document.getElementById('__clp_auto_locate__'); const resumeEl = document.getElementById('__clp_resume_enabled__'); const rateEl = document.getElementById('__clp_rate__'); const rangeStartEl = document.getElementById('__clp_range_start__'); const rangeEndEl = document.getElementById('__clp_range_end__'); const pdfSimpleEl = document.getElementById('__clp_pdf_simple__'); const pdfSecondsEl = document.getElementById('__clp_pdf_seconds__'); if (autoNext) autoNext.checked = autoNextEnabled; if (autoLocate) autoLocate.checked = autoLocateCurrent; if (resumeEl) resumeEl.checked = resumeEnabled; if (rateEl) rateEl.value = String(playbackRate); if (rangeStartEl) rangeStartEl.value = rangeStart; if (rangeEndEl) rangeEndEl.value = rangeEnd; if (pdfSimpleEl) pdfSimpleEl.checked = pdfSimpleEnabled; if (pdfSecondsEl) pdfSecondsEl.value = String(pdfReadSeconds); const catalogOnlyEls = document.querySelectorAll('[data-catalog-only="1"]'); catalogOnlyEls.forEach(el => { el.style.display = courseItems.length > 0 ? '' : 'none'; }); } function applyPlaybackRate(video = null) { const target = video || getVisibleVideo(); if (!target) return; try { target.playbackRate = playbackRate; } catch (e) { console.warn('设置倍速失败:', e); } } function saveCurrentProgress(force = false) { const now = Date.now(); if (!force && now - saveProgressThrottle < 1500) return; saveProgressThrottle = now; const current = getCurrentSelectedItem() || getCurrentPseudoItem(); if (!current) return; const content = getCurrentContentInfo(); if (content.type === 'video') { if (!Number.isFinite(content.currentTime) || content.currentTime <= 0) return; progressStore[getCourseKey(current)] = { index: current.index || 1, text: current.text || document.title || '当前视频', currentTime: content.currentTime, duration: content.duration || 0, progress: content.progress || 0, src: content.src || '', updatedAt: Date.now(), completed: content.ended || (content.duration > 0 && content.progress >= 99.5), pageMode: getPageMode(), pageUrl: location.href, resourceType: 'video' }; saveProgressStore(); return; } if (content.type === 'pdf') { progressStore[getCourseKey(current)] = { index: current.index || 1, text: current.text || document.title || '当前PDF', currentTime: pdfTimerStartedAt ? Math.max(0, (Date.now() - pdfTimerStartedAt) / 1000) : 0, duration: Number(pdfReadSeconds || 0), progress: pdfReadSeconds > 0 && pdfTimerStartedAt ? Math.min(100, ((Date.now() - pdfTimerStartedAt) / 1000 / pdfReadSeconds) * 100) : 0, src: content.src || '', updatedAt: Date.now(), completed: false, pageMode: getPageMode(), pageUrl: location.href, resourceType: 'pdf' }; saveProgressStore(); } } function markCurrentCompleted(extra = {}) { const item = getCurrentSelectedItem() || getCurrentPseudoItem(); if (!item) return; const content = getCurrentContentInfo(); const isPdf = content.type === 'pdf' || item.resourceType === 'pdf'; progressStore[getCourseKey(item)] = { index: item.index || 1, text: item.text || document.title || '当前内容', currentTime: isPdf ? Number(pdfReadSeconds || 0) : (content.duration || content.currentTime || 0), duration: isPdf ? Number(pdfReadSeconds || 0) : (content.duration || 0), progress: 100, src: content.src || '', updatedAt: Date.now(), completed: true, pageMode: getPageMode(), pageUrl: location.href, resourceType: isPdf ? 'pdf' : 'video', ...extra }; saveProgressStore(); } async function restoreProgressForCurrent(video = null) { if (!resumeEnabled) return; const targetVideo = video || getVisibleVideo(); if (!targetVideo) return; const item = getCurrentSelectedItem() || getCurrentPseudoItem(); if (!item) return; const rec = progressStore[getCourseKey(item)]; if (!rec || rec.resourceType !== 'video' || !Number.isFinite(rec.currentTime) || rec.currentTime <= 0 || rec.completed) return; const token = `${getCourseKey(item)}|${targetVideo.currentSrc || targetVideo.src || ''}`; if (activeResumeToken === token) return; const duration = Number(targetVideo.duration || 0); if (duration > 0 && rec.currentTime >= duration - 2) return; try { activeResumeToken = token; await wait(300); const targetTime = Math.max(0, rec.currentTime - 1); if (targetTime > 0) { targetVideo.currentTime = targetTime; setStatus(`已恢复断点:${item.text} -> ${formatTime(targetTime)}`); updateVideoInfoUI(); } } catch (e) { console.warn('恢复断点失败:', e); } } async function tryPlayVideo(video = null) { const targetVideo = video || getVisibleVideo(); if (!targetVideo) return { ok: false, reason: '未找到 video 元素' }; applyPlaybackRate(targetVideo); try { if (!targetVideo.paused) return { ok: true, reason: '视频已在播放' }; await targetVideo.play(); applyPlaybackRate(targetVideo); return { ok: true, reason: 'play() 成功' }; } catch { try { targetVideo.muted = true; await targetVideo.play(); applyPlaybackRate(targetVideo); return { ok: true, reason: 'muted + play() 成功' }; } catch { return { ok: false, reason: '浏览器拦截自动播放或站点脚本干预' }; } } } function stopPdfCountdown() { if (pdfTimerHandle) { clearInterval(pdfTimerHandle); pdfTimerHandle = null; } pdfTimerToken = ''; pdfTimerStartedAt = 0; pdfTimerLeft = 0; } function startPdfCountdown(item, src = '') { if (!pdfSimpleEnabled || !autoNextEnabled || !item) return; const token = `${item.index}|${item.id}|${src || item.href || ''}|${pdfReadSeconds}`; if (pdfTimerToken === token && pdfTimerHandle) return; stopPdfCountdown(); pdfTimerToken = token; pdfTimerStartedAt = Date.now(); pdfTimerLeft = pdfReadSeconds; setStatus(`PDF 简单阅读计时开始:${item.text}(${pdfReadSeconds}秒)`); updateVideoInfoUI(); pdfTimerHandle = setInterval(async () => { const elapsed = Math.floor((Date.now() - pdfTimerStartedAt) / 1000); pdfTimerLeft = Math.max(0, pdfReadSeconds - elapsed); updateVideoInfoUI(); saveCurrentProgress(false); if (pdfTimerLeft <= 0) { clearInterval(pdfTimerHandle); pdfTimerHandle = null; const current = getCurrentSelectedItem() || item; markCurrentCompleted({ completeReason: 'pdf_simple_wait' }); setStatus(`PDF 停留计时完成:${current.text}`); if (!isAutoAdvancing) { await autoPlayNext('pdfTimeout'); } } }, 1000); } async function playCurrentVideoOnly() { const content = getCurrentContentInfo(); if (content.type === 'video') { setStatus('尝试播放当前视频...'); const result = await tryPlayVideo(); updateVideoInfoUI(); setStatus(result.ok ? `成功:${result.reason}` : `失败:${result.reason}`); return; } if (content.type === 'pdf') { const item = getCurrentSelectedItem() || getCurrentPseudoItem(); startPdfCountdown(item, content.src || ''); setStatus(`当前为 PDF,已开始简单阅读计时 ${pdfReadSeconds} 秒`); return; } setStatus('当前未检测到视频/PDF内容'); } function triggerAnchorOpen(a) { if (!a) return false; try { highlightElement(a.closest('li') || a); a.click(); ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach(type => { a.dispatchEvent(new MouseEvent(type, { view: window, bubbles: true, cancelable: true })); }); if (typeof a.onclick === 'function') a.onclick(); return true; } catch (err) { console.error('触发课程点击失败:', err); return false; } } async function waitForSelectionChange(oldId, timeout = 6000) { const start = Date.now(); while (Date.now() - start < timeout) { const current = getCurrentSelectedAnchor(); const newId = current?.id || ''; if (newId && newId !== oldId) { return { changed: true, anchor: current, id: newId }; } await wait(200); } const current = getCurrentSelectedAnchor(); return { changed: false, anchor: current, id: current?.id || '' }; } async function waitForContentChange(oldVideoSrc, oldPdfSrc, timeout = 10000) { const start = Date.now(); while (Date.now() - start < timeout) { const video = getVisibleVideo(); const videoSrc = video?.currentSrc || video?.src || ''; const pdf = getVisiblePdfDetail(); const pdfSrc = pdf.src || ''; if (videoSrc && videoSrc !== oldVideoSrc) { return { changed: true, type: 'video', src: videoSrc }; } if (pdfSrc && pdfSrc !== oldPdfSrc) { return { changed: true, type: 'pdf', src: pdfSrc }; } await wait(250); } const content = getCurrentContentInfo(); return { changed: false, type: content.type, src: content.src || '' }; } async function waitForVideoReady(timeout = 10000) { const start = Date.now(); while (Date.now() - start < timeout) { const detail = getVisibleVideoDetail(); const video = detail.video; if (video) { const src = video.currentSrc || video.src || ''; if (src || video.readyState >= 1) { return { ok: true, video, doc: detail.doc }; } } await wait(300); } const detail = getVisibleVideoDetail(); return { ok: false, video: detail.video, doc: detail.doc }; } async function openCourseAndPlay(item, options = {}) { if (!item?.anchor) { setStatus('当前项没有可点击链接'); return false; } const { autoTriggered = false, locate = true, retry = 2 } = options; for (let attempt = 1; attempt <= retry; attempt++) { const oldVideo = getVisibleVideo(); const oldVideoSrc = oldVideo?.currentSrc || oldVideo?.src || ''; const oldPdfSrc = getVisiblePdfDetail().src || ''; const oldSelectedId = getCurrentSelectedAnchor()?.id || ''; if (locate) highlightElement(item.element); setStatus(`${autoTriggered ? '自动' : '手动'}打开:${item.text}(第 ${attempt}/${retry} 次)`); const clicked = triggerAnchorOpen(item.anchor); if (!clicked) continue; await waitForSelectionChange(oldSelectedId, 5000); const changed = await waitForContentChange(oldVideoSrc, oldPdfSrc, 10000); const currentType = changed.type || item.resourceType || getCurrentContentInfo().type; if (currentType === 'video') { const readyResult = await waitForVideoReady(10000); const targetVideo = readyResult.video || getVisibleVideo(); applyPlaybackRate(targetVideo); await restoreProgressForCurrent(targetVideo); const playResult = await tryPlayVideo(targetVideo); updateVideoInfoUI(); if (playResult.ok) { stopPdfCountdown(); lightweightRescan(false, true); setStatus(`成功:${item.text} -> ${playResult.reason}`); return true; } await wait(700); continue; } if (currentType === 'pdf' || item.resourceType === 'pdf') { stopPdfCountdown(); lightweightRescan(false, true); startPdfCountdown(item, changed.src || item.href || ''); updateVideoInfoUI(); setStatus(`成功:已打开 PDF -> ${item.text},开始停留 ${pdfReadSeconds} 秒`); return true; } await wait(700); } setStatus(`最终失败:${item.text}`); return false; } function shouldTriggerNearEnd(info) { if (!info.exists) return false; if (!Number.isFinite(info.duration) || info.duration <= 0) return false; if (info.paused || info.ended) return false; const remain = info.duration - info.currentTime; return remain <= 0.8 && info.progress >= 98.5; } async function autoPlayNext(reason = 'ended') { if (!autoNextEnabled || isAutoAdvancing) return; const currentItem = getCurrentSelectedItem(); if (!currentItem) return; const content = getCurrentContentInfo(); const currentSrc = content.src || ''; const token = `${reason}|${currentSrc}|${currentItem.index}|${currentItem.id || ''}`; if (reason === 'ended' && lastEndedToken === token) return; if (reason === 'nearEnd' && lastNearEndToken === token) return; if (reason === 'ended') lastEndedToken = token; if (reason === 'nearEnd') lastNearEndToken = token; const nextItem = getNextItemFromCurrent(); if (!nextItem) { setStatus(`已播放完范围末尾或最后一项:${currentItem.text}`); return; } isAutoAdvancing = true; try { setStatus(`当前内容已完成,自动切换下一项:${nextItem.text}`); await openCourseAndPlay(nextItem, { autoTriggered: true, locate: autoLocateCurrent, retry: 3 }); } finally { setTimeout(() => { isAutoAdvancing = false; }, 1500); } } function locateCurrentSelected() { const a = getCurrentSelectedAnchor(); if (!a) { setStatus('未找到当前选中课程'); return; } highlightElement(a.closest('li') || a); setStatus(`已定位当前课程:${a.getAttribute('title') || getText(a)}`); } function copyCurrentContentUrl() { const content = getCurrentContentInfo(); const src = content.src || ''; if (!src) { setStatus('复制失败:当前没有内容地址'); return; } navigator.clipboard.writeText(src).then(() => { setStatus('已复制当前内容地址'); }).catch(() => { const ta = document.createElement('textarea'); ta.value = src; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); setStatus('已复制当前内容地址'); }); } function clearAllProgress() { if (!confirm('确定清空所有断点记录吗?')) return; progressStore = {}; saveProgressStore(); stopPdfCountdown(); lightweightRescan(false, true); updateVideoInfoUI(); setStatus('已清空所有断点记录'); } function updateVideoInfoUI() { const infoEl = document.getElementById('__clp_video_info__'); if (!infoEl) return; const content = getCurrentContentInfo(); const currentAnchor = getCurrentSelectedAnchor(); const currentTitle = currentAnchor?.getAttribute('title') || getText(currentAnchor) || document.title || '无'; const currentProgress = getCurrentCourseProgress(); const mode = getPageMode(); if (!content.exists) { infoEl.innerHTML = `
| 序号 | 课程标题 | data-id | 类型 | 状态 | 操作 |
|---|