// ==UserScript== // @name WE Learn 课件资源助手 // @namespace https://cxbank.icu/welearn-tool // @version 1.0.1 // @description 解析当前课件页的音频/视频/PDF 资源并一键下载,附带学习时长统计。只在主页面工作,不需要进入课件 iframe。 // @author cx // @match *://welearn.sflep.com/* // @match *://course.sflep.com/* // @match *://wetest.sflep.com/* // @connect centercourseware.sflep.com // @connect sflep.com // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-idle // ==/UserScript== (function () { 'use strict'; var BUILD = '1.0.1'; var LS = { study: 'wlt_study_', pos: 'wlt_panel_pos' }; var S = { resources: [], busy: false, log: [], timer: null, studySec: 0, }; /* ---------------- 工具 ---------------- */ function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); } function qsa(sel, root) { try { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); } catch (e) { return []; } } function todayKey() { return LS.study + new Date().toISOString().slice(0, 10); } function log(t, kind) { var tm = new Date().toLocaleTimeString('zh-CN', { hour12: false }); S.log.push({ t: t, kind: kind || 'info', tm: tm }); if (S.log.length > 80) S.log.shift(); renderLog(); } /* ---------------- 找课件 iframe ---------------- */ function coursewareFrame() { var list = qsa('iframe'); for (var i = 0; i < list.length; i++) { var src = list[i].getAttribute('src') || ''; if (src.indexOf('centercourseware') >= 0) return src; } return ''; } function parseFrame(src) { try { var u = new URL(src, location.href); var m = u.pathname.match(/^\/([^/]+)\/([^/]+)\/([^/]+)$/); return { href: u.href, course: m ? decodeURIComponent(m[1]) : '', unit: m ? m[2] : '', page: m ? m[3] : '', origin: u.origin, query: u.search, }; } catch (e) { return null; } } /* ---------------- 拉取课件 HTML(跨域用 GM 请求) ---------------- */ function gmGet(url, type) { return new Promise(function (resolve, reject) { try { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: type || 'text', timeout: 30000, onload: function (r) { if (r.status >= 200 && r.status < 300) resolve(r.response); else reject(new Error('HTTP ' + r.status)); }, onerror: function () { reject(new Error('网络错误')); }, ontimeout: function () { reject(new Error('超时')); }, }); } catch (e) { reject(e); } }); } /* ---------------- 解析资源 ---------------- */ var EXT_RE = /\.(mp3|mp4|m4a|wav|ogg|pdf|docx?|pptx?|xlsx?|zip|png|jpe?g|gif|svg)$/i; function extractResources(html, base) { var out = []; var seen = {}; var re = /(?:src|href)\s*=\s*["']([^"']+)["']/gi; var m; while ((m = re.exec(html))) { var raw = m[1]; if (!EXT_RE.test(raw)) continue; var abs; try { abs = new URL(raw, base).href; } catch (e) { continue; } /* 过滤掉界面音效 / 样式脚本里的图 */ var low = abs.toLowerCase(); if (low.indexOf('/resource/') >= 0) continue; if (/(^|\/)(t|f|empty|click|correct|wrong)\.mp3$/.test(low)) continue; if (seen[abs]) continue; seen[abs] = 1; out.push({ url: abs, name: decodeURIComponent(abs.split('/').pop().split('?')[0]), ext: (abs.match(EXT_RE) || [, ''])[1].toLowerCase(), }); } return out; } async function scan() { if (S.busy) return; var src = coursewareFrame(); if (!src) { log('当前页面没有课件,先点「目录」进入具体一节', 'warn'); return; } var info = parseFrame(src); if (!info) { log('课件地址解析失败', 'warn'); return; } S.busy = true; render(); log('正在解析:' + info.unit + ' / ' + info.page); try { var html = await gmGet(info.href, 'text'); var list = extractResources(html, info.href); /* 同一单元的其它页面也可以顺带扫一下 */ S.resources = list; log('找到 ' + list.length + ' 个资源', 'ok'); render(); } catch (e) { log('解析失败:' + e.message + '(可尝试直接打开课件地址)', 'warn'); } finally { S.busy = false; render(); } } /* ---------------- 下载 ---------------- */ function saveBlob(blob, name) { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = name || 'download'; document.body.appendChild(a); a.click(); setTimeout(function () { URL.revokeObjectURL(url); a.remove(); }, 5000); } async function downloadOne(item) { try { log('下载中:' + item.name); var blob = await gmGet(item.url, 'blob'); saveBlob(blob instanceof Blob ? blob : new Blob([blob]), item.name); log('完成:' + item.name, 'ok'); } catch (e) { log('失败:' + item.name + '(' + e.message + ')', 'warn'); } } async function downloadAll() { var audio = S.resources.filter(function (r) { return /^(mp3|m4a|wav|ogg|mp4)$/.test(r.ext); }); if (!audio.length) { log('没有音频/视频可下载', 'warn'); return; } log('开始批量下载 ' + audio.length + ' 个文件'); for (var i = 0; i < audio.length; i++) { await downloadOne(audio[i]); await sleep(600); } log('批量下载结束', 'ok'); } /* ---------------- 学习时长 ---------------- */ function loadStudy() { try { return Number(localStorage.getItem(todayKey()) || 0) || 0; } catch (e) { return 0; } } function saveStudy(v) { try { localStorage.setItem(todayKey(), String(v)); } catch (e) {} } function fmtDur(sec) { var h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60; return (h ? h + ' 时 ' : '') + m + ' 分 ' + s + ' 秒'; } function startTimer() { S.studySec = loadStudy(); S.timer = setInterval(function () { if (document.visibilityState === 'hidden') return; S.studySec++; if (S.studySec % 5 === 0) saveStudy(S.studySec); var el = document.getElementById('wlt-study'); if (el) el.textContent = fmtDur(S.studySec); }, 1000); } /* ---------------- 面板 ---------------- */ var root = null; var CSS = [ ':host{all:initial}', '.wrap{position:fixed;right:14px;top:14px;width:330px;z-index:2147483000;', 'background:rgba(255,255,255,.94);backdrop-filter:blur(18px);border:.5px solid rgba(60,60,67,.14);', 'border-radius:14px;box-shadow:0 10px 34px rgba(0,0,0,.2);', 'font:13px/1.6 -apple-system,"Segoe UI",sans-serif;color:#1c1c1e;overflow:hidden}', '.hd{padding:10px 12px;border-bottom:.5px solid rgba(60,60,67,.12);display:flex;align-items:center;gap:7px}', '.ttl{flex:1;font-weight:700;font-size:12.5px}', '.ver{color:#8e8e93;font-weight:500;font-size:11px}', '.bd{padding:10px 12px 12px}', '.row{display:flex;justify-content:space-between;font-size:11.5px;color:#8e8e93;margin-bottom:8px}', '.row b{color:#1c1c1e}', '.btns{display:flex;gap:8px;margin:10px 0}', '.btn{flex:1;border:none;border-radius:9px;padding:8px 0;font:600 12.5px inherit;cursor:pointer}', '.b1{background:#007aff;color:#fff}', '.b2{background:#34c759;color:#fff}', '.list{max-height:190px;overflow:auto;border:.5px solid rgba(60,60,67,.12);border-radius:9px}', '.item{display:flex;align-items:center;gap:8px;padding:7px 9px;border-bottom:.5px solid rgba(60,60,67,.08)}', '.item:last-child{border-bottom:none}', '.item .n{flex:1;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', '.item .x{border:none;background:#f2f2f7;border-radius:7px;padding:4px 9px;font:600 11px inherit;cursor:pointer}', '.log{max-height:120px;overflow:auto;background:#f7f7fa;border-radius:9px;padding:7px 9px;', 'font:11.5px/1.6 ui-monospace,Consolas,monospace;color:#3a3a3c;margin-top:10px}', '.log .ok{color:#248a3d}.log .warn{color:#c77700}', '.qq{margin-top:10px;text-align:center;font-size:11.5px;color:#007aff;background:rgba(0,122,255,.08);', 'border-radius:9px;padding:6px}', '.empty{color:#8e8e93;font-size:11.5px;text-align:center;padding:14px}', ].join(''); function el(id) { try { return root ? root.getElementById(id) : null; } catch (e) { return null; } } function build() { var host = document.createElement('div'); host.id = 'wlt-host'; root = host.attachShadow({ mode: 'open' }); var st = document.createElement('style'); st.textContent = CSS; root.appendChild(st); var wrap = document.createElement('div'); wrap.className = 'wrap'; wrap.innerHTML = '
课件资源助手 ' + BUILD + '
' + '
' + '
当前课件-
' + '
今日学习-
' + '
' + '' + '' + '
' + '
点上面「解析本页资源」
' + '
' + '
有问题加 QQ 群:228460464
' + '
'; root.appendChild(wrap); document.body.appendChild(host); /* 拖动 */ var drag = null; root.querySelector('.hd').addEventListener('mousedown', function (e) { drag = { x: e.clientX, y: e.clientY, r: wrap.getBoundingClientRect() }; e.preventDefault(); }); document.addEventListener('mousemove', function (e) { if (!drag) return; wrap.style.left = Math.max(0, drag.r.left + e.clientX - drag.x) + 'px'; wrap.style.top = Math.max(0, drag.r.top + e.clientY - drag.y) + 'px'; wrap.style.right = 'auto'; }); document.addEventListener('mouseup', function () { drag = null; }); root.getElementById('bScan').onclick = function () { scan(); }; root.getElementById('bAll').onclick = function () { downloadAll(); }; render(); } function render() { try { var src = coursewareFrame(); var info = src ? parseFrame(src) : null; var cur = el('cur'); if (cur) cur.textContent = info ? (info.unit + ' / ' + info.page) : '未进入课件'; var st = el('wlt-study'); if (st) st.textContent = fmtDur(S.studySec); var list = el('list'); if (!list) return; if (!S.resources.length) { list.innerHTML = '
' + (S.busy ? '解析中…' : '点上面「解析本页资源」') + '
'; } else { list.innerHTML = ''; S.resources.forEach(function (r) { var d = document.createElement('div'); d.className = 'item'; var n = document.createElement('div'); n.className = 'n'; n.textContent = r.name; n.title = r.url; var b = document.createElement('button'); b.className = 'x'; b.textContent = '下载'; b.onclick = function () { downloadOne(r); }; d.appendChild(n); d.appendChild(b); list.appendChild(d); }); } } catch (e) {} } function renderLog() { try { var box = el('log'); if (!box) return; box.innerHTML = S.log.slice(-30).map(function (x) { return '
' + x.tm + ' ' + String(x.t).replace(/[<>&]/g, function (c) { return { '<': '<', '>': '>', '&': '&' }[c]; }) + '
'; }).join(''); box.scrollTop = box.scrollHeight; } catch (e) {} } /* ---------------- 启动 ---------------- */ function boot() { if (!document.body) { setTimeout(boot, 300); return; } if (document.getElementById('wlt-host')) return; try { build(); startTimer(); log('已加载 v' + BUILD); /* 课件切页时刷新显示 */ setInterval(function () { render(); }, 3000); window.addEventListener('hashchange', function () { S.resources = []; render(); }); } catch (e) { try { console.warn('[课件资源助手]', e); } catch (x) {} } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else setTimeout(boot, 500); window.__wlt = { state: S, scan: scan, downloadAll: downloadAll, resources: function () { return S.resources; } }; })();