// ==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 = '