├── LICENSE ├── README.md └── overrides └── v.ustc.edu.cn └── assets ├── capture-443b2e25.js └── captureCourses-571fa455.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 TMYTiMidlY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # capture-courses 2 | 3 | ## 安装 4 | 在 [USTC 在线视频平台-直录播-录播课程](https://v.ustc.edu.cn/capture-course/)标签页打开浏览器开发者工具,使用 [Edge 的本地覆写](https://learn.microsoft.com/zh-cn/microsoft-edge/devtools-guide-chromium/javascript/overrides)或 [Chrome 的本地覆写](https://developer.chrome.com/docs/devtools/overrides?hl=zh-cn),将覆写文件夹设为本仓库根目录下的 overrides,然后即可访问所有已录制的课程。设置时有以下几点注意事项: 5 | 1. 设置覆写文件夹时应保持本仓库原本的目录结构: 6 |
7 | overrides(设置为覆写文件夹) 8 | └─v.ustc.edu.cn 9 | └─assets 10 | │ capture*.js 11 | │ captureCourses*.js 12 |13 | 2. 设置完成后,覆写文件夹的位置不可移动,否则需要在开发者工具内重新设置路径。本仓库更新后,直接将覆写文件夹内的文件更新即可。 14 | 3. 本地覆写需要开发者工具**始终处于打开状态**,打开以后,刷新网页即可生效。成功观看一个录播视频后,在后续不刷新网页的情况下,可以关闭开发者工具。 15 | 4. 在观看录播视频前,网站需要通过统一身份认证登录。 16 | 17 | ## 搜索 18 | 使用本工具后,**仅支持完整课程号搜索,如MATH1006.01**(在[USTC 教务处-全校开课查询](https://catalog.ustc.edu.cn/query/lesson)等平台可以搜索到课程号)。网站默认搜索本学期课程,在搜索框中输入如下格式中的任意一个,即可搜索往期课程(最早支持22春)。 19 | - MATH1006.01 23秋 20 | - MATH1006.01-23秋 21 | - MATH1006.01/23秋 22 | - 23秋 MATH1006.01 23 | - 23秋-MATH1006.01 24 | - 23秋/MATH1006.01 25 | 26 | ## 声明 27 | 本工具仅供校内学生学习使用,请勿将视频平台的内容私自上传至公共平台,这可能对任课老师造成非常严重的负面影响。教师手动授权后,无需使用本工具即可访问录播课程,具体方法如下(助教推荐使用): 28 | 1. 教师登录 https://home.v.ustc.edu.cn - 进入"我的课程"(左栏) - 进入具体课程 - 点击"发布" 29 | 2. 学生登录 https://home.v.ustc.edu.cn - 进入"我的课程"(左栏) - 进入具体课程 - 在"录播教材"中查看往期课程 30 | 31 | 由于一些老师反馈/课程内容流传到公共平台的原因,录课网站每半年左右就会做一次更新,导致工具不可用。虽然目前为止我们仍有应对手段,但是录课网站做一次根本性的修复并不难,请勿完全依赖本工具和录课网站,重要的内容请提前下载备份。 32 | 33 | ## 支持我们 34 | 目前,几乎所有研究生/本研同堂课程以及部分本科生课程会出现课程内容为空/不全/声音缺失/时长不完整的情况,我们对这种情况无能为力。这些问题与课程类别和教室的自动录课设置存在很大的关系。以下是一些可能的解决方法: 35 | - 向老师申请录课权限:以更正义的手段获取录课资源,但似乎仍无法解决教室硬件未录课的问题。 36 | - 右键将视频另存至本地:如果同时存在电脑录屏和教室录屏,且两视频时长不一致,录课平台可能会选择短的时长作为总时长。单独下载两个视频文件可以获得原始的完整内容。 37 | - 在课前打开教室电脑,并在课程结束后关闭:部分教室的自动录课设置要求电脑处于开机状态,如果提前关机,录课可能会提前终止。如果始终不开机而一直使用板书,也可能成功录制。 38 | 39 | 由于作者现在即将脱离本科生阶段,将不再持续使用本工具,如果对于上面的问题有更新的认识和更合适的解决方案,欢迎在 Issues 处提出。 -------------------------------------------------------------------------------- /overrides/v.ustc.edu.cn/assets/capture-443b2e25.js: -------------------------------------------------------------------------------- 1 | import { 2 | dF as v, 3 | di as N, 4 | fy as S, 5 | dl as P, 6 | a as L, 7 | k as E, 8 | u as R, 9 | a1 as k, 10 | A as w, 11 | bZ as p, 12 | fz as m, 13 | cb as d, 14 | fA as V, 15 | bT as y, 16 | } from "./index-8e80cd51.js"; 17 | import { g as D, o as M, q as U, r as W, s as $ } from "./capture-6b754c68.js"; 18 | import { i as T } from "./includes-7746ba81.js"; 19 | import { i as h } from "./_isIterateeCall-8408f4d1.js"; 20 | function B(r, l) { 21 | var n; 22 | return ( 23 | v(r, function (u, i, f) { 24 | return (n = l(u, i, f)), !n; 25 | }), 26 | !!n 27 | ); 28 | } 29 | function z(r, l, n) { 30 | var u = N(r) ? S : B; 31 | return n && h(r, l, n) && (l = void 0), u(r, P(l)); 32 | } 33 | const oe = () => { 34 | const { state: r } = L(D, { DOWNLOAD_REPLAY: "ALLOWALL" }); 35 | return { 36 | enableDownloadCapture: E(() => r.value.DOWNLOAD_REPLAY === "ALLOWALL"), 37 | }; 38 | }, 39 | te = (r) => { 40 | const { locale: l } = R(), 41 | { userInfo: n } = k(), 42 | u = w(!0), 43 | i = w(), 44 | f = w(), 45 | { 46 | getOrgSettingByName: A, 47 | getOrgSettingByNameJsonValue: O, 48 | getOrgSettingByNameNumberValue: b, 49 | } = V(); 50 | return { 51 | hasCapture: u, 52 | capture: i, 53 | capturePermission: f, 54 | initCapture: async (o, e = !1) => { 55 | (i.value = await M(r, "", e)), // modified: o to "" 56 | i.value && i.value.id 57 | ? ((u.value = !0), 58 | (f.value = await U(i.value.id)), 59 | (i.value.videos = i.value.lessonVideos)) 60 | : (u.value = !1); 61 | }, 62 | getPlayerVideos: (o) => 63 | o.videos 64 | ? o.videos.map((e) => ({ 65 | id: e.id, 66 | src: e.playUrl, 67 | type: e.playType, 68 | label: e.label.toLowerCase(), 69 | key: e.label.toLowerCase() + e.id, 70 | mute: e.mute, 71 | volume: e.mute ? 0 : 0.7, 72 | })) 73 | : [], 74 | getCaptionConfig: async (o, e, t, g = "") => { 75 | if (!e.caption.toggle) return { toggle: !1 }; 76 | const a = p(o.videos, { isBestAudio: !0 }); 77 | if (!a) 78 | return ( 79 | console.warn("No best audio source for generate caption"), 80 | { toggle: !1 } 81 | ); 82 | let s = ""; 83 | e.caption.create && (s = a.downloadUrl); 84 | const c = await b(m.CAPTION_PARAGRAPH_SPLIT_INTERVAL), 85 | C = `/incast/api/media-captions/${ 86 | o.code 87 | }/download?filename=${encodeURIComponent(o.name)}&org_code=${ 88 | o.orgCode 89 | }`; 90 | return { 91 | toggle: !0, 92 | mediaId: o.code, 93 | server: t, 94 | source: s, 95 | organization: o.orgCode, 96 | callback: g, 97 | downloadUrl: C, 98 | permissions: e.caption, 99 | paragraphSplitInterval: c, 100 | enableTranslate: e.caption.enableTranslate, 101 | }; 102 | }, 103 | getTopicConfig: async (o, e, t, g = "") => { 104 | const a = p(o.videos, { isBestAudio: !0 }), 105 | s = p(o.videos, { label: "ENCODER" }); 106 | if ((!s && !a) || !e.topic.toggle) 107 | return ( 108 | console.warn("no best audio and encoder video for generate topic"), 109 | { toggle: !1 } 110 | ); 111 | const c = (await A(m.TOPIC_SOURCE)) || "ASR"; 112 | let C = null, 113 | I = null; 114 | return ( 115 | a && T(c, "ASR") && (C = a.downloadUrl), 116 | s && T(c, "OCR") && (I = s.downloadUrl), 117 | { 118 | toggle: e.topic.toggle, 119 | mediaId: o.code, 120 | server: t, 121 | callback: g, 122 | upsertCallback: g, 123 | organization: o.orgCode, 124 | lang: l.value, 125 | permissions: d(e, "topic.permissions", {}), 126 | aiTopic: { 127 | toggle: !0, 128 | category: o.name, 129 | asrInput: C, 130 | ocrInput: I, 131 | permissions: d(e, "topic.ai", {}), 132 | }, 133 | manualTopic: { toggle: !0, permissions: d(e, "topic.manual", {}) }, 134 | } 135 | ); 136 | }, 137 | getWaterMarkConfig: async (o, e) => { 138 | if (!e.canWaterMark) return { toggle: !1 }; 139 | const t = (await A(m.WATER_MARK_CUSTOMIZATION_INFO)) || ""; 140 | return { 141 | toggle: !0, 142 | text: n.value ? `${t} ${n.value.username}${n.value.userNo}` : t, 143 | }; 144 | }, 145 | getNoteConfig: async (o, e) => { 146 | if (!e.note.toggle) return { toggle: !1 }; 147 | let t = null; 148 | return ( 149 | e.note.toggle && (t = await W(o.code)), 150 | { toggle: e.note.toggle, token: t, permissions: d(e, "note", {}) } 151 | ); 152 | }, 153 | getDownloadConfig: (o, e) => ({ 154 | toggle: e.canDownload, 155 | onClick: () => { 156 | o && (window.location.href = `/api/v1/captures/${o.code}/download`); 157 | }, 158 | }), 159 | getPPTConfig: async (o, e, t, g = "") => { 160 | if (!e.ppt.toggle) return { toggle: !1 }; 161 | const a = p(o.videos, { label: "ENCODER" }); 162 | return a 163 | ? { 164 | toggle: e.ppt.toggle, 165 | mediaId: o.code, 166 | server: t, 167 | source: a.downloadUrl, 168 | organization: o.orgCode, 169 | callback: g, 170 | permissions: d(e, "ppt", {}), 171 | } 172 | : (console.warn("no encoder video for generate ppt"), { toggle: !1 }); 173 | }, 174 | getVideoEditorConfig: (o, e, t) => { 175 | const g = async (a) => { 176 | const s = { mediaId: o.code, targetIds: [], ...a }, 177 | c = await $(s); 178 | return ( 179 | c.id 180 | ? s.sourceId 181 | ? y.success(t("captureManagement.saveSuccessTip")) 182 | : y.success(t("captureManagement.savedWithoutTaskTip")) 183 | : y.error(t("operateFailed")), 184 | !!c.id 185 | ); 186 | }; 187 | return { 188 | toggle: z([e.canClip, e.canComposite]), 189 | mediaId: o.code, 190 | clip: { 191 | toggle: e.canClip, 192 | onClick: () => { 193 | window.open( 194 | `${window.location.origin}/capture-course/${o.code}/clip`, 195 | "_blank" 196 | ); 197 | }, 198 | }, 199 | audioCopier: { toggle: e.canComposite, onSubmit: g }, 200 | }; 201 | }, 202 | getCopyrightNotice: async () => 203 | await O(m.CUSTOM_REPLAY_VIDEO_COPYRIGHT_NOTICE), 204 | }; 205 | }; 206 | export { te as a, oe as u }; 207 | -------------------------------------------------------------------------------- /overrides/v.ustc.edu.cn/assets/captureCourses-571fa455.js: -------------------------------------------------------------------------------- 1 | import { 2 | i as o, 3 | h as t, 4 | p as n, 5 | Z as y, 6 | b$ as u, 7 | dK as C, 8 | dL as g, 9 | } from "./index-8e80cd51.js"; 10 | import { S as z, a as p, B as K, b as v } from "./captureCourse-1d01f607.js"; 11 | const b = async (a = {}, e = 1, r = 10) => { 12 | const s = { ...u(a, (d) => d !== ""), page: e, pageSize: r }; 13 | // modified 14 | function str2code(semesterStr) { 15 | const baseYear = 2021; 16 | 17 | // 解析输入字符串 18 | let year = parseInt(semesterStr.slice(0, 2), 10) + 2000; 19 | const season = semesterStr.slice(2); 20 | 21 | // 将季节转换为数字 22 | let number; 23 | switch (season) { 24 | case "春": 25 | number = 2; 26 | year -= 1; // 属于上一年 27 | break; 28 | case "夏": 29 | number = 3; 30 | year -= 1; // 属于上一年 31 | break; 32 | case "秋": 33 | number = 1; 34 | break; 35 | default: 36 | return null; // 无效的季节 37 | } 38 | 39 | // 检查输入是否有效 40 | if (isNaN(year) || year < baseYear) { 41 | return null; 42 | } 43 | 44 | const semesterCode = `${year}-${number}`; 45 | return semesterCode; 46 | } 47 | 48 | function id2code(id) { 49 | const baseYear = 2021; 50 | const baseNumber = 2; 51 | 52 | id += baseNumber - 1; 53 | console.log(id); 54 | const year = baseYear + Math.floor(id / 3); 55 | console.log(year); 56 | const number = id % 3 || 3; 57 | 58 | const code = `${year}-${number}`; 59 | return code; 60 | } 61 | 62 | function splitString(str, func) { 63 | str = str.trim(); 64 | 65 | // 使用正则表达式匹配空格、斜杠或减号 66 | let parts = str.split(/[\s\/-]+/); 67 | 68 | // 如果分割后的数组长度小于2,说明没有找到分隔符 69 | if (parts.length < 2) { 70 | parts = [str, ""]; // 返回原字符串和空字符串 71 | } 72 | 73 | // 查找func返回真值的部分 74 | const truePart = parts.find((part) => func(part)) || ""; 75 | 76 | // 查找func返回假值的部分 77 | const falsePart = parts.find((part) => !func(part)) || ""; 78 | 79 | return [falsePart, truePart]; 80 | } 81 | 82 | let semesterCode, courseCode; 83 | 84 | if (s.keyword) { 85 | const keywordResult = splitString(s.keyword, str2code); 86 | if (keywordResult) { 87 | courseCode = keywordResult[0]; 88 | semesterCode = str2code(keywordResult[1]); 89 | } 90 | } 91 | 92 | const c = await o.get( 93 | `/api/v1/${semesterCode || id2code(s.semesterId)}/capture-courses/${ 94 | courseCode || "MATH1006.01" 95 | }` 96 | ), 97 | courseData = c.data.data, 98 | i = t.decamelizeKeys({ 99 | items: courseData ? [courseData] : [], 100 | page: 1, 101 | page_size: 20, 102 | total: 3, 103 | }), 104 | m = i.items; 105 | return (i.items = n(p, m)), i; 106 | 107 | // end modified 108 | }, 109 | B = async (a) => { 110 | const e = await o.get(`/api/v1/course/${a}/schedules`); 111 | return y(e.data.data) 112 | ? [] 113 | : n(z, t.camelizeKeys(e.data.data), { strategy: "excludeAll" }); 114 | }, 115 | $ = async (a) => { 116 | if (a.length) { 117 | const e = t.decamelizeKeys({ courseCodes: a }); 118 | return (await o.get("/api/v1/course/avatar", { params: e })).data || {}; 119 | } 120 | return {}; 121 | }, 122 | S = async (a = {}, e = 1, r = 10) => { 123 | const s = { ...u(a, (l) => l !== ""), page: e, pageSize: r }, 124 | i = (await o.get("/api/v1/user-courses", { params: t.decamelizeKeys(s) })) 125 | .data.data, 126 | m = n(p, t.camelizeKeys(i.items)); 127 | delete i.items; 128 | const d = t.camelizeKeys(i); 129 | return (d.items = m), d; 130 | }, 131 | G = async (a) => { 132 | const e = t.decamelizeKeys(u(a, (i) => i !== "")), 133 | r = await o.get("/api/v1/my-instructed-capture-courses", { params: e }), 134 | s = t.camelizeKeys(r.data.data), 135 | c = n(p, s.items); 136 | return (s.items = c), s; 137 | }, 138 | U = async (a) => { 139 | const e = t.decamelizeKeys({ ...u(a, (i) => i !== "") }), 140 | r = await o.get("/api/v1/capture-courses", { params: e }), 141 | s = t.camelizeKeys(r.data.data), 142 | c = n(p, s.items); 143 | return (s.items = c), s; 144 | }, 145 | f = async (a) => { 146 | const e = t.decamelizeKeys({ captureCourseIds: a }); 147 | return ( 148 | await o.get("/api/v1/capture-courses/classroom-names", { params: e }) 149 | ).data.data; 150 | }, 151 | A = async (a, e) => 152 | (await o.put(`/api/v1/capture-courses/${a}/setting`, t.decamelizeKeys(e))) 153 | .data, 154 | I = async (a, e) => { 155 | const r = t.decamelizeKeys({ ...u(a, (c) => c !== "") }); 156 | return ( 157 | await o.put("/api/v1/capture-courses:batchSetting", t.decamelizeKeys(e), { 158 | params: r, 159 | }) 160 | ).data; 161 | }, 162 | x = async (a) => { 163 | const e = t.decamelizeKeys({ ...u(a, (s) => s !== "") }); 164 | return (await o.get("/api/v1/capture-courses:batchGetCount", { params: e })) 165 | .data; 166 | }, 167 | N = async (a) => { 168 | await o.post(`/api/v1/capture-courses/${a}/visits`); 169 | }, 170 | k = async (a) => { 171 | const e = t.decamelizeKeys({ captureCourseIds: a }), 172 | r = await o.get("/api/v1/capture-courses:batchGetByIds", { params: e }), 173 | s = t.camelizeKeys(r.data.data); 174 | return n(p, s); 175 | }, 176 | D = async (a, e) => { 177 | const r = await o.get( 178 | `/api/v1/capture-courses/${a}/recently-visited-users`, 179 | { params: t.decamelizeKeys(e) } 180 | ), 181 | s = t.camelizeKeys(r.data.data), 182 | c = n(g, s.items); 183 | return (s.items = c), s; 184 | }, 185 | T = async (a, e, r) => { 186 | const c = await o.get(`/api/v1/${a}/capture-courses/${e}`); 187 | // modified: remove entry information "{params: u({entry: r})}" 188 | return ( 189 | (c.data.data = n(p, t.camelizeKeys(c.data.data), { 190 | strategy: "excludeAll", 191 | })), 192 | (c.data.error = t.camelizeKeys(c.data.error)), 193 | c.data 194 | ); 195 | }, 196 | V = async (a, e) => { 197 | const r = u({ entry: e }), 198 | s = await o.get(`/api/v1/capture-courses/${a}:byCode`, { params: r }); 199 | return ( 200 | (s.data.data = n(p, t.camelizeKeys(s.data.data), { 201 | strategy: "excludeAll", 202 | })), 203 | (s.data.error = t.camelizeKeys(s.data.error)), 204 | s.data 205 | ); 206 | }, 207 | E = async (a) => { 208 | const e = await o.get( 209 | `/api/v1/capture-courses/${a}/collections:batchGetAll` 210 | ); 211 | return n(C, t.camelizeKeys(e.data.data)); 212 | }, 213 | M = async (a) => { 214 | await o.post(`/api/v1/capture-courses/${a}/collection`); 215 | }, 216 | j = async (a) => { 217 | await o.delete(`/api/v1/capture-courses/${a}/collection`); 218 | }, 219 | q = async (a) => { 220 | const e = t.decamelizeKeys({ userNo: a }), 221 | r = await o.get("/api/v1/current-canrecord-capture-courses:batchGet", { 222 | params: e, 223 | }); 224 | return n(K, t.camelizeKeys(r.data.data)); 225 | }, 226 | L = async (a, e = 1, r = 10) => { 227 | const s = await o.get("/api/v1/capture-courses/teaching-class-names", { 228 | params: { page: e, pageSize: r, keyword: a }, 229 | }), 230 | c = t.camelizeKeys(s.data.data); 231 | return (c.items = n(v, c.items)), c; 232 | }; 233 | export { 234 | $ as a, 235 | k as b, 236 | x as c, 237 | I as d, 238 | U as e, 239 | f, 240 | b as g, 241 | V as h, 242 | T as i, 243 | B as j, 244 | j as k, 245 | M as l, 246 | E as m, 247 | D as n, 248 | q as o, 249 | L as p, 250 | S as q, 251 | G as r, 252 | N as t, 253 | A as u, 254 | }; 255 | --------------------------------------------------------------------------------