Udemy 字幕下载 | Udemy Subtitle Downloader v2

下载 Udemy 的字幕 | Download Udemy Subtitle as .srt file

As of 2021-03-03. See the latest version.

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