Youtube 翻译中文字幕下载 v8

Youtube 播放器右下角有个 Auto-tranlsate,可以把视频字幕翻成中文。这个脚本是下载这个中文字幕

اعتبارا من 03-12-2020. شاهد أحدث إصدار.

  1. // ==UserScript==
  2. // @name Youtube 翻译中文字幕下载 v8
  3. // @include https://*youtube.com/*
  4. // @author Cheng Zheng
  5. // @copyright 2018-2021 Cheng Zheng;
  6. // @license GNU GPL v3.0 or later. http://www.gnu.org/copyleft/gpl.html
  7. // @require http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
  8. // @version 8
  9. // @grant GM_xmlhttpRequest
  10. // @namespace https://greasyfork.org/users/5711
  11. // @description Youtube 播放器右下角有个 Auto-tranlsate,可以把视频字幕翻成中文。这个脚本是下载这个中文字幕
  12. // ==/UserScript==
  13.  
  14. /*
  15. 作者 : 郑诚
  16. 新浪微博: 糖醋陈皮 https://weibo.com/u/2004104451/home?wvr=5
  17. 邮箱 : guokrfans@gmail.com
  18. Github: https://github.com/1c7/Youtube-Auto-Subtitle-Download
  19.  
  20. 测试视频:
  21. https://www.youtube.com/watch?v=nGlQkaoIfBI 1门语言
  22. https://www.youtube.com/watch?v=O5nskjZ_GoI 13门语言
  23. https://www.youtube.com/watch?v=VfEz3DIbkvo 测试自动字幕(西班牙语)
  24. */
  25.  
  26. (function () {
  27. // 配置项
  28. const NO_SUBTITLE = '无字幕';
  29. const HAVE_SUBTITLE = '下载翻译的中文字幕';
  30. const TEXT_LOADING = '载入中...';
  31. const BUTTON_ID = 'youtube-translate-to-chinese-subtitle-downloader-by-1c7'
  32. // 配置项
  33.  
  34. var HASH_BUTTON_ID = `#${BUTTON_ID}`
  35. var first_load = true;
  36.  
  37. // return true / false
  38. // Detect [new version UI(material design)] OR [old version UI]
  39. // I tested this, accurated.
  40. function new_material_design_version() {
  41. var old_title_element = document.getElementById('watch7-headline');
  42. if (old_title_element) {
  43. return false;
  44. } else {
  45. return true;
  46. }
  47. }
  48.  
  49. // trigger when first load (hit refresh button)
  50. $(document).ready(function () {
  51. // because document ready still not enough
  52. // it's still too early, we have to wait certain element exist, then execute function.
  53. if (new_material_design_version()) {
  54. var material_checkExist = setInterval(function () {
  55. if (document.querySelectorAll('.title.style-scope.ytd-video-primary-info-renderer').length) {
  56. init();
  57. clearInterval(material_checkExist);
  58. }
  59. }, 330);
  60. } else {
  61. var checkExist = setInterval(function () {
  62. if ($('#watch7-headline').length) {
  63. init();
  64. clearInterval(checkExist);
  65. }
  66. }, 330);
  67. }
  68.  
  69. });
  70.  
  71. // trigger when loading new page (actually this would also trigger when first loading, that's not what we want, that's why we need to use firsr_load === false)
  72. // (new Material design version would trigger this "yt-navigate-finish" event. old version would not.)
  73. var body = document.getElementsByTagName("body")[0];
  74. body.addEventListener("yt-navigate-finish", function (event) {
  75. if (first_load === false) {
  76. remove_subtitle_download_button();
  77. init();
  78. }
  79. });
  80.  
  81. // trigger when loading new page
  82. // (old version would trigger this "spfdone" event. new Material design version not sure yet.)
  83. window.addEventListener("spfdone", function (e) {
  84. if (current_page_is_video_page()) {
  85. remove_subtitle_download_button();
  86. var checkExist = setInterval(function () {
  87. if ($('#watch7-headline').length) {
  88. init();
  89. clearInterval(checkExist);
  90. }
  91. }, 330);
  92. }
  93.  
  94. });
  95.  
  96. // return true / false
  97. function current_page_is_video_page() {
  98. return get_video_id() !== null;
  99. }
  100.  
  101. // return string like "RW1ChiWyiZQ", from "https://www.youtube.com/watch?v=RW1ChiWyiZQ"
  102. // or null
  103. function get_video_id() {
  104. return getURLParameter('v');
  105. }
  106.  
  107. //https://stackoverflow.com/questions/11582512/how-to-get-url-parameters-with-javascript/11582513#11582513
  108. function getURLParameter(name) {
  109. return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
  110. }
  111.  
  112. function remove_subtitle_download_button() {
  113. $(HASH_BUTTON_ID).remove();
  114. }
  115.  
  116. function init() {
  117. unsafeWindow.caption_array = [];
  118. inject_our_script();
  119. first_load = false;
  120. }
  121.  
  122. function inject_our_script() {
  123. var div = document.createElement('div'),
  124. select = document.createElement('select'),
  125. option = document.createElement('option'),
  126. controls = document.getElementById('watch7-headline'); // Youtube video title DIV
  127.  
  128. if (new_material_design_version()) {
  129. div.setAttribute('style', `display: table;
  130. margin-top:4px;
  131. border: 1px solid rgb(0, 183, 90);
  132. cursor: pointer; color: rgb(255, 255, 255);
  133. border-top-left-radius: 3px;
  134. border-top-right-radius: 3px;
  135. border-bottom-right-radius: 3px;
  136. border-bottom-left-radius: 3px;
  137. background-color: #00B75A;
  138. padding: 4px;
  139. padding-right: 8px;
  140. `);
  141. } else {
  142. div.setAttribute('style', `display: table;
  143. margin-top:4px;
  144. border: 1px solid rgb(0, 183, 90);
  145. cursor: pointer; color: rgb(255, 255, 255);
  146. border-top-left-radius: 3px;
  147. border-top-right-radius: 3px;
  148. border-bottom-right-radius: 3px;
  149. border-bottom-left-radius: 3px;
  150. background-color: #00B75A;
  151. padding: 3px;
  152. padding-right: 8px;
  153. `);
  154. }
  155.  
  156. div.id = BUTTON_ID;
  157.  
  158. select.id = 'captions_selector';
  159. select.disabled = true;
  160. select.setAttribute('style', 'display:block; border: 1px solid rgb(0, 183, 90); cursor: pointer; color: rgb(255, 255, 255); background-color: #00B75A;');
  161.  
  162. option.textContent = TEXT_LOADING;
  163. option.selected = true;
  164. select.appendChild(option);
  165.  
  166. // 下拉菜单中选择后的事件侦听
  167. select.addEventListener('change', function () {
  168. download_subtitle(this);
  169. }, false);
  170.  
  171. div.appendChild(select);
  172. // put <select> into <div>
  173.  
  174. // put the div into page: new material design
  175. var title_element = document.querySelectorAll('.title.style-scope.ytd-video-primary-info-renderer');
  176. if (title_element) {
  177. $(title_element[0]).after(div);
  178. }
  179. // put the div into page: old version
  180. if (controls) {
  181. controls.appendChild(div);
  182. }
  183.  
  184. load_language_list(select);
  185.  
  186. // <a> element is for download
  187. var a = document.createElement('a');
  188. a.style.cssText = 'display:none;';
  189. a.setAttribute("id", "ForSubtitleDownload");
  190. var body = document.getElementsByTagName('body')[0];
  191. body.appendChild(a);
  192. }
  193.  
  194. // Input: 语言代码(代表要从哪个语言转成中文)
  195. // Ouput: 字幕的 URL,或者 false(找不到的话)
  196. function get_chinese_subtitle_url(from_language_code) {
  197. var json = get_json();
  198. var captionTracks = json.captions.playerCaptionsTracklistRenderer.captionTracks;
  199. // 有多少种语言,captionTracks 就有多少个数组。
  200.  
  201. // 从 captionTracks 里面用 loop 来搜索对应语言。返回 baseUrl。
  202. var baseURL = '';
  203. for (var index in captionTracks) {
  204. var caption = captionTracks[index];
  205. if (caption.languageCode === from_language_code) {
  206. baseURL = captionTracks[index].baseUrl;
  207. }
  208. }
  209. if (baseURL == '') {
  210. return false; // 上面的 loop 要是没找到对应的语言,导致 url 是空,那也只能返回 false
  211. }
  212. var chinese_subtitle_url = baseURL + "&tlang=zh-Hans"; // 转成中文字幕
  213. return chinese_subtitle_url;
  214. }
  215.  
  216. // Trigger when user select <option>
  217. async function download_subtitle(selector) {
  218. // if user select first <option>
  219. // we just return, do nothing.
  220. if (selector.selectedIndex == 0) {
  221. return;
  222. }
  223.  
  224. var caption = caption_array[selector.selectedIndex - 1]; // because first <option> is for display, so index-1
  225. if (!caption) return;
  226.  
  227. var lang_code = caption.lang_code;
  228. var lang_name = caption.lang_name;
  229.  
  230. // if user choose auto subtitle // 如果用户选的是自动字幕
  231. if (caption.lang_code == 'AUTO') {
  232. var file_name = get_file_name(lang_name);
  233. download_auto_subtitle(file_name);
  234. selector.options[0].selected = true; // after download, select first <option>
  235. return
  236. }
  237.  
  238. // 如果用户选的是完整字幕
  239. // 原文
  240. // sub mean "subtitle"
  241. var sub_original_url = await get_closed_subtitle_url(lang_code)
  242.  
  243. // 中文
  244. var sub_translated_url = sub_original_url + "&tlang=" + "zh-Hans"
  245. var sub_translated_xml = await get(sub_translated_url);
  246.  
  247. var sub_translated_srt = parse_youtube_XML_to_object_list(sub_translated_xml)
  248.  
  249. var srt_string = object_array_to_SRT_string(sub_translated_srt)
  250. var title = get_file_name(lang_name);
  251. downloadString(srt_string, "text/plain", title);
  252.  
  253. // after download, select first <option>
  254. selector.options[0].selected = true;
  255. }
  256.  
  257. // Return something like: "(English)How Did Python Become A Data Science Powerhouse?.srt"
  258. function get_file_name(x) {
  259. return `(${x}) ${document.title}.srt`;
  260. }
  261.  
  262. // 载入有多少种语言, 然后加到 <select> 里
  263. function load_language_list(select) {
  264. // auto
  265. var auto_subtitle_exist = false;
  266.  
  267. // closed
  268. var closed_subtitle_exist = false;
  269. var captions = null;
  270.  
  271. // get auto subtitle
  272. var auto_subtitle_url = get_auto_subtitle_xml_url();
  273. if (auto_subtitle_url != false) {
  274. auto_subtitle_exist = true;
  275. // console.log('自动字幕的确存在');
  276. } else {
  277. // console.log('自动字幕不存在');
  278. // console.log(auto_subtitle_url);
  279. }
  280.  
  281. // get closed subtitle
  282. var list_url = 'https://video.google.com/timedtext?v=' + get_video_id() + '&type=list&hl=zh-CN';
  283. // https://video.google.com/timedtext?v=if36bqHypqk&type=list&hl=en // 英文
  284. // https://video.google.com/timedtext?v=n1zpnN-6pZQ&type=list&hl=zh-CN // 中文
  285.  
  286. GM_xmlhttpRequest({
  287. method: 'GET',
  288. url: list_url,
  289. onload: function (xhr) {
  290.  
  291. captions = new DOMParser().parseFromString(xhr.responseText, "text/xml").getElementsByTagName('track');
  292. if (captions.length != 0) {
  293. closed_subtitle_exist = true;
  294. }
  295.  
  296. // if no subtitle at all, just say no and stop
  297. if (auto_subtitle_exist == false && closed_subtitle_exist == false) {
  298. select.options[0].textContent = NO_SUBTITLE;
  299. disable_download_button();
  300. return false;
  301. }
  302.  
  303. // if at least one type of subtitle exist
  304. select.options[0].textContent = HAVE_SUBTITLE;
  305. select.disabled = false;
  306.  
  307. // if at least one type of subtitle exist
  308. select.options[0].textContent = HAVE_SUBTITLE;
  309. select.disabled = false;
  310.  
  311. var caption = null; // for inside loop
  312. var option = null; // for <option>
  313. var caption_info = null; // for our custom object
  314.  
  315. // 自动字幕
  316. if (auto_subtitle_exist) {
  317. var auto_sub_name = get_auto_subtitle_name()
  318. var lang_name = `${auto_sub_name} 翻译成 中文`
  319. caption_info = {
  320. lang_code: 'AUTO', // later we use this to know if it's auto subtitle
  321. lang_name: lang_name // for display only
  322. };
  323. caption_array.push(caption_info);
  324.  
  325. option = document.createElement('option');
  326. option.textContent = caption_info.lang_name;
  327. select.appendChild(option);
  328. }
  329.  
  330. // if closed_subtitle_exist
  331. if (closed_subtitle_exist) {
  332. for (var i = 0, il = captions.length; i < il; i++) {
  333. caption = captions[i];
  334. // console.log(caption); // <track id="0" name="" lang_code="en" lang_original="English" lang_translated="English" lang_default="true"/>
  335. var lang_code = caption.getAttribute('lang_code')
  336. var lang_translated = caption.getAttribute('lang_translated')
  337. var lang_name = `${lang_translated} 翻译成 中文`
  338. caption_info = {
  339. lang_code: lang_code, // for AJAX request
  340. lang_name: lang_name, // display to user
  341. };
  342. caption_array.push(caption_info);
  343. // 注意这里是加到 caption_array, 一个全局变量, 待会要靠它来下载
  344. option = document.createElement('option');
  345. option.textContent = caption_info.lang_name;
  346. select.appendChild(option);
  347. }
  348. }
  349. }
  350. });
  351. }
  352.  
  353. // 处理时间. 比如 start="671.33" start="37.64" start="12" start="23.029"
  354. // 处理成 srt 时间, 比如 00:00:00,090 00:00:08,460 00:10:29,350
  355. function process_time(s) {
  356. s = s.toFixed(3);
  357. // 超棒的函数, 不论是整数还是小数都给弄成3位小数形式
  358. // 举个柚子:
  359. // 671.33 -> 671.330
  360. // 671 -> 671.000
  361. // 注意函数会四舍五入. 具体读文档
  362.  
  363. var array = s.split('.');
  364. // 把开始时间根据句号分割
  365. // 671.330 会分割成数组: [671, 330]
  366.  
  367. var Hour = 0;
  368. var Minute = 0;
  369. var Second = array[0]; // 671
  370. var MilliSecond = array[1]; // 330
  371. // 先声明下变量, 待会把这几个拼好就行了
  372.  
  373. // 我们来处理秒数. 把"分钟"和"小时"除出来
  374. if (Second >= 60) {
  375. Minute = Math.floor(Second / 60);
  376. Second = Second - Minute * 60;
  377. // 把 秒 拆成 分钟和秒, 比如121秒, 拆成2分钟1秒
  378.  
  379. Hour = Math.floor(Minute / 60);
  380. Minute = Minute - Hour * 60;
  381. // 把 分钟 拆成 小时和分钟, 比如700分钟, 拆成11小时40分钟
  382. }
  383. // 分钟,如果位数不够两位就变成两位,下面两个if语句的作用也是一样。
  384. if (Minute < 10) {
  385. Minute = '0' + Minute;
  386. }
  387. // 小时
  388. if (Hour < 10) {
  389. Hour = '0' + Hour;
  390. }
  391. // 秒
  392. if (Second < 10) {
  393. Second = '0' + Second;
  394. }
  395. return Hour + ':' + Minute + ':' + Second + ',' + MilliSecond;
  396. }
  397.  
  398. function downloadFile(fileName, content) {
  399. var version = getChromeVersion();
  400.  
  401. // dummy element for download
  402. if ($('#youtube-subtitle-downloader-dummy-element-for-download').length > 0) {} else {
  403. $("body").append('<a id="youtube-subtitle-downloader-dummy-element-for-download"></a>');
  404. }
  405. var dummy = $('#youtube-subtitle-downloader-dummy-element-for-download');
  406.  
  407. // 判断 Chrome 版本选择下载方法,Chrome 52 和 53 的文件下载方式不一样
  408. if (version > 52) {
  409. dummy.attr('download', fileName);
  410. dummy.attr('href', 'data:Content-type: text/plain,' + htmlDecode(content));
  411. dummy[0].click();
  412. } else {
  413. downloadViaBlob(fileName, htmlDecode(content));
  414. }
  415. }
  416.  
  417. // 复制自: http://www.alloyteam.com/2014/01/use-js-file-download/
  418. // Chrome 53 之后这个函数失效。52有效。
  419. function downloadViaBlob(fileName, content) {
  420. var aLink = document.createElement('a');
  421. var blob = new Blob([content]);
  422. var evt = document.createEvent("HTMLEvents");
  423. evt.initEvent("click", false, false);
  424. aLink.download = fileName;
  425. aLink.href = URL.createObjectURL(blob);
  426. aLink.dispatchEvent(evt);
  427. }
  428.  
  429. //http://stackoverflow.com/questions/4900436/how-to-detect-the-installed-chrome-version
  430. function getChromeVersion() {
  431. var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
  432. return raw ? parseInt(raw[2], 10) : false;
  433. }
  434.  
  435. // https://css-tricks.com/snippets/javascript/unescape-html-in-js/
  436. // turn HTML entity back to text, example: &quot; should be "
  437. function htmlDecode(input) {
  438. var e = document.createElement('div');
  439. e.class = 'dummy-element-for-tampermonkey-Youtube-Subtitle-Downloader-script-to-decode-html-entity';
  440. e.innerHTML = input;
  441. return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
  442. }
  443.  
  444. // return URL or null;
  445. // later we can send a AJAX and get XML subtitle
  446. function get_auto_subtitle_xml_url() {
  447. try {
  448. var json = get_json();
  449. var captionTracks = json.captions.playerCaptionsTracklistRenderer.captionTracks;
  450. for (var index in captionTracks) {
  451. var caption = captionTracks[index];
  452. if (typeof caption.kind === 'string' && caption.kind == 'asr') {
  453. return captionTracks[index].baseUrl;
  454. }
  455. // ASR – A caption track generated using automatic speech recognition.
  456. // https://developers.google.com/youtube/v3/docs/captions
  457. }
  458. return false;
  459. } catch (error) {
  460. return false;
  461. }
  462. }
  463.  
  464. function disable_download_button() {
  465. $(HASH_BUTTON_ID)
  466. .css('border', '#95a5a6')
  467. .css('cursor', 'not-allowed')
  468. .css('background-color', '#95a5a6');
  469. $('#captions_selector')
  470. .css('border', '#95a5a6')
  471. .css('cursor', 'not-allowed')
  472. .css('background-color', '#95a5a6');
  473.  
  474. if (new_material_design_version()) {
  475. $(HASH_BUTTON_ID).css('padding', '6px');
  476. } else {
  477. $(HASH_BUTTON_ID).css('padding', '5px');
  478. }
  479. }
  480.  
  481. // 下载自动字幕的中英双语
  482. // 输入: file_name: 保存的文件名
  483. // 输出: 无 (会触发浏览器下载一个文件)
  484. async function download_auto_subtitle(file_name) {
  485. // console.log(`进入了 download_auto_subtitle, 参数 file_name 是 ${file_name}`)
  486. // 1. English Auto Sub in json3 format
  487. var auto_sub_url = get_auto_subtitle_xml_url();
  488. var format_json3_url = auto_sub_url + '&fmt=json3'
  489. // var en_auto_sub = await get(format_json3_url); // 格式参考 Youtube-Subtitle-Downloader/fmt=json3/en.json
  490. // console.log(en_auto_sub);
  491.  
  492. // 2. 自动字幕的翻译中文
  493. var cn_url = format_json3_url + '&tlang=zh-Hans'
  494. // console.log(cn_url);
  495. var cn_srt = await auto_sub_in_chinese_fmt_json3_to_srt(cn_url) // 格式参考 Youtube-Subtitle-Downloader/fmt=json3/zh-Hans.json
  496. // console.log(cn_srt);
  497.  
  498. // console.log(cn_srt)
  499. // 到了这一步,cn_srt 的每一个 item 应该是:
  500. // var item_example = {
  501. // "startTime": "00:00:06,640",
  502. // "endTime": "00:00:09,760",
  503. // "text": "在与朋友的长时间交谈中以及与陌生人的简短交谈中",
  504. // "tStartMs": 6640,
  505. // "dDurationMs": 3120,
  506. // "words": ["in", " a", " long", " conversation", " with", " a", " friend", " and", "a", " short", " chat", " with", " a", " stranger", "the", " endless", " streams"]
  507. // }
  508.  
  509. // 最后保存下来
  510. var srt_string = auto_sub_dual_language_to_srt(cn_srt) // 结合中文和英文
  511. // console.log(srt_string);
  512. downloadString(srt_string, "text/plain", file_name);
  513. }
  514.  
  515. function auto_sub_dual_language_to_srt(srt_array) {
  516. // var srt_array_item_example = {
  517. // "startTime": "00:00:06,640",
  518. // "endTime": "00:00:09,760",
  519. // "text": "在与朋友的长时间交谈中以及与陌生人的简短交谈中",
  520. // "tStartMs": 6640,
  521. // "dDurationMs": 3120,
  522. // "words": ["in", " a", " long", " conversation", " with", " a", " friend", " and", "a", " short", " chat", " with", " a", " stranger", "the", " endless", " streams"]
  523. // }
  524.  
  525. var result_array = []
  526. for (let i = 0; i < srt_array.length; i++) {
  527. const line = srt_array[i];
  528. var text = line.text; // 中文
  529. // var text = line.text + NEW_LINE + line.words.join(''); // 中文 \n 英文
  530. var item = {
  531. startTime: line.startTime,
  532. endTime: line.endTime,
  533. text: text
  534. }
  535. result_array.push(item)
  536. }
  537.  
  538. var srt_string = object_array_to_SRT_string(result_array)
  539. return srt_string
  540. }
  541.  
  542. // return "English (auto-generated)" or a default name;
  543. function get_auto_subtitle_name() {
  544. const name = "自动字幕"
  545. try {
  546. var json = get_json();
  547. if (typeof json.captions !== "undefined") {
  548. var captionTracks = json.captions.playerCaptionsTracklistRenderer.captionTracks;
  549. for (var index in captionTracks) {
  550. var caption = captionTracks[index];
  551. if (typeof caption.kind === 'string' && caption.kind == 'asr') {
  552. return captionTracks[index].name.simpleText;
  553. }
  554. }
  555. }
  556. return name;
  557. } catch (error) {
  558. console.log(error);
  559. return name;
  560. }
  561. }
  562.  
  563. // Usage: var result = await get(url)
  564. function get(url) {
  565. return $.ajax({
  566. url: url,
  567. type: 'get',
  568. success: function (r) {
  569. return r
  570. },
  571. fail: function (error) {
  572. return error
  573. }
  574. });
  575. }
  576.  
  577.  
  578. // 输入: url (String)
  579. // 输出: SRT (Array)
  580. async function auto_sub_in_chinese_fmt_json3_to_srt(url) {
  581. var srt_array = []
  582.  
  583. var json = await get(url);
  584. var events = json.events;
  585. for (let index = 0; index < events.length; index++) {
  586. const event = events[index];
  587. var tStartMs = event.tStartMs
  588. var dDurationMs = event.dDurationMs
  589. var segs = event.segs
  590. var text = segs[0].utf8;
  591.  
  592. var item = {
  593. startTime: ms_to_srt(tStartMs),
  594. endTime: ms_to_srt(tStartMs + dDurationMs),
  595. text: text,
  596.  
  597. tStartMs: tStartMs,
  598. dDurationMs: dDurationMs,
  599. }
  600. srt_array.push(item);
  601. }
  602. return srt_array
  603. }
  604.  
  605. // 把毫秒转成 srt 时间
  606. // 代码来源网络
  607. function ms_to_srt($milliseconds) {
  608. var $seconds = Math.floor($milliseconds / 1000);
  609. var $minutes = Math.floor($seconds / 60);
  610. var $hours = Math.floor($minutes / 60);
  611. var $milliseconds = $milliseconds % 1000;
  612. var $seconds = $seconds % 60;
  613. var $minutes = $minutes % 60;
  614. return ($hours < 10 ? '0' : '') + $hours + ':' +
  615. ($minutes < 10 ? '0' : '') + $minutes + ':' +
  616. ($seconds < 10 ? '0' : '') + $seconds + ',' +
  617. ($milliseconds < 100 ? '0' : '') + ($milliseconds < 10 ? '0' : '') + $milliseconds;
  618. }
  619.  
  620. /*
  621. Input: [ {startTime: "", endTime: "", text: ""}, {...}, {...} ]
  622. Output: SRT
  623. */
  624. function object_array_to_SRT_string(object_array) {
  625. var result = '';
  626. var BOM = '\uFEFF';
  627. result = BOM + result; // store final SRT result
  628.  
  629. for (var i = 0; i < object_array.length; i++) {
  630. var item = object_array[i]
  631. var index = i + 1;
  632. var start_time = item.startTime
  633. var end_time = item.endTime
  634. var text = item.text
  635.  
  636. var new_line = "\n";
  637. result = result + index + new_line;
  638.  
  639. result = result + start_time;
  640. result = result + ' --> ';
  641. result = result + end_time + new_line;
  642.  
  643. result = result + text + new_line + new_line;
  644. }
  645.  
  646. return result;
  647. }
  648.  
  649. // Copy from: https://gist.github.com/danallison/3ec9d5314788b337b682
  650. // Thanks! https://github.com/danallison
  651. // Work in Chrome 66
  652. // Test passed: 2018-5-19
  653. function downloadString(text, fileType, fileName) {
  654. var blob = new Blob([text], {
  655. type: fileType
  656. });
  657. var a = document.createElement('a');
  658. a.download = fileName;
  659. a.href = URL.createObjectURL(blob);
  660. a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
  661. a.style.display = "none";
  662. document.body.appendChild(a);
  663. a.click();
  664. document.body.removeChild(a);
  665. setTimeout(function () {
  666. URL.revokeObjectURL(a.href);
  667. }, 1500);
  668. }
  669.  
  670. // Input: lang_code like 'en'
  671. // Output: URL (String)
  672. async function get_closed_subtitle_url(lang_code) {
  673. try {
  674. var json = get_json();
  675. var captionTracks = json.captions.playerCaptionsTracklistRenderer.captionTracks;
  676. for (var index in captionTracks) {
  677. var caption = captionTracks[index];
  678. if (caption.languageCode === lang_code && caption.kind != 'asr') {
  679. var url = captionTracks[index].baseUrl;
  680. return url
  681. }
  682. }
  683. } catch (error) {
  684. console.log(error);
  685. return false;
  686. }
  687. }
  688.  
  689. // Input: XML (provide by Youtube)
  690. // Output: Array of object
  691. // each object look like:
  692. /*
  693. {
  694. startTime: "",
  695. endTime: "",
  696. text: ""
  697. }
  698. */
  699. // it's intermediate representation for SRT
  700. function parse_youtube_XML_to_object_list(youtube_xml_string) {
  701. if (youtube_xml_string === '' || youtube_xml_string === undefined || youtube_xml_string === null) {
  702. return false;
  703. }
  704. var result_array = []
  705. var text_nodes = youtube_xml_string.getElementsByTagName('text');
  706. var len = text_nodes.length;
  707. for (var i = 0; i < len; i++) {
  708. var text = text_nodes[i].textContent.toString();
  709. text = text.replace(/(<([^>]+)>)/ig, ""); // remove all html tag.
  710. text = htmlDecode(text);
  711.  
  712. var start = text_nodes[i].getAttribute('start');
  713. var end = '';
  714.  
  715. if (i + 1 >= len) {
  716. end = parseFloat(text_nodes[i].getAttribute('start')) + parseFloat(text_nodes[i].getAttribute('dur'));
  717. } else {
  718. end = text_nodes[i + 1].getAttribute('start');
  719. }
  720.  
  721. var start_time = process_time(parseFloat(start));
  722. var end_time = process_time(parseFloat(end));
  723.  
  724. var item = {
  725. startTime: start_time,
  726. endTime: end_time,
  727. text: text
  728. }
  729. result_array.push(item)
  730. }
  731.  
  732. return result_array
  733. }
  734.  
  735. // return player_response
  736. // or return null
  737. function get_json() {
  738. try {
  739. var json = null
  740. if (ytplayer.config.args.player_response) {
  741. var raw_string = ytplayer.config.args.player_response;
  742. json = JSON.parse(raw_string);
  743. }
  744. if (ytplayer.config.args.raw_player_response) {
  745. json = ytplayer.config.args.raw_player_response;
  746. }
  747. return json
  748. } catch (error) {
  749. return null
  750. }
  751. }
  752. })();