Youtube Full Text (Subtitle Downloader + Viewer)

在线字幕阅读/下载神器! - View full text of the subtitles/captions just online! And even download them as srt files !

  1. // ==UserScript==
  2. // @name Youtube Full Text (Subtitle Downloader + Viewer)
  3. // @namespace https://gf.qytechs.cn/zh-CN/scripts/421483-youtube-full-text
  4. // @version 1
  5. // @description 在线字幕阅读/下载神器! - View full text of the subtitles/captions just online! And even download them as srt files !
  6. // @author KnIfER
  7. // @include https://*youtube.com/*
  8. // ==/UserScript==
  9.  
  10. (function() {
  11. 'use strict';
  12. var loadOnStart = false; /* true false 是否自动分析字幕 */
  13. var pinFTMenu = false; /* true false 是否不自动关闭字幕列表 */
  14. var autoFTM = false; /* true false 是否自动打开字幕列表 */
  15.  
  16. var cssData = "#TextView{position:fixed;bottom:0;left:0;width:100%;height:30px;box-sizing:border-box;background:#fff;z-index: 1000;overflow-y:scroll}#drag_resizer{position:sticky;top:0;right:0;height:6px;width:100%;padding:0;cursor:ns-resize}#ftv{margin-top:9px;margin-left:5px;font-size:x-large;padding:0 100px 0 100px;}a.ft-time:before{content:attr(data-val)}a.ft-time{text-decoration:none;color:blue;user-select:none;-moz-user-select:none}.ft-ln.curr {border-bottom: 2px solid #0000ffac;}ytd-masthead{background: transparent;}";
  17.  
  18. var btnCss = ".ytp-gradient-top,.ytp-chrome-top{opacity:0}.ytp-fulltext-menu{right: 12px;bottom: 53px;z-index: 71;will-change: width,height;}.ytp-fulltext-menu .ytp-menuitem-label{width: 65%;}";
  19.  
  20. // the dialog
  21. var pageData = '<p id="drag_resizer"></p><p id="ftv">CAPTION</p>';
  22.  
  23. var menuData = `<div class="ytp-popup ytp-fulltext-menu" data-layer="6" id="yft-select"
  24. style="width: 251px; height: 137px; display: block;">
  25. <div class="ytp-panel" style="min-width: 250px; width: 251px; height: 137px;" id="yft_cc">
  26. <div class="ytp-panel-menu" role="menu" style="height: 137px;"></div>
  27. </div>
  28. </div>`;
  29.  
  30. // the svg icon from Android Assets && the VectorPathTweaker plugin
  31. var btnData = '<svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"><g class="ytp-fullscreen-button-corner-0"><use class="ytp-svg-shadow" xlink:href="#ytp-id-99"></use><path class="ytp-svg-fill" d="M18.97,18h6.82v1.46h-6.82zM18.97,15.57h6.82L25.8,17.03h-6.82zM18.97,20.43h6.82L25.8,21.89h-6.82zM26.77,10.19L9.23,10.19c-1.07,0 -1.96,0.88 -1.96,1.96v12.67c0,1.07 0.88,1.96 1.96,1.96h17.55c1.07,0 1.96,-0.88 1.96,-1.96L28.73,12.15c0,-1.07 -0.88,-1.96 -1.96,-1.96zM26.77,24.83h-8.77L18,12.15h8.77v12.67z" id="ytp-id-99"></path></g></svg>';
  32.  
  33. // api address
  34. var baseUrl = 'https://video.google.com/timedtext';
  35.  
  36. // the panel, the text, the button
  37. var YFT, ftv, BTN;
  38.  
  39. // the menu
  40. var YFT_menu, YFT_mps;
  41.  
  42. // video tag
  43. var H5Vid;
  44.  
  45. var lrcLoaded;
  46.  
  47. function initBTN(){
  48. if(!BTN){
  49. var doc=document,rct = doc.getElementsByClassName("ytp-right-controls")[0];
  50. if(rct&&rct.firstChild){
  51. // insert a control btn
  52. var e = doc.createElement("style");
  53. e.id = "FTCB"
  54. doc.head.appendChild(e);
  55. e.innerHTML = btnCss;
  56. e = doc.createElement("button");
  57. e.id = "YFTB"
  58. e.className = "ytp-fulltext-button ytp-button";
  59. e.title="Full text (t)";
  60. rct.insertBefore(e,rct.firstChild);
  61. e.innerHTML = btnData;
  62. e.onclick = function() {
  63. if(YFT_mps) {
  64. var st = YFT_mps;
  65. if(st.display!="none") {
  66. st.display="none"
  67. } else {
  68. st.display="";
  69. build_cc_menu()
  70. }
  71. } else {
  72. build_cc_menu()
  73. }
  74. }
  75. BTN=e;
  76. if(autoFTM) {
  77. build_cc_menu()
  78. }
  79. if(loadOnStart) {
  80. // todo load initial lyrics
  81. build_cc_menu(1);
  82. initYFT();
  83. }
  84. } else {
  85. setTimeout(initBTN, 100);
  86. }
  87. }
  88. }
  89.  
  90. function initYFT(H){
  91. if(!YFT) {
  92. var doc=document,item = doc.createElement("style");
  93. item.id = "YFT"
  94. doc.head.appendChild(item);
  95. item.innerHTML = cssData;
  96.  
  97. item=doc.createElement("div");
  98. item.id="TextView";
  99. doc.body.appendChild(item);
  100. item.innerHTML=pageData;
  101. YFT = item;
  102.  
  103. ftv = YFT.children[1];
  104.  
  105. // drag-resize the TextView
  106. //item.onload= ()=> bindResize();
  107. bindResize();
  108.  
  109. function bindResize(){
  110. var tvP = YFT;
  111. var tvPs = tvP.style,
  112. x = 0;
  113. var el = drag_resizer;
  114. function mousedown(e){
  115. if(e.clientY==undefined)
  116. e.clientY=e.originalEvent.changedTouches[0].clientY;
  117. x = e.clientY + tvP.offsetHeight;
  118. e.preventDefault()
  119. document.addEventListener("mousemove", mouseMove); document.addEventListener("mouseup", mouseUp);
  120. };
  121. function mouseMove(e){
  122. if(e.clientY==undefined)
  123. e.clientY=e.originalEvent.changedTouches[0].clientY;
  124. tvPs.height = x - e.clientY + 'px';
  125. }
  126. function mouseUp(){
  127. document.removeEventListener("mousemove", mouseMove); document.removeEventListener("mouseup", mouseUp);
  128. }
  129.  
  130. el.addEventListener("mousedown", mousedown);
  131. el.addEventListener("touchstart", mousedown);
  132. el.addEventListener("touchmove", mouseMove);
  133. el.addEventListener("touchend", mouseUp);
  134. }
  135. installTimers();
  136. //var insertionLis = e => {
  137. // //console.log("DOMNodeInserted")
  138. // if(document.body.lastElementChild!=YFT){
  139. // document.body.removeChild(YFT);
  140. // document.body.appendChild(YFT);
  141. // }
  142. //};
  143. //document.body.addEventListener('DOMNodeInserted', insertionLis)
  144. }
  145. ensureFTH(H||30)
  146. }
  147.  
  148. function ensureFTH(e){
  149. // ensure visibility
  150. if(YFT){
  151. var h = parseFloat(YFT.style.height);
  152. if(h!=h||h<e) {
  153. YFT.style.height = e+"px"
  154. }
  155. if(YFT.style.display!=="") {
  156. YFT.style.display = ""
  157. }
  158. }
  159. }
  160.  
  161. /*via mdict-js*/
  162. function reduce(val,arr,st,ed) {
  163. var len = ed-st;
  164. if (len > 1) {
  165. len = len >> 1;
  166. return val > arr[st + len - 1].endTime
  167. ? reduce(val,arr,st+len,ed)
  168. : reduce(val,arr,st,st+len);
  169. } else {
  170. return arr[st];
  171. }
  172. }
  173.  
  174. function installTimers(){
  175. if(H5Vid==null) {
  176. H5Vid=document.querySelector('video')
  177. if(H5Vid==null) {
  178. setTimeout(initTimer, 100)
  179. } else {
  180. H5Vid.addEventListener('timeupdate', e => {
  181. // lyrics scroll sync to time
  182. var tm=H5Vid.currentTime;
  183. if(lrcArr&&(!lcN||tm>=lcN.endTime||tm<lcN.startTime)) {
  184. var n = reduce(tm,lrcArr,0,lrcArr.length);
  185. if(n&&n!=lcN) {
  186. lcN = n;
  187. if(lcE) {
  188. lcE.className="ft-ln";
  189. }
  190. n = n.ele;
  191. lcE = n;
  192. if(n) {
  193. n.className+=" curr";
  194. }
  195. if(window.getSelection().isCollapsed
  196. &&(n.offsetTop+n.offsetHeight>TextView.scrollTop+TextView.offsetHeight
  197. ||n.offsetTop<TextView.scrollTop)) {
  198. TextView.scrollTop=n.offsetTop;
  199. //TextView.scrollTo(n.offsetTop);
  200. }
  201. }
  202. }
  203. })
  204. window.addEventListener("click", function(e){
  205. if(e.srcElement.className==="ft-time") {
  206. e.preventDefault();
  207. H5Vid.currentTime=parseFloat(e.srcElement.getAttribute("data-tm"));
  208. }
  209. });
  210. }
  211. }
  212. }
  213.  
  214. // http://qtdebug.com/fe-srt/
  215. function parseSrtSubtitles(srt) {
  216. var subtitles = [];
  217. var textSubtitles = srt.split('\n\n'); // 每条字幕的信息,包含了序号,时间,字幕内容
  218. for (var i = 0; i < textSubtitles.length; ++i) {
  219. var textSubtitle = textSubtitles[i].split('\n');
  220. if (textSubtitle.length >= 2) {
  221. var sn = textSubtitle[0];
  222. var tms = textSubtitle[1].split(' --> ');
  223. var startTime = toSeconds(tms[0]);
  224. var endTime = toSeconds(tms[1]);
  225. var content = textSubtitle[2];
  226. // 字幕可能有多行
  227. if (textSubtitle.length > 2) {
  228. for (var j = 3; j < textSubtitle.length; j++) {
  229. content += ' ' + textSubtitle[j];
  230. }
  231. }
  232. // 你没有对象
  233. var subtitle = {
  234. sn: sn,
  235. startTime: startTime,
  236. endTime: endTime,
  237. content: content
  238. };
  239. subtitles.push(subtitle);
  240. }
  241. }
  242. return subtitles;
  243. }
  244.  
  245. function toSeconds(t) {
  246. var s = 0.0;
  247. if (t) {
  248. var p = t.trim().split(':');
  249. for (var i = 0; i < p.length; i++) {
  250. s = s * 60 + parseFloat(p[i].replace(',', '.'));
  251. }
  252. }
  253. return s;
  254. }
  255.  
  256. var lrcArr;
  257. var lcN, lcE;
  258.  
  259. // Append full text.
  260. function APFT(e, d) {
  261. //console.log("APFT", e, d);
  262. var lrc = e.srt;
  263. if(d) {
  264. var t=document.getElementsByTagName("H1")[0];
  265. if(t)t=t.innerText;
  266. else t=document.title;
  267. downloadString(lrc, "text/plain", t+"."+(e.lang_code||"a")+".srt");
  268. return;
  269. }
  270. unsafeWindow.srtlrc=e;
  271. // parse
  272. var lrcs = parseSrtSubtitles(lrc);
  273. var span="";
  274. var lastTime=0;
  275. // concatenate
  276. for(var i=0;i<lrcs.length;i++){
  277. var lI=lrcs[i];
  278. var text = lI.content;
  279. var lnSep="<br><br>";
  280. var sepLn="";
  281. if(lI.startTime-lastTime>3){
  282. var idx = text.indexOf(".");
  283. // skip numberic dots
  284. while(idx>0) {
  285. if(idx+1>=text.length||text[idx+1]<=' ') {
  286. break;
  287. }
  288. idx = text.indexOf(".", idx+1);
  289. }
  290. if(idx<0) idx = text.indexOf("。");
  291. if(idx<0) idx = text.indexOf(",");
  292. if(idx<0) idx = text.indexOf(",");
  293. if(idx>=0) {
  294. text=" "+text.substring(0, idx+1)
  295. +lnSep+text.substring(idx+1);
  296. } else {
  297. sepLn = lnSep;
  298. }
  299. lnSep = " ";
  300. } else {
  301. // merge to previous line
  302. text="&nbsp;"+text;
  303. lnSep = "";
  304. }
  305.  
  306. //console.log(lI.startTime-lastTime);
  307. var s = lI.startTime;
  308. var m = parseInt(lI.startTime/60);
  309. span+=sepLn+"<a class='ft-time' href='' data-val='" + " "
  310. +(m+":"+parseInt(s-m*60))+lnSep+"' data-tm='"+s+"'></a>"
  311. +"<span class='ft-ln'>"+text+"</span>"
  312. lastTime = lI.startTime;
  313. }
  314. ftv.innerHTML=span;
  315.  
  316. // attach ele to array
  317. lrcArr = lrcs;
  318. lcN = 0;
  319. var cc=0;
  320. var sz = ftv.childElementCount;
  321. for(var i=0;i<sz,cc<lrcArr.length;i++) {
  322. if(ftv.children[i].className==="ft-ln") {
  323. lrcArr[cc++].ele=ftv.children[i];
  324. }
  325. }
  326. window.lrcArr=lrcArr;
  327. //console.log(lrcArr);
  328. }
  329.  
  330. //window.APFT = APFT;
  331.  
  332. initBTN();
  333.  
  334. unsafeWindow.yft_captions = []; // store all subtitle
  335.  
  336.  
  337. // trigger when loading new page
  338. // (actually this would also trigger when first loading, that's not what we want, that's why we need to use firsr_load === false)
  339. // (new Material design version would trigger this "yt-navigate-finish" event. old version would not.)
  340. var body = document.getElementsByTagName("body")[0];
  341. body.addEventListener("yt-navigate-finish", function (event) {
  342. if (is_video_page()&&autoFTM) {
  343. if(build_cc_menu()) {
  344. var st = YFT_mps;
  345. if(st.display!="") {
  346. st.display=""
  347. }
  348. }
  349. }
  350. });
  351.  
  352. // trigger when loading new page
  353. // (old version would trigger "spfdone" event. new Material design version not sure yet.)
  354. window.addEventListener("spfdone", function (e) {
  355. //if (is_video_page()) {
  356. // remove_dwnld_btn();
  357. // var checkExist = setInterval(function () {
  358. // if (unsafeWindow.watch7_headline) {
  359. // init();
  360. // clearInterval(checkExist);
  361. // }
  362. // }, 330);
  363. //}
  364. });
  365.  
  366. function is_video_page() {
  367. return get_vid() !== null;
  368. }
  369.  
  370. function get_vid() {
  371. return getURLParameter('v');
  372. }
  373.  
  374. //https://stackoverflow.com/questions/11582512/how-to-get-url-parameters-with-javascript/11582513#11582513
  375. function getURLParameter(name) {
  376. return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
  377. }
  378.  
  379. // https://stackoverflow.com/questions/32142656/get-youtube-captions#58435817
  380. function buildXmlurl(videoId, loc) {
  381. return `${baseUrl}?lang=${loc}&v=${videoId}`;//&fmt=json3
  382. }
  383.  
  384. // pull the selected caption.
  385. function pullLyrics(e, d) {
  386. var url;
  387. if(e==0) {
  388. console.log("auto");
  389. url = get_auto_xml_url();
  390. console.log("auto", url);
  391. }
  392. e = yft_captions[e]
  393. if(e) {
  394. if(!e.srt)
  395. fetch(url||buildXmlurl(get_vid(), e.lang_code))
  396. .then(v => v.text())
  397. .then(v => (new window.DOMParser()).parseFromString(v, "text/xml"))
  398. .then(v => {
  399. v = buildSrtFromXML(v);
  400. e.srt = v;
  401. APFT(e, d)
  402. })
  403. else APFT(e, d)
  404. }
  405. }
  406.  
  407. function buildMenu(e){
  408. return `<div class="ytp-menuitem" aria-haspopup="true" role="menuitem" tabindex="${e.cid||0}">
  409. <div class="ytp-menuitem-icon"></div>
  410. <div class="ytp-menuitem-label">
  411. ${e.lang_name}
  412. </div>
  413. <div class="ytp-menuitem-content">
  414. 下载
  415. </div>
  416. </div>`;
  417. }
  418.  
  419. function menuic(e){
  420. var t = e.target;
  421. var i = parseInt(t.parentNode.getAttribute("tabindex"));
  422. if(i==i) {
  423. if(t.className==="ytp-menuitem-content") {
  424. // 下载
  425. pullLyrics(i, 1);
  426. } else {
  427. // 查看
  428. initYFT(120);
  429. pullLyrics(i);
  430. }
  431. }
  432. t.blur();
  433. if(!pinFTMenu) {
  434. YFT_mps.display="none";
  435. }
  436. }
  437. var lastVid;
  438.  
  439. function build_cc_menu(src) {
  440. var vid = get_vid();
  441. if(vid==lastVid) {
  442. return false;
  443. }
  444. lastVid=vid;
  445. if(loadOnStart) {
  446. src=1;
  447. }
  448. // todo validify auto caption exists
  449. if(!YFT_menu&&unsafeWindow.movie_player) {
  450. var item = document.createElement("div");
  451. movie_player.appendChild(item);
  452. item.innerHTML = menuData;
  453. YFT_mps = item.style;
  454. YFT_menu = unsafeWindow.yft_cc;
  455. if(src==1&&!autoFTM) {
  456. YFT_mps.display = "none";
  457. }
  458. }
  459. if(YFT_menu) {
  460. YFT_menu.innerHTML = "";
  461. var list_url = `${baseUrl}?hl=en&v=${vid}&type=list`;
  462. console.log("loading_list::", list_url);
  463. makeRequest('GET',list_url, function (xhr) {
  464. // todo auto select if requested
  465. try{
  466. yft_captions = [];
  467. var tracks = xhr.responseXML.getElementsByTagName('track');
  468. xhr="";
  469. var autosel=-1;
  470. for (var i = 0, len = tracks.length, xml, ety; i <= len; i++) {
  471. if(i==0) {
  472. ety={lang_code:'AUTO',lang_name:'AUTO'}
  473. } else {
  474. xml = tracks[i-1];
  475. ety = {
  476. lang_code: xml.getAttribute('lang_code'),
  477. lang_name: xml.getAttribute('lang_original')
  478. ||xml.getAttribute('lang_translated'),
  479. cid:i
  480. }
  481. if(src==1&&xml.getAttribute('lang_default')) {
  482. autosel=i;
  483. src=0;
  484. }
  485. }
  486. yft_captions.push(ety); // 加到 yft_captions, 待会靠它下载
  487. xhr+=buildMenu(ety);
  488. }
  489. if(src==1) {
  490. autosel=0;
  491. }
  492. console.log("autosel", autosel);
  493. YFT_menu.innerHTML=xhr;
  494. var cc = YFT_menu.children;
  495. for (var i = 0, len = cc.length; i < len; i++) {
  496. cc[i].onclick = menuic;
  497. if(autosel==i) {
  498. initYFT(120);
  499. pullLyrics(i);
  500. }
  501. }
  502. } catch(e) {console.log(e)}
  503. });
  504. } else {
  505. lastVid="";
  506. }
  507. return true;
  508. }
  509.  
  510. // 处理时间. 比如 start="671.33" start="37.64" start="12" start="23.029"
  511. // 处理成 srt 时间, 比如 00:00:00,090 00:00:08,460 00:10:29,350
  512. function process_time(s) {
  513. s = s.toFixed(3);
  514. // 超棒的函数, 不论是整数还是小数都给弄成3位小数形式
  515. // 举个柚子:
  516. // 671.33 -> 671.330
  517. // 671 -> 671.000
  518. // 注意函数会四舍五入. 具体读文档
  519.  
  520. var array = s.split('.');
  521. // 把开始时间根据句号分割
  522. // 671.330 会分割成数组: [671, 330]
  523.  
  524. var Hour = 0;
  525. var Minute = 0;
  526. var Second = array[0]; // 671
  527. var MilliSecond = array[1]; // 330
  528. // 先声明下变量, 待会把这几个拼好就行了
  529.  
  530. // 我们来处理秒数. 把"分钟"和"小时"除出来
  531. if (Second >= 60) {
  532. Minute = Math.floor(Second / 60);
  533. Second = Second - Minute * 60;
  534. // 把 秒 拆成 分钟和秒, 比如121秒, 拆成2分钟1秒
  535.  
  536. Hour = Math.floor(Minute / 60);
  537. Minute = Minute - Hour * 60;
  538. // 把 分钟 拆成 小时和分钟, 比如700分钟, 拆成11小时40分钟
  539. }
  540. // 分钟,如果位数不够两位就变成两位,下面两个if语句的作用也是一样。
  541. if (Minute < 10) {
  542. Minute = '0' + Minute;
  543. }
  544. // 小时
  545. if (Hour < 10) {
  546. Hour = '0' + Hour;
  547. }
  548. // 秒
  549. if (Second < 10) {
  550. Second = '0' + Second;
  551. }
  552. return Hour + ':' + Minute + ':' + Second + ',' + MilliSecond;
  553. }
  554.  
  555. // copy from: https://gist.github.com/danallison/3ec9d5314788b337b682
  556. // Thanks! https://github.com/danallison
  557. // work in Chrome 66
  558. // test passed: 2018-5-19
  559. function downloadString(text, fileType, fileName) {
  560. var blob = new Blob([text], {type: fileType});
  561. var a = document.createElement('a');
  562. a.download = fileName;
  563. a.href = URL.createObjectURL(blob);
  564. a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
  565. a.style.display = "none";
  566. document.body.appendChild(a);
  567. a.click();
  568. document.body.removeChild(a);
  569. setTimeout(function () {
  570. URL.revokeObjectURL(a.href);
  571. }, 1500);
  572. }
  573.  
  574. // https://css-tricks.com/snippets/javascript/unescape-html-in-js/
  575. // turn HTML entity back to text, example: &quot; should be "
  576. function htmlDecode(input) {
  577. var e = document.createElement('div');
  578. e.class = 'dummy-element-for-tampermonkey-Youtube-Subtitle-Downloader-script-to-decode-html-entity';
  579. e.innerHTML = input;
  580. return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
  581. }
  582.  
  583. // return URL or null;
  584. // later we can send a AJAX and get XML subtitle
  585. function get_auto_xml_url() {
  586. try {
  587. var captionTracks = get_captionTracks()
  588. for (var index in captionTracks) {
  589. var caption = captionTracks[index];
  590. if (caption.kind === 'asr') {
  591. return captionTracks[index].baseUrl;
  592. }
  593. // ASR – A caption track generated using automatic speech recognition.
  594. // https://developers.google.com/youtube/v3/docs/captions
  595. }
  596. return false;
  597. } catch (e) {
  598. console.log(e);
  599. return false;
  600. }
  601. }
  602. async function get_auto_subtitle() {
  603. var url = get_auto_xml_url();
  604. console.log("dwnld_auto_url::", url);
  605. if (url == false) {
  606. return false;
  607. }
  608. var result = await getUrl(url)
  609. return result
  610. }
  611.  
  612. // Youtube return XML. we want SRT
  613. // input: Youtube XML format
  614. // output: SRT format
  615. function buildSrtFromXML(youtube_xml_string) {
  616. if (youtube_xml_string === '') {
  617. return false;
  618. }
  619. var text = youtube_xml_string.getElementsByTagName('text');
  620. var result = '';
  621. var BOM = '\uFEFF';
  622. result = BOM + result; // store final SRT result
  623. var len = text.length;
  624. for (var i = 0; i < len; i++) {
  625. var index = i + 1;
  626. var content = text[i].textContent.toString();
  627. content = content.replace(/(<([^>]+)>)/ig, ""); // remove all html tag.
  628. var start = text[i].getAttribute('start');
  629. var end = parseFloat(text[i].getAttribute('start')) + parseFloat(text[i].getAttribute('dur'));
  630. var new_line = "\n";
  631. result = result + index + new_line;
  632. // 1
  633. if (i + 1 >= len) {
  634. end = parseFloat(text[i].getAttribute('start')) + parseFloat(text[i].getAttribute('dur'));
  635. } else {
  636. end = text[i + 1].getAttribute('start');
  637. }
  638.  
  639. var start_time = process_time(parseFloat(start));
  640. var end_time = process_time(parseFloat(end));
  641. result = result + start_time;
  642. result = result + ' --> ';
  643. result = result + end_time + new_line;
  644. // 00:00:01,939 --> 00:00:04,350
  645.  
  646. content = htmlDecode(content);
  647. // turn HTML entity back to text. example: &#39; back to apostrophe (')
  648.  
  649. result = result + content + new_line + new_line;
  650. // everybody Craig Adams here I'm a
  651. }
  652. return result;
  653. }
  654.  
  655. function get_captionTracks() {
  656. var json = null
  657. if (unsafeWindow.youtube_playerResponse_1c7) {
  658. json = youtube_playerResponse_1c7;
  659. } else if(ytplayer.config.args.player_response) {
  660. let raw_string = ytplayer.config.args.player_response;
  661. json = JSON.parse(raw_string);
  662. } else if (ytplayer.config.args.raw_player_response) {
  663. json = ytplayer.config.args.raw_player_response;
  664. }
  665. let captionTracks = json.captions.playerCaptionsTracklistRenderer.captionTracks;
  666. return captionTracks
  667. }
  668.  
  669. // https://stackoverflow.com/questions/48969495/in-javascript-how-do-i-should-i-use-async-await-with-xmlhttprequest
  670. function makeRequest(method, url, load) {
  671. return new Promise(function (resolve, reject) {
  672. let xhr = new XMLHttpRequest();
  673. xhr.open(method, url);
  674. xhr.timeout = 2000;
  675. xhr.onload = function () {
  676. if (this.status >= 200 && this.status < 300) {
  677. if(load) {
  678. load(xhr);
  679. resolve('');
  680. } else {
  681. resolve(xhr.responseXML);
  682. }
  683. } else {
  684. reject({
  685. status: this.status,
  686. statusText: xhr.statusText
  687. });
  688. }
  689. };
  690. xhr.onerror = function () {
  691. reject({
  692. status: this.status,
  693. statusText: xhr.statusText
  694. });
  695. };
  696. xhr.send();
  697. });
  698. }
  699. async function getUrl(url) {
  700. return makeRequest("GET", url);
  701. }
  702. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址