// ==UserScript== // @name 山东省教师教育网 · 课程倍速助手(免费版) // @namespace lm4217 // @version 3.0.0 // @description 免费版学习辅助:后台播放,实验性的倍速播放。自动完成全部课程为独立 VIP 版,需卡密激活后使用。 // @author lm4217 // @license MIT // @match *://yxjc.qlteacher.com/project/* // @match *://yxjc.qlteacher.com/pre-project/* // @match *://player.qlteacher.com/* // @grant none // @run-at document-end // @antifeature payment // @tag 山东省教育教育网 // @tag 学习辅助 // @tag 后台播放 // ==/UserScript== (() => { 'use strict'; // ================================================================== // 免费版:只做倍速与观察,不含任何自动化学习动作。 // 本脚本不发起任何网络请求,全部功能在本地完成。 // 完整学习引擎是另一个独立脚本,卡密激活后由用户自行安装。 // ================================================================== const BUY_URL = 'https://wzyp.cn/item/i9awt5'; // VIP 脚本启动时会写下这个 cookie,并注入带 lcs-dot 的悬浮窗。 // 命中任一即说明 VIP 已在工作,本脚本立刻让位,避免两个面板叠加。 function paidRunning() { if (window.__LCS_PAID__) return true; try { if (/(?:^|; )lcs_paid=/.test(document.cookie)) return true; } catch (e) {} return !!document.getElementById('lcs-dot'); } const projectPage = location.hostname === 'yxjc.qlteacher.com' && /^\/(?:pre-)?project\/(?:yey|xx|cz|gz)20\d{2}(?:\/|$)/.test(location.pathname); const playerPage = location.hostname === 'player.qlteacher.com' && /^\/learning(?:\/|$)/.test(location.pathname); if (!projectPage && !playerPage) return; if (document.getElementById('qlteacher-playback-helper')) return; if (paidRunning()) return; const RATES = [1,1.25,1.5,2,3,4,8,16]; const PROBE_RATES = [2,3,4,8,16]; const states = new WeakMap(); let speedMode='follow'; let requestedRate=1; let speedNote=''; let probe=null; let validationBlocked = false; let scheduled = false; let busy = false; let message = '等待课程加载'; let backgroundStarted = null; let backgroundStartTime = null; let backgroundVideo = null; let backgroundSource = null; let backgroundSeeked = false; let backgroundResult = ''; let retired = false; const panel = document.createElement('div'); panel.id = 'qlteacher-playback-helper'; panel.style.cssText = 'position:fixed;right:16px;bottom:18px;z-index:2147483646;transition:bottom .3s ease;'; const root = panel.attachShadow({mode:'open'}); root.innerHTML = `
📚课程倍速助手 · 免费版

