Udemy 字幕下载 v3

下载字幕为 .vtt 文件, 也可以下载一整门课程的字幕(多个文件),也可以下载视频(.mp4)

  1. // ==UserScript==
  2. // @name:zh-CN Udemy 字幕下载 v3
  3. // @name Udemy Subtitle Downloader v3
  4. // @version 3
  5. // @description:zh-CN 下载字幕为 .vtt 文件, 也可以下载一整门课程的字幕(多个文件),也可以下载视频(.mp4)
  6. // @description Download Udemy Subtitle as .vtt file
  7. // @author Zheng Cheng
  8. // @match https://www.udemy.com/course/*
  9. // @run-at document-end
  10. // @grant unsafeWindow
  11. // @namespace https://greasyfork.org/users/5711
  12. // ==/UserScript==
  13.  
  14. // 写于2021-3-2
  15. // [优点]
  16. // 1. 使用门槛比 udemy-dl 低 (不需要用命令行)
  17. // 2. 方便,点击就下载
  18.  
  19. // [备注]
  20. // 本脚本依赖于 Udemy 的 API,如果哪天 Udemy 进行了改动,那么本程序不能用了是很正常的,修复一下即可。
  21. // 作者邮箱 guokrfans@gmail.com
  22. // 测试/开发环境:
  23. // macOS Big Sur 11.2.1
  24. // Chrome 版本 88.0.4324.192(正式版本) (x86_64)
  25. // Tampermonkey v4.11
  26. // 不保证其他浏览器可用
  27.  
  28. // [实现原理]
  29. // 数据从 API 拿, 发请求时带上一个 token 就行,放到请求头里,这个 token 去 Cookie 里面拿 access_token 就行。
  30. // 这是基本概念,具体作法参考下方的代码即可。
  31.  
  32. (function () {
  33. 'use strict';
  34.  
  35. // 全局变量
  36. var div = document.createElement('div'); // 所有元素都放这里面
  37. var button1 = document.createElement('button'); // 下载本集的字幕(1个 .vtt 文件)
  38. var button2 = document.createElement('button'); // 下载整门课程的字幕 (多个 .vtt 文件)
  39. var button3 = document.createElement('button'); // 下载本集视频
  40. var title_element = null; // 页面左上角的标题
  41.  
  42. // 用法 await sleep(1000) 毫秒
  43. function sleep(ms) {
  44. return new Promise(resolve => setTimeout(resolve, ms));
  45. }
  46.  
  47. // 在某节点后面插入新节点
  48. function insertAfter(newNode, referenceNode) {
  49. referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  50. }
  51.  
  52. // copy from: https://gist.github.com/danallison/3ec9d5314788b337b682
  53. // Example downloadString(srt, "text/plain", filename);
  54. function downloadString(text, fileType, fileName) {
  55. var blob = new Blob([text], {
  56. type: fileType
  57. });
  58. var a = document.createElement('a');
  59. a.download = fileName;
  60. a.href = URL.createObjectURL(blob);
  61. a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
  62. a.style.display = "none";
  63. document.body.appendChild(a);
  64. a.click();
  65. document.body.removeChild(a);
  66. setTimeout(function () {
  67. URL.revokeObjectURL(a.href);
  68. }, 11500);
  69. }
  70.  
  71. // 获得参数
  72. function get_args() {
  73. var ud_app_loader = document.querySelector('.ud-app-loader')
  74. var args = ud_app_loader.dataset.moduleArgs
  75. var json = JSON.parse(args)
  76. return json
  77. }
  78.  
  79. // 获得课程 id
  80. function get_args_course_id() {
  81. var json = get_args()
  82. return json.courseId
  83. }
  84.  
  85. // 获得这一节的 id
  86. function get_args_lecture_id() {
  87. var json = get_args()
  88. return json.initialCurriculumItemId
  89. }
  90.  
  91. // 返回 Cookie 里指定名字的值
  92. // https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
  93. function getCookie(name) {
  94. return (document.cookie.match('(?:^|;)\\s*' + name.trim() + '\\s*=\\s*([^;]*?)\\s*(?:;|$)') || [])[1];
  95. }
  96.  
  97. // 单个视频的数据 URL
  98. // 可以传参数也可以不传,不传就当做取当前视频的
  99. function get_lecture_data_url(param_course_id = null, param_lecture_id = null) {
  100. // var course_id = '3681012'
  101. // var lecture_id = '23665120'
  102. // var example_url = `https://www.udemy.com/api-2.0/users/me/subscribed-courses/3681012/lectures/23665120/?fields[lecture]=asset,description,download_url,is_free,last_watched_second&fields[asset]=asset_type,length,media_license_token,media_sources,captions,thumbnail_sprite,slides,slide_urls,download_urls`
  103. var course_id = param_course_id || get_args_course_id()
  104. var lecture_id = param_lecture_id || get_args_lecture_id()
  105. var url = `https://www.udemy.com/api-2.0/users/me/subscribed-courses/${course_id}/lectures/${lecture_id}/?fields[lecture]=asset,description,download_url,is_free,last_watched_second&fields[asset]=asset_type,length,media_license_token,media_sources,captions,thumbnail_sprite,slides,slide_urls,download_urls`
  106. return url
  107. }
  108.  
  109.  
  110. // 一整门课的数据 URL
  111. function get_course_data_url() {
  112. var course_id = get_args_course_id()
  113. // var example_url = "https://www.udemy.com/api-2.0/courses/3681012/subscriber-curriculum-items/?page_size=1400&fields[lecture]=title,object_index,is_published,sort_order,created,asset,supplementary_assets,is_free&fields[quiz]=title,object_index,is_published,sort_order,type&fields[practice]=title,object_index,is_published,sort_order&fields[chapter]=title,object_index,is_published,sort_order&fields[asset]=title,filename,asset_type,status,time_estimation,is_external&caching_intent=True"
  114. var url = `https://www.udemy.com/api-2.0/courses/${course_id}/subscriber-curriculum-items/?page_size=1400&fields[lecture]=title,object_index,is_published,sort_order,created,asset,supplementary_assets,is_free&fields[quiz]=title,object_index,is_published,sort_order,type&fields[practice]=title,object_index,is_published,sort_order&fields[chapter]=title,object_index,is_published,sort_order&fields[asset]=title,filename,asset_type,status,time_estimation,is_external&caching_intent=True`
  115. return url
  116. }
  117.  
  118. // 获得一节的数据
  119. function get_lecture_data(course_id = null, lecture_id = null) {
  120. return new Promise((resolve, reject) => {
  121. var access_token = getCookie("access_token")
  122. var bearer_token = `Bearer ${access_token}`
  123. fetch(get_lecture_data_url(course_id, lecture_id), {
  124. headers: {
  125. 'x-udemy-authorization': bearer_token,
  126. 'authorization': bearer_token,
  127. }
  128. })
  129. .then(response => response.json())
  130. .then(data => {
  131. resolve(data);
  132. }).catch(e => {
  133. reject(e);
  134. })
  135. })
  136. }
  137.  
  138. // 获得一整门课的数据
  139. function get_course_data() {
  140. return new Promise((resolve, reject) => {
  141. var access_token = getCookie("access_token")
  142. var bearer_token = `Bearer ${access_token}`
  143. fetch(get_course_data_url(), {
  144. headers: {
  145. 'x-udemy-authorization': bearer_token,
  146. 'authorization': bearer_token,
  147. }
  148. })
  149. .then(response => response.json())
  150. .then(data => {
  151. // console.log(data);
  152. // var captions_array = data.asset.captions;
  153. // console.log(cations_array);
  154. resolve(data);
  155. }).catch(e => {
  156. reject(e);
  157. })
  158. })
  159. }
  160.  
  161. // 转换成安全的文件名
  162. function safe_filename(string) {
  163. var s = string
  164. s = s.replace(':', '-')
  165. s = s.replace('\'', ' ')
  166. return s
  167. }
  168.  
  169. // 输入 id
  170. // 返回那节课的标题
  171. // await get_lecture_title_by_id(id)
  172. async function get_lecture_title_by_id(id) {
  173. var data = await get_course_data()
  174. var array = data.results;
  175. for (let i = 0; i < array.length; i++) {
  176. const r = array[i];
  177. if (r._class == 'lecture' && r.id == id) {
  178. var name = `${r.object_index}. ${r.title}`
  179. return name;
  180. }
  181. }
  182. }
  183.  
  184. // 下载当前这一节视频的字幕
  185. // 如何调用: await parse_lecture_data();
  186. // 会下载得到一个 .vtt 字幕
  187. async function parse_lecture_data(course_id = null, lecture_id = null) {
  188. var data = await get_lecture_data(course_id, lecture_id) // 获得当前这一节的数据
  189. var lecture_id = data.id; // 获得这一节的 id
  190. var lecture_title = await get_lecture_title_by_id(lecture_id) // 根据 id 找到标题
  191.  
  192. // 遍历数组
  193. var array = data.asset.captions
  194. for (let i = 0; i < array.length; i++) {
  195. const caption = array[i];
  196. var url = caption.url // vtt 字幕的 URL
  197. // var locale_id = caption.locale_id // locale_id: "en_US"
  198. // var label = caption.video_label
  199. // var filename = `${label}_${safe_filename(lecture_title)}.vtt` // 构造文件名
  200. var filename = `${safe_filename(lecture_title)}.vtt` // 构造文件名
  201. save_vtt(url, filename); // 直接保存
  202. }
  203. }
  204.  
  205. // 保存 vtt
  206. // 参数: url 是 vtt 文件的 url,访问 url 应该得到文件内容
  207. // filename 是要保存的文件名
  208. function save_vtt(url, filename) {
  209. fetch(url, {})
  210. .then(response => response.text())
  211. .then(data => {
  212. downloadString(data, "text/plain", filename);
  213. }).catch(e => {
  214. console.log(e);
  215. })
  216. }
  217.  
  218. // 把 UI 元素放到页面上
  219. async function inject_our_script() {
  220. title_element = document.querySelector('a[data-purpose="course-header-title"]')
  221.  
  222. var button1_css = `
  223. font-size: 14px;
  224. padding: 1px 12px;
  225. border-radius: 4px;
  226. border: none;
  227. color: black;
  228. `;
  229.  
  230. var button2_css = `
  231. font-size: 14px;
  232. padding: 1px 12px;
  233. border-radius: 4px;
  234. border: none;
  235. color: black;
  236. margin-left: 8px;
  237. `;
  238.  
  239. var div_css = `
  240. margin-bottom: 10px;
  241. `;
  242.  
  243. button1.setAttribute('style', button1_css);
  244. button1.textContent = "下载本集字幕"
  245. button1.addEventListener('click', download_lecture_subtitle);
  246.  
  247. button2.setAttribute('style', button2_css);
  248. var num = await get_course_lecture_number()
  249. button2.textContent = `下载整门课程的字幕(${num}个文件)`
  250. button2.addEventListener('click', download_course_subtitle);
  251.  
  252. button3.setAttribute('style', button2_css);
  253. button3.textContent = "下载本集视频"
  254. button3.addEventListener('click', download_lecture_video);
  255.  
  256. div.setAttribute('style', div_css);
  257. div.appendChild(button1);
  258. div.appendChild(button2);
  259. div.appendChild(button3);
  260.  
  261. insertAfter(div, title_element);
  262. }
  263.  
  264. // 下载本集字幕
  265. async function download_lecture_subtitle() {
  266. await parse_lecture_data();
  267. }
  268.  
  269. // 下载课程全部字幕
  270. async function download_course_subtitle() {
  271. var course_id = get_args_course_id();
  272. var data = await get_course_data()
  273. var array = data.results;
  274. for (let i = 0; i < array.length; i++) {
  275. const result = array[i];
  276. if (result._class == 'lecture') {
  277. var lecture_id = result.id;
  278. await parse_lecture_data(course_id, lecture_id)
  279. await sleep(800);
  280. }
  281. }
  282. }
  283.  
  284. // 下载本集视频
  285. async function download_lecture_video() {
  286. button3.textContent = "下载本集视频 (开始下载)"
  287. var data = await get_lecture_data() // 获得当前这一节的数据
  288. var lecture_id = data.id; // 获得这一节的 id
  289. var lecture_title = await get_lecture_title_by_id(lecture_id) // 根据 id 找到标题
  290.  
  291. var r = data.asset.media_sources[0]
  292. // var example = {
  293. // "type": "video/mp4",
  294. // "src": "https://mp4-a.udemycdn.com/2020-12-04_12-48-10-150cfde997c5ba9f05e5e7d86c813db3/1/WebHD_720p.mp4?lKL6M-V-HXBl9MVKyHqfbP9nVBBFDd6lLLXl7USDCVB63OhpUk722Vt6EW1NlopbdZmF9J_9YZCTOhMrhxj26O1uGmgUqUL4F8e79BxKUeKCnxjTKPo3vA6eRzNAINw4k174S8MaD7ND9b37F_TOs4mxC9BLcUyPTxrSMhDLbjQuWl_P",
  295. // "label": "720"
  296. // }
  297.  
  298. var url = r.src // "https://mp4-a.udemycdn.com/2020-12-04_12-48-10-150cfde997c5ba9f05e5e7d86c813db3/1/WebHD_720p.mp4?XquxJGAXiyTc17qxb6iyah_9GXvjHC43UK98UHC3LUkZk7q9yPPll-BJ-5RKz--T9ucjtKOES68m_rZ6vzDZkyEROWwuaoHGFsr3DDuN0AWwk3RpjEo-JNfp98iIaEd_0Vfk0te375rNGtvtCnXibgcZmxDOx4tI5jqFKkl5hVDnwVE7"
  299. var resolution = r.label // 720 or 1080
  300. var filename = `${safe_filename(lecture_title)}_${resolution}p.mp4` // 构造文件名
  301. var type = r.type
  302.  
  303. fetch(url)
  304. .then(res => res.blob())
  305. .then(blob => {
  306. downloadString(blob, type, filename);
  307. button3.textContent = "下载本集视频 (下载完成)"
  308. });
  309. }
  310.  
  311. // 返回一个整数,代表有多少个视频
  312. async function get_course_lecture_number() {
  313. var data = await get_course_data()
  314. var array = data.results;
  315. var num = 0
  316. for (let i = 0; i < array.length; i++) {
  317. const r = array[i];
  318. if (r._class == 'lecture') {
  319. num += 1;
  320. }
  321. }
  322. return num
  323. }
  324.  
  325. // 主入口
  326. async function main() {
  327. inject_our_script()
  328. }
  329.  
  330. // 延迟执行,保险一点
  331. setTimeout(main, 2500);
  332. })();