// ==UserScript== // @name 中国继续医学教育网-刷课助手(免费版) // @namespace http://tampermonkey.net/ // @version 1.1.2 // @author 道道龙 // @description 中国继续医学教育网ncme.org.cn专用刷课工具,免费版支持3x/5x倍速播放 // @match *://*.ncme.org.cn/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; var CONFIG = { speeds: [3, 5], defaultSpeed: 3, }; var state = { video: null, currentSpeed: CONFIG.defaultSpeed, muted: true, hookedSet: new Set(), uiReady: false, }; // ============================================================ // 读取记忆的倍速和静音设置 // ============================================================ function loadSettings() { state.currentSpeed = GM_getValue('__ncme_speed', CONFIG.defaultSpeed); state.muted = GM_getValue('__ncme_muted', true); console.log('[刷课助手] 读取记忆设置: speed=' + state.currentSpeed + 'x, muted=' + state.muted); } // ============================================================ // 核心1:拦截 playbackRate setter(原始脚本同款) // ============================================================ function getPropertyDescriptor(obj, prop) { var proto = obj; while (proto) { var desc = Object.getOwnPropertyDescriptor(proto, prop); if (desc) return desc; proto = Object.getPrototypeOf(proto); } return null; } function hookPlaybackRate(video) { if (state.hookedSet.has(video)) return; try { var originalDesc = getPropertyDescriptor(video, 'playbackRate'); if (!originalDesc) { console.log('[刷课助手] playbackRate描述符不存在'); return; } Object.defineProperty(video, 'playbackRate', { get: function () { if (originalDesc.get) return originalDesc.get.call(video); return originalDesc.value; }, set: function (val) { if (video._vRate != null && video._vRate != val) return; if (originalDesc.set) originalDesc.set.call(video, val); else originalDesc.value = val; } }); state.hookedSet.add(video); console.log('[刷课助手] playbackRate hook成功'); } catch (e) { console.log('[刷课助手] playbackRate hook失败:', e.message); } } function applySpeed(video, speed) { if (!video) return; hookPlaybackRate(video); video._vRate = speed; video.playbackRate = speed; video.muted = state.muted; state.currentSpeed = speed; GM_setValue('__ncme_speed', speed); // 记忆 console.log('[刷课助手] applySpeed: ' + speed + 'x'); } // ============================================================ // 核心2:拦截 play() 方法(关键!) // ============================================================ function hookPlayMethod() { try { var proto = HTMLMediaElement.prototype; if (proto._playHooked) return; var originalPlay = proto.play; proto.play = function () { var result = originalPlay.apply(this, arguments); setTimeout(function () { if (state.video && state.currentSpeed) { applySpeed(state.video, state.currentSpeed); console.log('[刷课助手] play()拦截重新设速=' + state.currentSpeed); } }, 50); return result; }; proto._playHooked = true; console.log('[刷课助手] play()方法拦截成功'); } catch (e) { console.log('[刷课助手] play()拦截失败:', e.message); } } // ============================================================ // 找视频 // ============================================================ function findVideo() { var videos = document.querySelectorAll('video'); for (var i = 0; i < videos.length; i++) { var v = videos[i]; if (v.offsetWidth > 100 && v.duration > 0 && !isNaN(v.duration)) { return v; } } return videos.length > 0 ? videos[0] : null; } // ============================================================ // MutationObserver // ============================================================ function setupObserver() { var obs = new MutationObserver(function () { var v = findVideo(); if (v && v !== state.video) { state.video = v; applySpeed(v, state.currentSpeed); if (state.uiReady) showToast('检测到视频,已应用 ' + state.currentSpeed + 'x'); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); } // ============================================================ // 轮询 // ============================================================ function startPolling() { setInterval(function () { var v = findVideo(); if (!v) return; if (v !== state.video) { state.video = v; applySpeed(v, state.currentSpeed); console.log('[刷课助手] 发现新视频: ' + v.id); } else { if (v.playbackRate != state.currentSpeed) { applySpeed(v, state.currentSpeed); console.log('[刷课助手] 轮询重同步: ' + v.playbackRate + '->' + state.currentSpeed); } } updateProgress(v); }, 300); } // ============================================================ // 进度 // ============================================================ function updateProgress(v) { if (!v || !v.duration || isNaN(v.duration)) return; var pct = Math.min(100, (v.currentTime / v.duration) * 100); var fill = document.getElementById('__ncme_pgf'); var txt = document.getElementById('__ncme_pgt'); if (fill) fill.style.width = pct + '%'; if (txt) { var cur = fmt(v.currentTime), tot = fmt(v.duration); txt.textContent = v.paused ? ('已暂停 ' + cur + ' / ' + tot) : (cur + ' / ' + tot); } } function fmt(s) { s = Math.floor(s); return Math.floor(s / 60) + ':' + (s % 60).toString().padStart(2, '0'); } // ============================================================ // Toast // ============================================================ var toastTimer = null; function showToast(msg) { var t = document.getElementById('__ncme_toast'); if (!t) { t = document.createElement('div'); t.id = '__ncme_toast'; document.body.appendChild(t); } t.textContent = msg; t.className = 'show'; clearTimeout(toastTimer); toastTimer = setTimeout(function () { t.className = 'hide'; }, 3000); } // ============================================================ // 切换倍速 // ============================================================ function doSetSpeed(speed) { state.currentSpeed = speed; if (state.video) { applySpeed(state.video, speed); showToast('已切换至 ' + speed + 'x'); } else { showToast('已选择 ' + speed + 'x,等待视频…'); } updateSpeedButtons(speed); } function updateSpeedButtons(activeSpeed) { var btns = document.querySelectorAll('.__sb[data-v]'); for (var i = 0; i < btns.length; i++) { var v = parseInt(btns[i].getAttribute('data-v'), 10); if (v === activeSpeed) btns[i].classList.add('on'); else btns[i].classList.remove('on'); } } // ============================================================ // UI // ============================================================ function buildUI() { if (document.getElementById('__ncme_panel')) return; var css = [ '#__ncme_panel *{box-sizing:border-box;margin:0;padding:0;font-family:"PingFang SC","Microsoft YaHei",sans-serif}', '#__ncme_panel{position:fixed;top:20px;left:20px;width:260px;background:#fff;border-radius:14px;box-shadow:0 6px 28px rgba(0,0,0,.10),0 2px 6px rgba(0,0,0,.05);z-index:999999;overflow:hidden;border:1px solid rgba(0,0,0,.06)}', '#__ncme_panel .__h{background:linear-gradient(135deg,#2563eb,#1d4ed8);padding:12px 14px;color:#fff;position:relative}', '#__ncme_panel .__h .__t{font-size:13px;font-weight:600;display:flex;align-items:center;gap:5px}', '#__ncme_panel .__h .__p{font-size:10px;opacity:.7;margin-top:2px}', '#__ncme_panel .__h .__tag{position:absolute;top:10px;right:14px;background:rgba(255,255,255,.9);border-radius:20px;padding:2px 7px;font-size:9px;color:#dc2626;font-weight:700}', '#__ncme_panel .__bd{padding:12px 14px}', '#__ncme_panel .__lb{font-size:10px;color:#999;margin-bottom:6px;font-weight:500}', '#__ncme_panel .__sp{display:flex;gap:5px;margin-bottom:10px}', '#__ncme_panel .__sb{flex:1;height:34px;border:1.5px solid #e5e7eb;border-radius:7px;background:#fff;cursor:pointer;font-size:13px;font-weight:600;color:#374151;transition:all .2s;position:relative;display:flex;align-items:center;justify-content:center}', '#__ncme_panel .__sb:hover{border-color:#2563eb;color:#2563eb}', '#__ncme_panel .__sb.on{border-color:#2563eb;background:#eff6ff;color:#2563eb}', '#__ncme_panel .__sb .__lk{display:none;position:absolute;top:-5px;right:-5px;width:14px;height:14px;background:#f59e0b;border-radius:50%;font-size:8px;color:#fff;align-items:center;justify-content:center}', '#__ncme_panel .__sb.lk{opacity:.5;cursor:not-allowed}', '#__ncme_panel .__sb.lk .__lk{display:flex}', '#__ncme_panel .__ts{display:flex;gap:6px;margin-bottom:10px}', '#__ncme_panel .__tg{flex:1;display:flex;align-items:center;gap:4px;cursor:pointer;padding:5px 7px;border-radius:7px;border:1px solid #e5e7eb;background:#fff;transition:all .2s;font-size:10px;color:#666}', '#__ncme_panel .__tg:hover{border-color:#2563eb;color:#2563eb}', '#__ncme_panel .__tg.on{border-color:#16a34a;background:#f0fdf4;color:#16a34a}', '#__ncme_panel .__tg .__d{width:5px;height:5px;border-radius:50%;background:#ccc}', '#__ncme_panel .__tg.on .__d{background:#16a34a}', '#__ncme_panel .__pg{margin-bottom:10px}', '#__ncme_panel .__pgbar{height:3px;background:#e5e7eb;border-radius:2px;overflow:hidden}', '#__ncme_panel .__pgf{height:100%;background:linear-gradient(90deg,#2563eb,#1d4ed8);border-radius:2px;transition:width .8s ease;width:0%}', '#__ncme_panel .__pgt{font-size:10px;color:#999;margin-top:3px;text-align:right}', '#__ncme_panel .__note{padding:8px 14px;background:#f9fafb;border-top:1px solid #f0f0f0;font-size:10px;color:#333;text-align:center;line-height:1.5}', '#__ncme_toast{position:fixed;top:20px;left:300px;background:rgba(0,0,0,.72);color:#fff;padding:7px 14px;border-radius:7px;font-size:12px;z-index:9999999;pointer-events:none;transition:opacity .3s;display:none}', '#__ncme_toast.show{display:block;opacity:1}', '#__ncme_toast.hide{opacity:0}' ].join(''); var styleEl = document.createElement('style'); styleEl.textContent = css; document.head.appendChild(styleEl); var speedBtnsHtml = CONFIG.speeds.map(function (v) { return ''; }).join(''); var panel = document.createElement('div'); panel.id = '__ncme_panel'; panel.innerHTML = [ '