VIP 版:未安装
VIP 版是独立脚本,装好后在它的面板里输入卡密即可解锁自动串行完成全部课程。免费版只做倍速,不会自动播放或点击课程。
`; (document.body || document.documentElement).appendChild(panel); const status = root.getElementById('status'); const clock = root.getElementById('time'); const background = root.getElementById('background'); const speed = root.getElementById('speed'); const modeSelect = root.getElementById('mode'); const rateSelect = root.getElementById('rate'); const probeButton = root.getElementById('probe'); const vipState = root.getElementById('vipstate'); const buyLink = root.getElementById('buy'); modeSelect.value=speedMode; rateSelect.value=String(requestedRate); buyLink.href = BUY_URL; const collapseBtn = root.getElementById('collapse'); const panelBody = root.getElementById('pbody'); let panelCollapsed = false; collapseBtn.addEventListener('click', () => { panelCollapsed = !panelCollapsed; panelBody.classList.toggle('hide', panelCollapsed); collapseBtn.textContent = panelCollapsed ? '▸' : '▾'; }); var observer = null; var timer = null; // VIP 脚本接管后,本脚本彻底退场:不残留面板,也不再改播放器倍速 function retire() { if (retired) return; retired = true; if (probe) probe = null; try { observer && observer.disconnect(); } catch (e) {} try { clearInterval(timer); } catch (e) {} try { panel.remove(); } catch (e) {} } function applyRate(video, rate) { if (validationBlocked && rate !== 1) return false; const state=states.get(video); if (!state) return false; state.expectedRate=rate; try { video.playbackRate=rate; if (Math.abs(video.playbackRate-rate)>0.05) throw Error('Rate rejected'); state.appliedSource=video.currentSrc || video.src; return true; } catch (error) { state.expectedRate=null; state.appliedSource=video.currentSrc || video.src; speedNote='播放器不支持 '+rate+'×,请降低倍速'; return false; } } function stopProbe(note) { if (!probe) return; const run=probe; probe=null; speedMode='fixed'; requestedRate=run.best; modeSelect.value=speedMode; rateSelect.value=String(requestedRate); applyRate(run.video,requestedRate); speedNote=note+';采用 '+requestedRate+'×'; } function startProbe() { if (validationBlocked || validationWarning()) { tick(); return; } const video=currentVideo(); if (!video || video.paused || video.ended || questionOpen()) { speedNote='请先播放视频,再开始试速'; render(); return; } stopProbe('重新试速'); speedMode='auto'; modeSelect.value=speedMode; probe={video,source:video.currentSrc || video.src,index:0,best:1,sample:null}; if (!applyRate(video,PROBE_RATES[0])) stopProbe('首档倍速不可用'); else speedNote='试速 '+PROBE_RATES[0]+'×,每档观察至少 6 秒'; render(); } function observeSpeed(video,state) { const now=Date.now(); if (video.paused || video.ended || video.seeking || questionOpen()) { state.sample=null; if (probe) stopProbe('播放中断,试速结束'); return; } if (probe) { const run=probe; if (run.video !== video || run.source !== (video.currentSrc || video.src)) { stopProbe('视频已切换,试速结束'); return; } if (!run.sample) run.sample={at:now,time:video.currentTime}; const elapsed=(now-run.sample.at)/1000; if (elapsed<6) return; const observed=(video.currentTime-run.sample.time)/elapsed; const target=PROBE_RATES[run.index]; state.measured=observed; if (elapsed>15 || !Number.isFinite(observed) || observedtarget*1.3) { stopProbe(elapsed>15 ? '采样间隔过长,请在前台重新试速' : '当前档进度不稳定,结束试速'); return; } run.best=target; run.index++; if (run.index>=PROBE_RATES.length) { stopProbe('试速完成'); return; } run.sample=null; const next=PROBE_RATES[run.index]; if (!applyRate(video,next)) stopProbe('更高倍速不可用'); else speedNote='实测 '+observed.toFixed(2)+'×;正在试 '+next+'×'; return; } if (!state.sample) { state.sample={at:now,time:video.currentTime}; return; } const elapsed=(now-state.sample.at)/1000; if (elapsed<4) return; const observed=(video.currentTime-state.sample.time)/elapsed; state.measured=elapsed<=15 && observed>=0 ? observed : null; state.sample={at:now,time:video.currentTime}; } function visible(el) { if (!el || !el.isConnected || el.hidden) return false; const style = getComputedStyle(el); return style.display !== 'none' && style.visibility !== 'hidden' && el.getClientRects().length > 0; } function currentVideo() { const videos = [...document.querySelectorAll('video')].filter(visible); return videos.find(v => !v.paused && !v.ended) || videos[0] || null; } function questionOpen() { const title = document.querySelector('.segmented-text-ellipsis'); if (title && visible(title) && title.textContent.trim() === '测试题') return true; return [...document.querySelectorAll('[role="dialog"], .ant-modal-content')].some(el => visible(el) && (el.querySelector('input[type="radio"], input[type="checkbox"], .ant-radio-input, .ant-checkbox') || /测试题|请作答|请回答|单选题|多选题|提交答案/.test(el.textContent))); } function validationWarning() { const text=document.body?.innerText || ''; return /(?:进度[\s\S]{0,40}(?:时长|时间)|(?:时长|时间)[\s\S]{0,40}进度)[\s\S]{0,30}(?:不符|不一致|异常)|(?:学习进度|学习时长|学习时间|学习速度)[\s\S]{0,20}异常[\s\S]{0,40}刷新/.test(text); } function stopForValidation(video) { validationBlocked=true; probe=null; speedMode='follow'; requestedRate=1; modeSelect.value=speedMode; rateSelect.value='1'; speedNote='平台要求刷新;本页倍速控制已停止'; message='进度与学习时间校验未通过,请刷新播放器'; if (video) { applyRate(video,1); video.pause(); message='进度与学习时间校验未通过,请刷新播放器'; } } function formatTime(seconds) { if (!Number.isFinite(seconds)) return '--:--'; return Math.floor(seconds / 60) + ':' + String(Math.floor(seconds % 60)).padStart(2,'0'); } function render() { status.textContent = message; status.dataset.s = validationBlocked ? 'err' : ''; const video = currentVideo(); clock.textContent = video ? formatTime(video.currentTime) + ' / ' + formatTime(video.duration) : ''; background.textContent = backgroundResult; const measured=video && states.get(video)?.measured; speed.textContent=(video ? '当前 '+video.playbackRate+'×' : '目标 '+requestedRate+'×')+ (Number.isFinite(measured) ? ',实测 '+measured.toFixed(2)+'×' : '')+ (speedNote ? ';'+speedNote : ''); rateSelect.disabled=validationBlocked || speedMode !== 'fixed'; modeSelect.disabled=validationBlocked; probeButton.disabled=validationBlocked; probeButton.textContent=probe ? '结束试速' : '开始试速'; renderVip(); } function renderVip() { if (!vipState) return; if (paidRunning()) { vipState.textContent = 'VIP 版:已安装'; vipState.className = 'on'; buyLink.style.display = 'none'; } else { vipState.textContent = 'VIP 版:未安装'; vipState.className = ''; buyLink.style.display = ''; } } function stateFor(video) { let state = states.get(video); if (state) return state; state = {source:video.currentSrc || video.src, expectedRate:null,appliedSource:null,sample:null,measured:null}; states.set(video,state); video.addEventListener('ratechange', () => { state.sample=null; state.measured=null; if (state.expectedRate !== null && Math.abs(video.playbackRate-state.expectedRate)>0.05) { // 尊重播放器的限制,停止试速并跟随实际速度。 probe=null; speedMode='follow'; modeSelect.value=speedMode; speedNote='播放器调整了倍速,已改为跟随播放器'; state.expectedRate=null; } render(); }); video.addEventListener('seeking', () => { state.sample=null; state.measured=null; if (document.hidden) backgroundSeeked=true; if (probe) stopProbe('进度发生跳转,试速结束'); }); video.addEventListener('playing', () => { state.sample=null; message = document.hidden ? '正在后台播放' : '正在播放'; render(); }); video.addEventListener('pause', () => { if (video.ended) return; message = questionOpen() ? '等待本人处理测验' : '已暂停,点击视频继续'; queue(); }); video.addEventListener('ended', () => { message='本段视频播放结束'; render(); }); video.addEventListener('timeupdate', render); video.addEventListener('loadedmetadata', queue); video.addEventListener('emptied', queue); return state; } function tick() { if (retired) return; // VIP 版可能在本脚本之后才注入,每轮都复查一次 if (paidRunning()) { retire(); return; } if (busy) return; busy=true; try { const video=currentVideo(); if (validationBlocked) { message='进度与学习时间校验未通过,请刷新播放器'; return; } if (validationWarning()) { stopForValidation(video); return; } if (questionOpen()) { if(probe)stopProbe('遇到测验,试速结束'); message='等待本人处理测验'; return; } if (!video) { message='请在页面上点开课程视频'; return; } const state=stateFor(video); const source=video.currentSrc || video.src; if (source !== state.source) { state.source=source; state.sample=null; state.measured=null; } if (speedMode === 'fixed' && state.appliedSource !== source) applyRate(video,requestedRate); observeSpeed(video,state); if (video.error) { message='视频加载出错,请检查网络或课程资源'; return; } // 只观察,不代劳:播放/暂停由使用者自己操作 if (!video.paused && !video.ended) { message = document.hidden ? '正在后台播放' : '正在播放'; } else if (video.ended) { message='本段视频播放结束'; } else { message='已暂停,点击视频继续'; } } catch (error) { message='页面暂未就绪,稍后重试'; console.warn('[QL2026]',error); } finally { busy=false; if (!retired) render(); } } function queue() { if (scheduled) return; scheduled=true; setTimeout(() => { scheduled=false; tick(); },300); } modeSelect.addEventListener('change', () => { if (validationBlocked || validationWarning()) { tick(); return; } const selected=modeSelect.value; if (probe) stopProbe('已切换方式'); speedMode=selected; modeSelect.value=selected; speedNote=selected === 'auto' ? '仅比较播放速度,学时以平台记录为准;点击开始试速' : ''; const video=currentVideo(); if (video && selected === 'fixed') applyRate(video,requestedRate); if (video && selected === 'follow') stateFor(video).expectedRate=null; render(); }); rateSelect.addEventListener('change', () => { if (validationBlocked || validationWarning()) { tick(); return; } const selected=Number(rateSelect.value); if (!RATES.includes(selected)) return; if (probe) stopProbe('已选择固定倍速'); speedMode='fixed'; modeSelect.value=speedMode; requestedRate=selected; speedNote=''; const video=currentVideo(); if (video) applyRate(video,requestedRate); render(); }); probeButton.addEventListener('click', () => { if (validationBlocked || validationWarning()) { tick(); return; } if (probe) stopProbe('手动结束试速'); else startProbe(); render(); }); document.addEventListener('visibilitychange', () => { const video=currentVideo(); if (document.hidden) { backgroundStarted=Date.now(); backgroundStartTime=video ? video.currentTime : null; backgroundVideo=video; backgroundSource=video ? (video.currentSrc || video.src) : null; backgroundSeeked=false; if (video) stateFor(video).retries=0; backgroundResult='后台观察中'; } else if (backgroundStarted !== null) { const elapsed=(Date.now()-backgroundStarted)/1000; const sameVideo=video && video === backgroundVideo && (video.currentSrc || video.src) === backgroundSource; const advanced=sameVideo && !backgroundSeeked && backgroundStartTime !== null ? video.currentTime-backgroundStartTime : NaN; backgroundResult=Number.isFinite(advanced) ? '上次后台 '+Math.round(elapsed)+' 秒,视频前进 '+Math.max(0,Math.round(advanced))+' 秒'+ (elapsed>0 ? '('+(Math.max(0,advanced)/elapsed).toFixed(2)+'×)' : '') : '后台期间视频已切换、跳转或当前页没有视频,请重新测试'; backgroundStarted=null; backgroundStartTime=null; backgroundVideo=null; backgroundSource=null; } // 不刷新、不伪造页面可见性;让真实媒体继续播放。 queue(); render(); }); observer=new MutationObserver(queue); observer.observe(document.body || document.documentElement,{childList:true,subtree:true}); timer=setInterval(tick,2000); window.addEventListener('pagehide', () => { observer.disconnect(); clearInterval(timer); },{once:true}); tick(); })();