// ==UserScript== // @name 湖师大公需课-自动播放 // @namespace http://tampermonkey.net/local/hunnu-auto // @version 0.0.1 // @description 湖师大公需课-自动播放 v0.0.1 母版:雾蓝玻璃风 + v3.6.4 按钮切换逻辑 // @author 天才co // @match https://zjjxjy.hunnu.edu.cn/* // @match https://jyjd.hunnu.edu.cn/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; const TAG = '[湖师大公需课-自动播放]'; const AUTHOR = '天才co'; const log = (...a) => console.log(TAG, ...a); // ============ 0. 状态存储(按 trainPlanCourseId 隔离) ============ function getStateKey() { const t = location.search.match(/[?&]trainPlanCourseId=([^&]+)/); if (t) return 'hnav_state_v001_' + t[1]; const c = location.search.match(/[?&]courseId=([^&]+)/); if (c) return 'hnav_state_v001_' + c[1]; return 'hnav_state_v001_default'; } const LS_KEY = getStateKey(); const state = { userStopped: true, muted: false, autoNext: false, rate: 1, endedHandled: false, panelPos: null, }; function saveState() { try { localStorage.setItem(LS_KEY, JSON.stringify({ userStopped: state.userStopped, muted: state.muted, autoNext: state.autoNext, rate: state.rate, panelPos: state.panelPos, })); } catch (e) {} } function loadState() { try { const s = JSON.parse(localStorage.getItem(LS_KEY) || '{}'); if (typeof s.userStopped === 'boolean') state.userStopped = s.userStopped; if (typeof s.muted === 'boolean') state.muted = s.muted; if (typeof s.autoNext === 'boolean') state.autoNext = s.autoNext; if (typeof s.rate === 'number' && s.rate > 0) state.rate = s.rate; if (s.panelPos && typeof s.panelPos.left === 'number') state.panelPos = s.panelPos; log('已恢复状态 [' + LS_KEY + ']:', JSON.stringify({ muted: state.muted, autoNext: state.autoNext, rate: state.rate, userStopped: state.userStopped })); } catch (e) {} } loadState(); // ============ 1. 定时器注册表 ============ const timers = new Set(); function regTimer(id) { timers.add(id); return id; } function unregTimer(id) { if (timers.has(id)) { clearInterval(id); clearTimeout(id); timers.delete(id); } } function clearAllTimers() { let count = 0; timers.forEach(id => { try { clearInterval(id); clearTimeout(id); count++; } catch(e){} }); timers.clear(); return count; } // ============ 2. 加固版 forceMute ============ const origPlay = HTMLVideoElement.prototype.play; function forceMute(v) { if (!v) return; try { if (state.muted) { v.muted = true; v.volume = 0; } else { if (v.volume === 0 && v.muted) { v.muted = false; v.volume = 1; } } if (v.playbackRate !== state.rate) v.playbackRate = state.rate; } catch (e) {} try { if (window.player && typeof window.player.setMute === 'function') { window.player.setMute(state.muted); } } catch (e) {} } HTMLVideoElement.prototype.play = function () { forceMute(this); return origPlay.apply(this, arguments).catch(e => { console.debug(TAG, 'autoplay blocked:', e && e.name); }); }; // ============ 3. 工具 ============ const sleep = ms => new Promise(r => setTimeout(r, ms)); function fmt(t) { if (!isFinite(t) || t < 0) return '00:00'; const m = Math.floor(t / 60), s = Math.floor(t % 60); return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); } function closeDialogs() { document.querySelectorAll('.jbox-button').forEach(btn => { const text = (btn.innerText || '').trim(); if (/确定|确认|继续|知道了|我知道了|保存/.test(text) && !/取消|关闭/.test(text)) { btn.click(); } }); document.querySelectorAll('.el-message-box__wrapper').forEach(box => { if (box.offsetParent === null) return; const btn = box.querySelector('.el-button--primary'); if (btn) btn.click(); }); } // ============ 4. 播放控制 ============ async function ensurePlaying(video) { if (!video || state.userStopped) return; forceMute(video); if (video.paused) { try { await video.play(); } catch (e) { const btn = document.querySelector('.jw-icon-playback'); if (btn) btn.click(); } } } // ============ 5. 判断与查找 ============ function isLessonDone(el) { return !!(el && el.classList && el.classList.contains('playlist__lesson--done')); } function getAllLessons() { return Array.from(document.querySelectorAll('.playlist__lesson')); } function getActiveLesson() { return document.querySelector('.playlist__lesson--active'); } function getFirstUnfinished() { return getAllLessons().find(el => !isLessonDone(el)) || null; } // ============ 6. 跳转 ============ async function gotoNextUnfinished() { if (state.endedHandled) return; state.endedHandled = true; const next = getFirstUnfinished(); if (!next) { log('✅ 所有小节都已完成'); return; } const nextHref = next.getAttribute('href'); if (!nextHref) return; const currentUrl = location.pathname + location.search; if (nextHref === currentUrl) { log('当前就是第一个未完成,继续播放'); state.endedHandled = false; return; } const title = next.querySelector('.playlist__name'); log('跳到第一个未完成:', title ? title.getAttribute('title') : nextHref); await sleep(1200); closeDialogs(); const alreadyTimeEl = document.getElementById('alreadyTime'); if (!alreadyTimeEl) { console.warn(TAG, '⚠️ 未找到 #alreadyTime,固定等待3秒跳转'); const t = setTimeout(() => { window.location.href = nextHref; }, 3000); regTimer(t); return; } const oldTime = alreadyTimeEl.value; let waitCount = 0; const doJump = () => { const newTime = alreadyTimeEl.value; if (newTime !== oldTime || waitCount > 5) { log('进度已确认,执行跳转'); window.location.href = nextHref; } else { waitCount++; const t = setTimeout(doJump, 1000); regTimer(t); } }; const t = setTimeout(doJump, 1000); regTimer(t); } // ============ 7. 视频绑定 ============ function bindVideo(video) { if (!video || video.__hnav_bound) return; video.__hnav_bound = true; state.endedHandled = false; forceMute(video); video.addEventListener('ended', () => { log('视频结束(第一重)'); if (state.autoNext) gotoNextUnfinished(); }); const endedCheckId = setInterval(() => { if (!document.body.contains(video) || state.endedHandled) { unregTimer(endedCheckId); return; } if (video.duration && video.currentTime >= video.duration - 1.5) { log('时间兜底判定结束(第二重)'); unregTimer(endedCheckId); if (state.autoNext) gotoNextUnfinished(); } }, 3000); regTimer(endedCheckId); video.addEventListener('ratechange', () => { if (video.playbackRate !== state.rate && !state.userStopped) { video.playbackRate = state.rate; } }); video.addEventListener('volumechange', () => { if (state.muted && !video.muted) { video.muted = true; video.volume = 0; try { if (window.player && typeof window.player.setMute === 'function') { window.player.setMute(true); } } catch(e){} } }); log('已绑定新视频, 时长:', fmt(video.duration)); if (!state.userStopped) ensurePlaying(video); } // ============ 8. player.onComplete 链式调用 ============ let playerMonitorTimer = null; function bindPlayerComplete() { if (!window.player || typeof window.player.onComplete !== 'function') { const t = setTimeout(bindPlayerComplete, 2000); regTimer(t); return; } if (!window.player.__hnav_origOnComplete) { window.player.__hnav_origOnComplete = window.player.onComplete; } const origOnComplete = window.player.__hnav_origOnComplete; const ourHandler = function(event) { if (origOnComplete) { try { origOnComplete.call(window.player, event); } catch(e) {} } log('player.onComplete(第三重)'); if (state.autoNext && !state.endedHandled) { gotoNextUnfinished(); } }; window.player.onComplete = ourHandler; if (playerMonitorTimer) unregTimer(playerMonitorTimer); playerMonitorTimer = setInterval(() => { if (window.player && window.player.onComplete !== ourHandler) { log('onComplete 被覆盖,重新挂载'); window.player.onComplete = ourHandler; } }, 2500); regTimer(playerMonitorTimer); } // ============ 9. 观察与守护 ============ let mo = null; function observeVideos() { document.querySelectorAll('video').forEach(bindVideo); mo = new MutationObserver(muts => { muts.forEach(m => m.addedNodes.forEach(n => { if (n.nodeType !== 1) return; if (n.tagName === 'VIDEO') { bindVideo(n); return; } if (n.querySelectorAll) n.querySelectorAll('video').forEach(bindVideo); })); }); mo.observe(document.documentElement, { childList: true, subtree: true }); } let guardTimer = null; function startGuard() { if (guardTimer) unregTimer(guardTimer); guardTimer = setInterval(() => { if (state.userStopped) return; closeDialogs(); const v = document.querySelector('video'); if (v && v.paused && v.currentTime > 0 && !state.endedHandled) { ensurePlaying(v); } if (v && state.muted && (!v.muted || v.volume > 0)) { v.muted = true; v.volume = 0; } }, 2500); regTimer(guardTimer); } function stopAll() { const count = clearAllTimers(); if (mo) { try { mo.disconnect(); } catch(e){} log('MutationObserver 已断开'); } guardTimer = null; playerMonitorTimer = null; log(`已清理 ${count} 个定时器`); } // ============ 10. 启动流程 ============ async function startAutoPlay() { state.userStopped = false; state.endedHandled = false; saveState(); if (state.autoNext) { const active = getActiveLesson(); if (active && isLessonDone(active)) { log('🚀 当前视频已完成,跳到第一个未完成'); await gotoNextUnfinished(); return; } } log('开始播放当前小节'); bindPlayerComplete(); startGuard(); document.querySelectorAll('video').forEach(ensurePlaying); } // ============ 11. 控制面板:雾蓝玻璃风 + 深蓝标题 + 文字加深 ============ function buildPanel() { if (document.getElementById('hnav-panel')) return; const style = document.createElement('style'); style.textContent = ` /* ============================================ 湖师大公需课-自动播放 v0.0.1 骨架:v3.6.4 / 视觉:雾蓝玻璃(文字加深) ============================================ */ #hnav-panel { position: fixed; z-index: 2147483647; width: 268px; background: rgba(252, 253, 255, 0.94); backdrop-filter: blur(20px) saturate(1.6); -webkit-backdrop-filter: blur(20px) saturate(1.6); color: #2c4868; border-radius: 16px; font: 13px/1.5 "Microsoft YaHei", "PingFang SC", sans-serif; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.85), 0 8px 32px rgba(58, 90, 128, 0.22), 0 2px 8px rgba(58, 90, 128, 0.10); overflow: hidden; } /* ===== 标题栏:中深蓝渐变 ===== */ #hnav-panel .hd { display: flex; justify-content: space-between; align-items: center; padding: 11px 16px; background: linear-gradient(120deg, #4a8ac0 0%, #5b6ec0 100%); color: #ffffff; cursor: move; font-weight: 600; font-size: 13.5px; letter-spacing: 0.4px; text-shadow: 0 1px 2px rgba(20, 40, 70, 0.35); } #hnav-panel .bd { padding: 14px 16px 12px; } #hnav-panel .row { margin: 9px 0; display: flex; justify-content: space-between; align-items: center; gap: 8px; } /* 行标题:加深至 #2c4868 */ #hnav-panel .row > span:first-child { font-size: 12.5px; color: #2c4868; font-weight: 600; } /* 状态/进度文字:加深至 #2c4868 */ #hnav-panel .stat { font-size: 12px; color: #2c4868; text-align: right; font-weight: 600; letter-spacing: 0.2px; } /* ===== 按钮 ===== */ #hnav-panel button { background: #ffffff; color: #2c4868; border: 1px solid #cddcec; border-radius: 8px; padding: 5px 12px; cursor: pointer; font-size: 12px; font-family: inherit; font-weight: 500; transition: all 0.22s ease; } #hnav-panel button:hover { background: #eef6fd; border-color: #8bc0e0; color: #1a3a60; transform: translateY(-1px); } #hnav-panel button:active { transform: translateY(0); } #hnav-panel button.on { background: linear-gradient(135deg, #4a95d8 0%, #6b95e8 100%); color: #ffffff; border-color: transparent; box-shadow: 0 3px 10px rgba(74, 149, 216, 0.4); } /* ===== 开关 ===== */ #hnav-panel .switch { width: 38px; height: 21px; background: #cddbe8; border-radius: 11px; cursor: pointer; position: relative; flex: none; transition: all 0.25s ease; box-shadow: inset 0 1px 3px rgba(58, 90, 128, 0.15); } #hnav-panel .switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 17px; height: 17px; background: #ffffff; border-radius: 50%; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 1px 3px rgba(40, 70, 110, 0.28); } #hnav-panel .switch.on { background: linear-gradient(135deg, #4a95d8, #6b95e8); box-shadow: inset 0 1px 3px rgba(40, 70, 110, 0.15), 0 0 10px rgba(74, 149, 216, 0.5); } #hnav-panel .switch.on::after { left: 19px; box-shadow: 0 1px 4px rgba(40, 70, 110, 0.3); } /* ===== 总开关 ===== */ #hnav-panel .switch.master { width: 50px; height: 27px; border-radius: 14px; } #hnav-panel .switch.master::after { width: 23px; height: 23px; top: 2px; left: 2px; } #hnav-panel .switch.master.on { background: linear-gradient(135deg, #4a95d8, #7b8ce8); box-shadow: inset 0 1px 3px rgba(40, 70, 110, 0.15), 0 0 14px rgba(74, 149, 216, 0.6), 0 0 24px rgba(123, 140, 232, 0.4); } #hnav-panel .switch.master.on::after { left: 25px; } /* ===== 分隔线 ===== */ #hnav-panel .divider { border-top: 1px dashed #c8d6e5; margin: 12px 0 8px; } #hnav-panel .toggle { cursor: pointer; padding: 0 6px; font-weight: bold; } `; document.head.appendChild(style); const panel = document.createElement('div'); panel.id = 'hnav-panel'; panel.innerHTML = `
湖师大公需课-自动播放 v0.0.1
状态待机
进度--:--
当前节
未完成
倍速
静音开关
自动切集
一键自动静音播放
`; document.body.appendChild(panel); // 恢复 UI const muteSw = panel.querySelector('#hnav-mute'); const autoSw = panel.querySelector('#hnav-auto'); const allInOneSw = panel.querySelector('#hnav-allinone'); const startBtn = panel.querySelector('#hnav-start'); const stopBtn = panel.querySelector('#hnav-stop'); if (state.muted) muteSw.classList.add('on'); if (state.autoNext) autoSw.classList.add('on'); if (state.muted && state.autoNext) allInOneSw.classList.add('on'); panel.querySelectorAll('button[data-r]').forEach(btn => { if (parseFloat(btn.dataset.r) === state.rate) btn.classList.add('on'); }); if (!state.userStopped) startBtn.classList.add('on'); else stopBtn.classList.add('on'); if (state.panelPos) { panel.style.left = state.panelPos.left + 'px'; panel.style.top = state.panelPos.top + 'px'; } else { panel.style.right = '16px'; panel.style.bottom = '16px'; } panel.querySelector('.toggle').onclick = (e) => { const bd = panel.querySelector('.bd'); bd.style.display = bd.style.display === 'none' ? 'block' : 'none'; e.target.textContent = bd.style.display === 'none' ? '+' : '—'; }; // 拖拽 let dragging = false, sx, sy, ox, oy; const hd = panel.querySelector('.hd'); hd.addEventListener('mousedown', e => { dragging = true; sx = e.clientX; sy = e.clientY; const r = panel.getBoundingClientRect(); ox = r.left; oy = r.top; panel.style.left = ox + 'px'; panel.style.top = oy + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; e.preventDefault(); }); document.addEventListener('mousemove', e => { if (!dragging) return; let newLeft = ox + e.clientX - sx; let newTop = oy + e.clientY - sy; const w = panel.offsetWidth, h = panel.offsetHeight; const maxLeft = window.innerWidth - w; const maxTop = window.innerHeight - h; newLeft = Math.max(0, Math.min(newLeft, maxLeft)); newTop = Math.max(0, Math.min(newTop, maxTop)); panel.style.left = newLeft + 'px'; panel.style.top = newTop + 'px'; }); document.addEventListener('mouseup', () => { if (!dragging) return; dragging = false; const r = panel.getBoundingClientRect(); state.panelPos = { left: r.left, top: r.top }; saveState(); }); // 倍速 panel.querySelectorAll('button[data-r]').forEach(btn => { btn.onclick = () => { state.rate = parseFloat(btn.dataset.r); saveState(); const v = document.querySelector('video'); if (v) v.playbackRate = state.rate; panel.querySelectorAll('button[data-r]').forEach(b => b.classList.remove('on')); btn.classList.add('on'); }; }); function syncAllInOneUI() { allInOneSw.classList.toggle('on', state.muted && state.autoNext); } // ===== 独立开关 1:静音 ===== muteSw.onclick = () => { state.muted = !state.muted; saveState(); muteSw.classList.toggle('on', state.muted); document.querySelectorAll('video').forEach(v => { if (state.muted) { v.muted = true; v.volume = 0; } else { v.muted = false; v.volume = 1; } }); try { if (window.player && typeof window.player.setMute === 'function') { window.player.setMute(state.muted); } } catch(e){} syncAllInOneUI(); log('静音开关:', state.muted ? '开' : '关'); }; // ===== 独立开关 2:自动切集 ===== autoSw.onclick = async () => { state.autoNext = !state.autoNext; saveState(); autoSw.classList.toggle('on', state.autoNext); syncAllInOneUI(); log('自动切集:', state.autoNext ? '开' : '关'); if (state.autoNext && !state.userStopped) { const active = getActiveLesson(); if (active && isLessonDone(active)) { log('当前视频已完成,立即跳到第一个未完成'); state.endedHandled = false; await gotoNextUnfinished(); } } }; // ===== 总开关:一键自动静音播放 ===== allInOneSw.onclick = async () => { const turnOn = !(state.muted && state.autoNext); state.muted = turnOn; state.autoNext = turnOn; saveState(); muteSw.classList.toggle('on', state.muted); autoSw.classList.toggle('on', state.autoNext); allInOneSw.classList.toggle('on', turnOn); if (turnOn) { log('🚀 一键自动静音播放:开启(静音 ON + 自动切集 ON)'); document.querySelectorAll('video').forEach(v => { v.muted = true; v.volume = 0; }); try { if (window.player && typeof window.player.setMute === 'function') { window.player.setMute(true); } } catch(e){} if (!state.userStopped) { const active = getActiveLesson(); if (active && isLessonDone(active)) { log('当前视频已完成,立即跳到第一个未完成'); state.endedHandled = false; await gotoNextUnfinished(); } } } else { log('一键自动静音播放:关闭(静音 OFF + 自动切集 OFF)'); document.querySelectorAll('video').forEach(v => { v.muted = false; v.volume = 1; }); try { if (window.player && typeof window.player.setMute === 'function') { window.player.setMute(false); } } catch(e){} } }; // ===== 开始 / 停止(v3.6.4 逻辑:简单 on / off) ===== startBtn.onclick = () => { startBtn.classList.add('on'); stopBtn.classList.remove('on'); startAutoPlay(); }; stopBtn.onclick = () => { state.userStopped = true; saveState(); stopBtn.classList.add('on'); startBtn.classList.remove('on'); stopAll(); const v = document.querySelector('video'); if (v) v.pause(); }; // 状态刷新 const statusTimer = setInterval(() => { const v = document.querySelector('video'); const st = panel.querySelector('#hnav-status'); if (state.userStopped) { st.textContent = '已停止'; st.style.color = '#b85f75'; } else if (v && !v.paused) { st.textContent = '播放中'; st.style.color = '#2a8a6e'; } else { st.textContent = '待机'; st.style.color = '#2c4868'; } if (v && isFinite(v.duration)) { panel.querySelector('#hnav-prog').textContent = fmt(v.currentTime) + ' / ' + fmt(v.duration); } const cur = document.querySelector('.playlist__lesson--active .playlist__name'); panel.querySelector('#hnav-cur').textContent = cur ? (cur.getAttribute('title') || cur.innerText).slice(0, 22) : '—'; const total = getAllLessons().length; const remain = getAllLessons().filter(el => !isLessonDone(el)).length; panel.querySelector('#hnav-remain').textContent = remain + ' / ' + total; syncAllInOneUI(); }, 1000); regTimer(statusTimer); } // ============ 12. 启动 ============ function boot() { log('boot 湖师大公需课-自动播放 v0.0.1 · 作者:' + AUTHOR + ' [' + LS_KEY + '] 恢复:', JSON.stringify({ muted: state.muted, autoNext: state.autoNext, rate: state.rate, userStopped: state.userStopped })); observeVideos(); buildPanel(); if (!state.userStopped) { log('检测到上次处于运行状态,自动续跑'); const t = setTimeout(() => startAutoPlay(), 3000); regTimer(t); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();