// ==UserScript== // @name 上海开放大学/上开自动刷视频【可加速及自动下一页】 // @namespace 一心向善 // @description 上海开放大学相关视频自动学习自动看视频程序【可加速及自动下一页】 // @author hao // @version 1.1 // @match *://*.shou.org.cn/* // @icon https://www.google.com/s2/favicons?sz=64&domain=shou.org.cn // @grant none // @run-at document-start // @license GPL v2 // ==/UserScript== (function () { 'use strict'; const LEARN_ORIGIN = 'https://l.shou.org.cn'; const TARGET_SPEED = 1; const MEDIA_WAIT_LIMIT = 80; const MEDIA_WAIT_INTERVAL = 1000; const SWITCH_DELAY = 1500; const CATALOG_COLLECT_TIMEOUT = 20000; const COLLECT_MESSAGE = 'shkd-lite:catalog-collected'; const STORAGE_KEYS = { courseList: 'mustCourseCourseList', mustCourse: 'mustCourse', mustCourseStats: 'mustCourseStats', detectedAt: 'mustCourseLiteDetectedAt' }; let activeMedia = null; let switching = false; let collectingCatalogs = false; let currentSpeed = TARGET_SPEED; // 日志输出 function log(message, data) { if (data === undefined) { console.log(`[上海开放大学/上开自动刷视频【可加速及自动下一页】] ${message}`); } else { console.log(`上海开放大学/上开自动刷视频【可加速及自动下一页】] ${message}`, data); } } // 本地存储读写 function readJson(key, fallback) { try { const rawValue = localStorage.getItem(key); if (!rawValue) { return fallback; } return JSON.parse(rawValue); } catch (error) { return fallback; } } function writeJson(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (error) { log(`写入本地缓存失败: ${key}`); } } // URL解析 function parseUrl(url) { try { const urlObj = new URL(url || window.location.href, window.location.href); const params = {}; urlObj.searchParams.forEach((value, key) => { params[key] = value; }); if (urlObj.hash && urlObj.hash.includes('?')) { const hashParams = new URLSearchParams(urlObj.hash.split('?')[1]); hashParams.forEach((value, key) => { params[key] = value; }); } return { href: urlObj.href, path: urlObj.pathname, host: urlObj.hostname, params: params }; } catch (error) { return { href: window.location.href, path: window.location.pathname, host: window.location.hostname, params: {} }; } } function pickParam(params, names) { for (const name of names) { if (params[name]) { return params[name]; } } const lowerMap = {}; Object.keys(params).forEach((key) => { lowerMap[key.toLowerCase()] = params[key]; }); for (const name of names) { const value = lowerMap[String(name).toLowerCase()]; if (value) { return value; } } return ''; } // 获取课程ID function getCurrentCourseOpenId() { const params = parseUrl().params; const fromUrl = pickParam(params, ['courseOpenId', 'CourseOpenId', 'courseopenid', 'coursename']); if (fromUrl) { return fromUrl; } const hiddenInput = document.querySelector('input[name="courseOpenId"], input[name="courseopenid"]'); return hiddenInput ? String(hiddenInput.value || '').trim() : ''; } function getCurrentCellId() { const params = parseUrl().params; const fromUrl = pickParam(params, ['cellId', 'cellid']); if (fromUrl) { return fromUrl; } const hiddenInput = document.querySelector('input[name="cellId"], input[name="cellid"]'); return hiddenInput ? String(hiddenInput.value || '').trim() : ''; } // 课程列表管理 function getCourseList() { const list = readJson(STORAGE_KEYS.courseList, []); return Array.isArray(list) ? list.filter((item) => item && item.courseOpenId) : []; } function setCourseList(courseList) { if (!Array.isArray(courseList) || courseList.length === 0) { return; } const seen = new Set(); const normalized = courseList .map((item) => ({ courseOpenId: String(item.courseOpenId || item.courseId || '').trim(), courseName: String(item.courseName || item.name || item.title || item.courseOpenId || '').trim() })) .filter((item) => item.courseOpenId) .filter((item) => { if (seen.has(item.courseOpenId)) { return false; } seen.add(item.courseOpenId); return true; }); if (normalized.length > 0) { writeJson(STORAGE_KEYS.courseList, normalized); collectMissingCatalogs(normalized); } } function rememberCourse(courseOpenId, courseName) { if (!courseOpenId) { return; } const list = getCourseList(); const existed = list.find((item) => item.courseOpenId === courseOpenId); if (existed) { if (courseName && !existed.courseName) { existed.courseName = courseName; writeJson(STORAGE_KEYS.courseList, list); } return; } list.push({ courseOpenId: courseOpenId, courseName: courseName || courseOpenId }); writeJson(STORAGE_KEYS.courseList, list); } // 课程目录与状态 function getMustCourseMap() { const map = readJson(STORAGE_KEYS.mustCourse, {}); return map && typeof map === 'object' ? map : {}; } function getMustCourseStats() { const stats = readJson(STORAGE_KEYS.mustCourseStats, {}); return stats && typeof stats === 'object' ? stats : {}; } function setCourseItems(courseOpenId, courseName, items) { if (!courseOpenId || !Array.isArray(items)) { return; } const mustCourse = getMustCourseMap(); const previousItems = Array.isArray(mustCourse[courseOpenId]) ? mustCourse[courseOpenId] : []; const previousByCellId = new Map(previousItems.map((item) => [String(item.cellId || ''), item])); mustCourse[courseOpenId] = items.map((item) => { const previous = previousByCellId.get(String(item.cellId || '')) || {}; return Object.assign({}, item, { status: item.status || previous.status || '' }); }); writeJson(STORAGE_KEYS.mustCourse, mustCourse); const finished = mustCourse[courseOpenId].filter((item) => isDone(item.status)).length; const stats = getMustCourseStats(); stats[courseOpenId] = { courseOpenId: courseOpenId, courseName: courseName || courseOpenId, total: mustCourse[courseOpenId].length, finished: finished, pending: Math.max(mustCourse[courseOpenId].length - finished, 0), collected: true, collectedAt: new Date().toISOString() }; writeJson(STORAGE_KEYS.mustCourseStats, stats); writeJson(STORAGE_KEYS.detectedAt, new Date().toISOString()); rememberCourse(courseOpenId, courseName); } function markCourseItemDone(courseOpenId, cellId) { if (!courseOpenId || !cellId) { return; } const mustCourse = getMustCourseMap(); const items = Array.isArray(mustCourse[courseOpenId]) ? mustCourse[courseOpenId] : []; const item = items.find((entry) => String(entry.cellId || '') === String(cellId)); if (!item) { return; } item.status = '已完成'; writeJson(STORAGE_KEYS.mustCourse, mustCourse); const stats = getMustCourseStats(); const finished = items.filter((entry) => isDone(entry.status)).length; stats[courseOpenId] = Object.assign({}, stats[courseOpenId], { courseOpenId: courseOpenId, courseName: (stats[courseOpenId] && stats[courseOpenId].courseName) || courseOpenId, total: items.length, finished: finished, pending: Math.max(items.length - finished, 0), collected: true }); writeJson(STORAGE_KEYS.mustCourseStats, stats); } // URL构建 function buildDirectoryUrl(courseOpenId) { const params = new URLSearchParams({ courseOpenId: courseOpenId, minorCourseOpenId: courseOpenId }); return `${LEARN_ORIGIN}/study/directory.aspx?${params.toString()}`; } function buildCatalogCollectUrl(courseOpenId) { const params = new URLSearchParams({ courseOpenId: courseOpenId, minorCourseOpenId: courseOpenId, shkd_collect: '1' }); return `${LEARN_ORIGIN}/study/learnCatalogNew.aspx?${params.toString()}`; } // 状态判断 function isDone(status) { const text = String(status || '').trim(); return text.includes('已完成') || text.includes('已学习') || text.includes('完成'); } function isMediaItem(item) { const text = `${item && item.title || ''} ${item && item.type || ''}`.toLowerCase(); return /视频|音频|mp4|mp3|m3u8|video|audio/.test(text); } // 查找下一个视频 function getNextMediaItemInCourse(courseOpenId, currentCellId) { const items = getMustCourseMap()[courseOpenId]; if (!Array.isArray(items) || items.length === 0) { return null; } let startIndex = 0; if (currentCellId) { const currentIndex = items.findIndex((item) => String(item.cellId || '') === String(currentCellId)); if (currentIndex >= 0) { startIndex = currentIndex + 1; } } for (let index = startIndex; index < items.length; index += 1) { const item = items[index]; if (isMediaItem(item) && !isDone(item.status)) { return { courseOpenId: courseOpenId, item: item }; } } return null; } function getNextMediaItemInNextCourse(currentCourseOpenId) { const courseList = getCourseList(); if (courseList.length === 0) { return null; } const startIndex = Math.max(courseList.findIndex((item) => item.courseOpenId === currentCourseOpenId), -1) + 1; for (let index = startIndex; index < courseList.length; index += 1) { const course = courseList[index]; const nextItem = getNextMediaItemInCourse(course.courseOpenId, ''); if (nextItem) { return nextItem; } } return null; } function getItemUrl(nextItem) { if (!nextItem) { return ''; } if (nextItem.item && nextItem.item.url) { return nextItem.item.url; } return buildDirectoryUrl(nextItem.courseOpenId); } // 自动切课 function switchToNextMedia() { const courseOpenId = getCurrentCourseOpenId(); const cellId = getCurrentCellId(); markCourseItemDone(courseOpenId, cellId); const nextInCourse = getNextMediaItemInCourse(courseOpenId, cellId); const nextInNextCourse = nextInCourse || getNextMediaItemInNextCourse(courseOpenId); const nextUrl = getItemUrl(nextInNextCourse); if (!nextUrl) { log('没有找到下一个未完成的视频课时'); return; } log('切换到下一个视频课时', nextUrl); setTimeout(() => { window.location.href = nextUrl; }, SWITCH_DELAY); } // 查找视频元素 function findPlayableMedia() { return document.getElementById('myplayer_html5_api') || document.getElementById('player_html5_api') || document.querySelector('video, audio'); } // 安全播放 function safePlay(media) { if (!media || typeof media.play !== 'function') { return; } try { const promise = media.play(); if (promise && typeof promise.catch === 'function') { promise.catch(() => {}); } } catch (error) {} } // 应用倍速(支持自定义) function applySpeed(media) { if (!media) return false; try { media.muted = true; media.defaultPlaybackRate = currentSpeed; media.playbackRate = currentSpeed; } catch (error) {} try { const speedItem = document.querySelector(`.dplayer-setting-speed-item[data-speed="${currentSpeed}"]`); if (speedItem) { speedItem.dispatchEvent(new MouseEvent('click', { bubbles: true })); media.playbackRate = currentSpeed; } } catch (error) {} try { if (typeof window.jwplayer === 'function') { const player = window.jwplayer(); if (player && typeof player.setPlaybackRate === 'function') { player.setPlaybackRate(currentSpeed); } } } catch (error) {} return Number(media.playbackRate || 0).toFixed(1) === currentSpeed.toFixed(1); } // 绑定视频事件 function bindMedia(media) { if (!media || activeMedia === media) { return; } activeMedia = media; switching = false; applySpeed(media); safePlay(media); const keepSpeed = () => { if (Number(media.playbackRate || 0).toFixed(1) !== currentSpeed.toFixed(1)) { applySpeed(media); } }; const finish = () => { if (switching) return; switching = true; switchToNextMedia(); }; media.addEventListener('loadedmetadata', () => { applySpeed(media); safePlay(media); }); media.addEventListener('play', keepSpeed); media.addEventListener('ratechange', keepSpeed); media.addEventListener('ended', finish, { once: true }); media.addEventListener('timeupdate', () => { if (!Number.isFinite(media.duration) || media.duration <= 0) return; if (media.currentTime >= media.duration - 2) finish(); }); setTimeout(() => applySpeed(media), 300); setTimeout(() => applySpeed(media), 1000); setInterval(() => { if (activeMedia === media && !media.ended) { keepSpeed(); if (media.paused) safePlay(media); } }, 5000); createSpeedPanel(); // 创建倍速控制面板 log('已启用视频倍速自动播放 + 自定义调速'); } // ===================== 【新版】悬浮控制面板 ===================== function createSpeedPanel() { if (document.getElementById('shkd-speed-panel')) return; const panel = document.createElement('div'); panel.id = 'shkd-speed-panel'; panel.style.cssText = ` position: fixed; top: 120px; right: 20px; z-index: 99999; width: 290px; padding: 12px 15px; background: #fff; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); font-family: "Microsoft YaHei", sans-serif; font-size: 14px; user-select: none; `; const title = document.createElement('div'); title.innerText = '上海开放控制面板'; title.style.cssText = 'font-weight: bold; margin-bottom: 10px; color: #222; text-align: center;'; const speedDisplay = document.createElement('div'); speedDisplay.innerText = `当前倍速:${currentSpeed.toFixed(1)}x`; speedDisplay.style.cssText = 'text-align: center; margin-bottom: 8px; font-size: 16px; color: #1677ff;'; const slider = document.createElement('input'); slider.type = 'range'; slider.min = '0.5'; slider.max = '4.0'; slider.step = '0.1'; slider.value = currentSpeed; slider.style.cssText = 'width: 100%; height: 6px; cursor: pointer;'; slider.addEventListener('input', () => { currentSpeed = parseFloat(slider.value); speedDisplay.innerText = `当前倍速:${currentSpeed.toFixed(1)}x`; if (activeMedia) applySpeed(activeMedia); }); // 暂停/播放按钮 const pauseBtn = document.createElement('button'); pauseBtn.innerText = '暂停 / 播放'; pauseBtn.style.cssText = ` width: 100%; padding: 6px 0; margin: 10px 0; background: #1677ff; color: #fff; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; `; pauseBtn.addEventListener('click', () => { if (!activeMedia) return; if (activeMedia.paused) { safePlay(activeMedia); } else { activeMedia.pause(); } }); // 底部说明文字(已加粗第4条) const info = document.createElement('div'); info.style.cssText = 'margin-top:12px; font-size:12px; line-height:1.5; color:#555; white-space:pre-line;'; info.innerHTML = `基础功能: 1、播放完毕自动下一页 2、倍数可自由调节,建议最大调节到2倍即可; 3、作业及线上实验实践、文件题、主观题、简答题等进群:949193546联系群主 4、散单、批量单、合作、各类教务处理进QQ一群:756253160 二群:949193546联系群主`; panel.appendChild(title); panel.appendChild(speedDisplay); panel.appendChild(slider); panel.appendChild(pauseBtn); panel.appendChild(info); document.body.appendChild(panel); } // 等待视频加载并绑定 function waitForMediaAndBind() { let retryCount = 0; const tryBind = () => { const media = findPlayableMedia(); if (media) { bindMedia(media); return true; } return false; }; if (tryBind()) return; const timer = setInterval(() => { retryCount += 1; if (tryBind() || retryCount >= MEDIA_WAIT_LIMIT) clearInterval(timer); }, MEDIA_WAIT_INTERVAL); } // 监听DOM变化 function observeMediaChanges() { const start = () => { if (!document.body) { setTimeout(start, 200); return; } const observer = new MutationObserver(() => { const media = findPlayableMedia(); if (media) bindMedia(media); }); observer.observe(document.body, { childList: true, subtree: true }); }; start(); } // 工具函数 function normalizeText(text) { return String(text || '').replace(/\s+/g, ' ').trim(); } function getCourseTitle(courseOpenId) { const titleEl = document.querySelector('.sh-course-title, h1, h2'); const title = normalizeText(titleEl ? titleEl.textContent : ''); return title || courseOpenId; } function getItemStatus(row, cellId) { const directStatus = row.querySelector('img.warningnew1, [title*="完成"], [title*="未完成"]'); if (directStatus && directStatus.getAttribute('title')) { return normalizeText(directStatus.getAttribute('title')); } if (cellId) { const label = document.getElementById(cellId); if (label) { const status = label.querySelector('img.warningnew1, [title*="完成"], [title*="未完成"]'); if (status && status.getAttribute('title')) return normalizeText(status.getAttribute('title')); const labelText = normalizeText(label.textContent); if (labelText.includes('完成')) return labelText; } } return ''; } function extractCellIdFromHref(href, row) { try { const url = new URL(href, window.location.href); return url.searchParams.get('cellId') || url.searchParams.get('cellid') || (row && row.getAttribute('data-id')) || ''; } catch (error) { const match = String(href || '').match(/[?&]cellId=([^&#]+)/i); return match ? decodeURIComponent(match[1]) : ((row && row.getAttribute('data-id')) || ''); } } // 采集课程目录 function collectCourseItemsFromPage() { const params = parseUrl().params; const courseOpenId = getCurrentCourseOpenId(); if (!courseOpenId) return []; const courseTitle = getCourseTitle(courseOpenId); const rows = Array.from(document.querySelectorAll('.sh-res, li.cell_info1')); if (rows.length === 0) { rememberCourse(courseOpenId, courseTitle); return []; } const hasRequiredMarker = rows.some((row) => normalizeText(row.textContent).includes('必看')); const collectAll = courseTitle.includes('(补)') || courseTitle.includes('(补)') || !hasRequiredMarker; const items = []; const seen = new Set(); rows.forEach((row) => { if (!collectAll && !normalizeText(row.textContent).includes('必看')) return; const link = row.querySelector('a[href*="directory.aspx"][href*="cellId"], a[href*="directory.aspx"][href*="cellid"]'); if (!link) return; const href = link.getAttribute('href') || ''; const fullUrl = new URL(href, window.location.href).toString(); const cellId = extractCellIdFromHref(fullUrl, row); if (!cellId || seen.has(cellId)) return; seen.add(cellId); const typeEl = link.querySelector('.sh-res-b') || row.querySelector('.sh-res-b'); const type = normalizeText(typeEl ? typeEl.textContent : ''); const rawTitle = normalizeText(link.getAttribute('title') || link.textContent); const title = normalizeText(rawTitle.replace(type, '')); const item = { title, url: fullUrl, cellId, status: getItemStatus(row, cellId), type }; if (isMediaItem(item)) items.push(item); }); setCourseItems(courseOpenId, courseTitle, items); if (params.shkd_collect === '1') { window.parent.postMessage({ type: COLLECT_MESSAGE, courseOpenId, total: items.length }, '*'); } log(`已缓存课程视频目录: ${courseTitle} (${items.length} 项)`); return items; } // 解析课程列表 function extractCourseListFromResponse(responseText) { let response; try { response = typeof responseText === 'string' ? JSON.parse(responseText) : responseText; } catch (error) { return []; } const candidates = [response?.result, response?.data, response?.rows, response]; const list = candidates.find((v) => Array.isArray(v)); if (!list) return []; return list.map(item => ({ courseOpenId: item?.courseOpenId || item?.courseopenid || item?.courseId || item?.id, courseName: item?.courseName || item?.name || item?.title })).filter(item => item.courseOpenId); } function handlePossibleCourseList(url, responseText) { if (!String(url || '').includes('learning-course-list')) return; const courseList = extractCourseListFromResponse(responseText); if (courseList.length > 0) { log(`已缓存课程列表: ${courseList.length} 门`); setCourseList(courseList); } } // 劫持请求获取课程列表 function hookCourseListRequests() { const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url) { this.__shkdLiteUrl = url; this.addEventListener('load', function () { handlePossibleCourseList(this.__shkdLiteUrl, this.responseText); }); return originalOpen.apply(this, arguments); }; if (typeof window.fetch === 'function') { const originalFetch = window.fetch; window.fetch = function (input, init) { const requestUrl = typeof input === 'string' ? input : (input?.url); return originalFetch.apply(this, arguments).then(response => { if (String(requestUrl || '').includes('learning-course-list')) { response.clone().text().then(text => handlePossibleCourseList(requestUrl, text)).catch(() => {}); } return response; }); }; } } // 后台采集目录 function collectMissingCatalogs(courseList) { if (collectingCatalogs || !Array.isArray(courseList) || courseList.length === 0) return; const mustCourse = getMustCourseMap(); const pendingCourses = courseList.filter(course => !Array.isArray(mustCourse[course.courseOpenId]) || mustCourse[course.courseOpenId].length === 0); if (pendingCourses.length === 0) return; collectingCatalogs = true; let index = 0; let currentIframe = null; let timeoutId = null; const cleanup = () => { if (timeoutId) clearTimeout(timeoutId); if (currentIframe) currentIframe.remove(); timeoutId = null; currentIframe = null; }; const finish = () => { cleanup(); window.removeEventListener('message', onMessage); collectingCatalogs = false; log('课程目录缓存完成'); }; const next = () => { cleanup(); if (index >= pendingCourses.length) { finish(); return; } const course = pendingCourses[index]; currentIframe = document.createElement('iframe'); currentIframe.src = buildCatalogCollectUrl(course.courseOpenId); currentIframe.style.cssText = 'position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;'; document.documentElement.appendChild(currentIframe); timeoutId = setTimeout(() => { index++; next(); }, CATALOG_COLLECT_TIMEOUT); }; const onMessage = e => { const msg = e.data || {}; if (msg.type !== COLLECT_MESSAGE) return; if (!pendingCourses[index] || pendingCourses[index].courseOpenId !== msg.courseOpenId) return; index++; next(); }; window.addEventListener('message', onMessage); next(); } // 页面加载完成 function ready(callback) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback, { once: true }); } else { callback(); } } // 初始化页面 function initPage() { const detail = parseUrl(); if (detail.host !== 'l.shou.org.cn' && !detail.host.endsWith('.shou.org.cn')) return; if (detail.path === '/study/directory.aspx' || detail.path === '/study/learnCatalogNew.aspx') { setTimeout(() => collectCourseItemsFromPage(), 1200); } if (detail.path === '/study/directory.aspx' || detail.path === '/study/play.aspx') { waitForMediaAndBind(); observeMediaChanges(); } } hookCourseListRequests(); ready(initPage); })();