B站学习机 | Bilibili+Youtube字幕全文阅读 | coursera-like subtitles fulltext reader

在线字幕阅读或下载,B站油管秒变cousera! - Read & learn subtitles full text online!

  1. // ==UserScript==
  2. // @name B站学习机 | Bilibili+Youtube字幕全文阅读 | coursera-like subtitles fulltext reader
  3. // @namespace https://gist.github.com/KnIfER/9e43ffa31c3b9831a500edf35595c1dc
  4. // @version 6
  5. // @description 在线字幕阅读或下载,B站油管秒变cousera! - Read & learn subtitles full text online!
  6. // @author KnIfER
  7. // @match https://*.bilibili.com/video/*
  8. // @match https://*.youtube.com/*
  9. // @match https://*/watch?v=*
  10. // @match https://*/embed/*?si=*
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. // (function() {
  16. var trustedPolicy = trustedTypes.createPolicy("myPolicy", {
  17. createHTML: (s) => s
  18. });
  19. 'use strict';
  20. var lastVid='x';
  21. var win = window.unsafeWindow || window, doc=document, d=doc
  22. , bank=win.parent._xxj_bank
  23. , isBY = location.host.indexOf('bilibili')>=0?0:1
  24. , isYProxy = e=>parent.advancedVideoPlayer && parent.advancedVideoPlayerContainer // YOUTUBEUNBLOCKED
  25. , Data;
  26. if(!bank) {
  27. bank = win.parent._xxj_bank = {};
  28. } else try{
  29. lastVid = bank.unreg();
  30. } catch(e) {}
  31. bank.unreg = uninstall;
  32. // unregister the script for hot reload
  33. var unregs = [];
  34. function uninstall() {
  35. if(Btn) Btn.remove();
  36. if(TextPane) TextPane.remove();
  37. //
  38. Btn = 0;
  39. Menu = 0;
  40. TextPane = 0;
  41. //
  42. if(isBY==0) {
  43. proto.open = proto.realOpen;
  44. proto.send = proto.realSend;
  45. }
  46. var tmp = ge('_xxj_sty');
  47. if(tmp) tmp.remove();
  48. for(var i=0;i<unregs.length;i++) {
  49. unregs[i]();
  50. }
  51. return lastVid;
  52. }
  53.  
  54. function debug(a,b,c,d,e){var t=[a,b,c,d,e];for(var i=5;i>=0;i--){if(t[i]===undefined)t[i]='';else break}console.log("%c 学习机 ","color:#eee!important;background:#0FF;",t[0],t[1],t[2],t[3],t[4])}
  55. function gc(c, d) {
  56. return (d||document).getElementsByClassName(c)[0];
  57. }
  58. function gt(c, d) {
  59. return (d||document).getElementsByTagName(c)[0];
  60. }
  61. function ge(id) {
  62. return document.getElementById(id);
  63. }
  64. function gcs(c, d) {
  65. return (d||document).getElementsByClassName(c);
  66. }
  67. function gts(c, d) {
  68. return (d||document).getElementsByTagName(c);
  69. }
  70. function gcp(c, d, max) {
  71. var p = d||document;
  72. if(!max) max=99999;
  73. while(p) {
  74. if(p.classList && p.classList.contains(c)) return p;
  75. p = p.parentNode;
  76. if(--max<=0) return null;
  77. }
  78. return p;
  79. }
  80. function addEvent(a, b, c, d) {
  81. if(!d) d = win;
  82. d.addEventListener(a, b, c);
  83. unregs.push(function(){ d.removeEventListener(a, b, c)} );
  84. }
  85. function delEvent(a, b, c, d) {
  86. if(!d) d = win;
  87. d.removeEventListener(a, b, c);
  88. }
  89. function stop(e) {
  90. try{
  91. e.stopPropagation();
  92. e.preventDefault();
  93. } catch(e) {debug(e)}
  94. }
  95. function editing() {
  96. var act = document.activeElement;
  97. if(act)
  98. return act.tagName==='INPUT'
  99. || act.contentEditable==='true'
  100. || act.tagName==='TEXTAREA'
  101. }
  102.  
  103. var proto = XMLHttpRequest.prototype, html=e=>e;
  104. if(isBY==0) {
  105. proto.realOpen = proto.open;
  106. proto.open = function(method, url, a, u, p) {
  107. //debug('request::open!!!', url);
  108. this.realOpen(method, url , true, u, p); // set async to true to avoid 'sync responseType error'
  109. if(url) {
  110. var tmp = new RegExp('(aid=[0-9]+&cid=[0-9]+)').exec(url);
  111. if(tmp) tmp = tmp[0];
  112. if(tmp && lastVid!=tmp) {
  113. lastVid = tmp;
  114. debug('正在播放='+lastVid);
  115. }
  116. }
  117. };
  118. proto.realSend = proto.send;
  119. proto.send = function(b) {
  120. //debug('request::send!!!', b);
  121. this.realSend(b);
  122. };
  123. } else {
  124. html=e=>trustedPolicy.createHTML(e);
  125. }
  126. // 动态z-order,配合B站笔记窗口
  127. var zIndexes = ['1500', '10000'];
  128. if(isBY==1) {
  129. zIndexes = ['2030', '10000'];
  130. }
  131.  
  132. var loadOnStart = false; /* true false 是否自动分析字幕 */
  133. var autoFTM = false; /* true false 是否自动打开字幕列表 */
  134. // the panel, textview, and the button
  135. var TextPane, tv, Btn, installTryCnt=0
  136. , autoScroling, userScrollTm=0
  137. , moved, focused=0
  138. // the menu
  139. , Menu, MenuSty
  140. // video tag
  141. , Vid
  142. ;
  143. function ge(e,p){return (p||doc).getElementById(e)};
  144. function gc(e,p){return (p||doc).getElementsByClassName(e)[0]};
  145. function craft(p, t, c) {
  146. var e=doc.createElement(t||'DIV');
  147. if(c)e.className=c;
  148. if(p)p.appendChild(e);
  149. return e;
  150. }
  151. function installBtn(){
  152. if(!Btn || !Btn.parentNode){
  153. var ibf = 0, tmp;
  154. if(isBY==0) {
  155. ibf = doc.getElementsByClassName("bpx-player-ctrl-subtitle")[0];
  156. if(!ibf) ibf = doc.getElementsByClassName("bpx-player-ctrl-volume")[0];
  157. if(ibf) ibf = ibf.nextElementSibling;
  158. } else {
  159. ibf = doc.getElementsByClassName("ytp-settings-button")[0];
  160. if(!ibf) {
  161. tmp = doc.getElementsByClassName("slim_video_action_bar_renderer_button");
  162. ibf = tmp[tmp.length-1];
  163. }
  164. }
  165. debug('insertBefore', ibf, installTryCnt);
  166. if(ibf) {
  167. // insert a control BUTTON
  168. tmp = craft(doc.head, "STYLE");
  169. tmp.id = "_xxj_sty"
  170. tmp.textContent = ".ytp-gradient-top,.ytp-chrome-top{opacity:0}.ytp-fulltext-menu{right: 12px;bottom: 53px;z-index: 71;will-change: width,height;}._xxj_menu .ytp-menuitem-label{width:65%;}._xxj_menu{user-select:none}";
  171. if(isBY==0) {
  172. tmp.textContent+=".ytp-menuitem>div{display:inline-block;font-size:medium}.ytp-menuitem-label{cursor:pointer}";
  173. }
  174. if(isBY==1) {
  175. tmp.textContent+="._xxj_menu .ytp-menuitem-label{width:65%;white-space:nowrap;font-size:100%;}._xxj_menu .ytp-menuitem-content{white-space:nowrap;font-size:100%;}";
  176. }
  177. tmp = craft(0, isBY==1?'BUTTON':'DIV', "ytp-fulltext-button ytp-button bpx-player-ctrl-btn");
  178. tmp.id = "_xxj_btn"
  179. tmp.title="字幕学习机 (x)";
  180. // button svg icon
  181. debug('create the control button')
  182. tmp.innerHTML = html('<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>'
  183. );
  184. tmp.onclick = function() {
  185. if(MenuSty) {
  186. tmp = MenuSty;
  187. if(tmp.display!="none") {
  188. tmp.display="none"
  189. } else {
  190. tmp.display="";
  191. build_cc_menu()
  192. }
  193. } else {
  194. build_cc_menu()
  195. }
  196. }
  197. var ts = tmp.style;
  198. if(isBY==0) {
  199. ts.maxHeight='30px'
  200. tmp.firstElementChild.style = "transform:scale(1.5);"
  201. }
  202. if(isBY==1) {
  203. if(location.host[0]=='m') {
  204. ts.marginTop='.5%';
  205. ts.minWidth='25px';
  206. gc('ytp-svg-fill', tmp).style.fill='#000';
  207. }
  208. }
  209. ibf.parentNode.insertBefore(tmp, ibf);
  210. Btn=tmp;
  211. debug('成功安装按钮:', tmp);
  212. // if(autoFTM) {
  213. // build_cc_menu()
  214. // }
  215. // if(loadOnStart) {
  216. // // todo load initial lyrics
  217. // build_cc_menu(1);
  218. // initYFT();
  219. // }
  220. } else if(installTryCnt++<15){
  221. setTimeout(installBtn, 500);
  222. }
  223. }
  224. }
  225. function tvShown(){
  226. return TextPane && TextPane.style.display!='none';
  227. }
  228.  
  229. var keysDwn=[];
  230. let pdoc = doc;
  231. if(isYProxy()) {
  232. pdoc = parent.document;
  233. }
  234. function fnKeydown(e){
  235. //debug('fnKeydown', tvShown(), e.code, e.code==="KeyX", e.altKey)
  236. if(!keysDwn[e.code]) {
  237. keysDwn[e.code] = e;
  238. if(!editing())
  239. if(focused || tvShown()) {
  240. if(focused) {
  241. if(e.code==="Escape") {
  242. TextPane.close();
  243. stop(e);
  244. }
  245. }
  246. // if(userScrollTm && e.code==="ArrowRight" && e.code==="ArrowLeft") {
  247. // userScrollTm = 0;
  248. // debug('userScrollTm = 0');
  249. // }
  250. if(e.code==="KeyX"/* && e.altKey */) {
  251. TextPane.close();
  252. }
  253. }
  254. else if(e.code==="KeyX"/* && e.altKey */) {
  255. installTextPane().style.display = "";
  256. }
  257. }
  258. }
  259. function fnKeyup(e){
  260. delete keysDwn[e.code];
  261. }
  262. addEvent("keydown", fnKeydown, 1, pdoc);
  263. addEvent("keyup", fnKeyup, 1, pdoc);
  264. if(pdoc!=doc) {
  265. addEvent("keydown", fnKeydown, 1, doc);
  266. addEvent("keyup", fnKeyup, 1, doc);
  267. }
  268. function insertTextToEl(){
  269. if(isBY==0) {
  270. return gc('bpx-player-container');
  271. } else {
  272. return isYProxy()&&parent['player-container-inner']
  273. }
  274. }
  275. function installTextPane(H){
  276. if(!TextPane) {
  277. craft(pdoc.head, "STYLE").innerHTML = html("a.ft-time:before{content:attr(data-val)}a.ft-time{text-decoration:none;color:blue;user-select:none;-moz-user-select:none}._xxj_ft_ln.curr {border-bottom: 5px solid #0000ffac;}ytd-masthead{background: transparent;}._xxj_btn:hover{ box-shadow: 1px 1px 2px 1px rgb(0 0 0 / 15%); }._xxj_btn:active{ box-shadow: inset 1px 1px 2px 1px rgb(0 0 0 / 15%);}"
  278. + ".bpx-player-container[data-screen=full], .bpx-player-container[data-screen=web] {z-index: 1500!important;}"
  279. + "#bilibili-player.mode-webscreen {z-index: 1500!important;}"
  280. // + "._xxj_tv {display: none;}"
  281. );
  282. if(isYProxy() && win==parent) {
  283. return;
  284. }
  285. // the lyrics display float window.
  286. var advPlayer = insertTextToEl();
  287. TextPane=craft(advPlayer||pdoc.body,0,"_xxj_tv");
  288. TextPane.innerHTML=html('<p class="drag_resizer"></p><div class="_xxj_tvp"><p class="_xxj_ftv">FETCHING……</p></div>');
  289. tv = gc('_xxj_ftv', TextPane);
  290. tv.style = 'margin-left:5px;font-size:x-large;padding:9px 100px 0 100px;';
  291. var tvP = gc('_xxj_tvp', TextPane)
  292. , tvPs = TextPane.style
  293. , x = 0
  294. , minHeight = 1.35 * (parseInt(getComputedStyle(tv).lineHeight)||tv.offsetHeight);
  295. ;
  296. tvPs.zIndex = zIndexes[1];
  297. tvP.style = 'overflow-y:scroll;height:100%;';
  298. TextPane.style='position:fixed;bottom:0;left:0;width:100%;height:'+minHeight+'px;box-sizing:border-box;background:#fff;z-index:10000;overflow:hidden;transition:background 0.25s';
  299. // the play button.
  300. var playBtn = craft(TextPane);
  301. playBtn.style = 'position:fixed;height:70px;width:80px;bottom:0;';
  302. playBtn.innerHTML = html('<svg style="background-color:#fff;fill: #03a9f4ab;width: 60px;border-radius: 4px;" class="_xxj_btn" height="100%" version="1.1" viewBox="0 0 36 36" width="100%"><g><path class="btnPlay" d="M12,7.5v21l16.5,-10.5z"></path><path class="btnPause" d="M9,7.5h6v21L9,28.5zM21,7.5h6v21h-6z"></path></g></svg>');
  303. var btnPause = gc('btnPause', playBtn), btnPlay = gc('btnPlay', playBtn);
  304. function syncPlay(p) {
  305. btnPause.style.display=p?'':'none';
  306. btnPlay.style.display=p?'none':'';
  307. playBtn.title = p?'暂停':'播放';
  308. }
  309. playBtn = playBtn.children[0];
  310. function togglePlay(){
  311. var p = Vid.playing;
  312. if(p) Vid.pause();
  313. else Vid.play();
  314. syncPlay(Vid.playing);
  315. }
  316. addEvent('click', togglePlay, true, playBtn)
  317. addEvent('contextmenu', playBtn.ctx = function(e) {
  318. lcN=userScrollTm=0;
  319. timeUpdate();
  320. stop(e);
  321. }, true, playBtn)
  322. //
  323. // float window control buttons on the top-right corner.
  324. //
  325. var topBtns = craft(TextPane);
  326. topBtns.style='user-select:none;padding-right:0.25em;font-weight:600;text-decoration:none;position:absolute;top:0;right:20px;font-size:17px;';
  327. // the close button.
  328. var closeBtn = craft(topBtns, 'A', 'notranslate');
  329. closeBtn.innerText = '[X]';
  330. closeBtn.style = 'color:#175199;';
  331. closeBtn.title = '关闭';
  332. closeBtn.onclick = TextPane.close = function(){
  333. tvPs.display = 'none';
  334. focused = 0;
  335. }
  336. // the maximise button.
  337. craft(topBtns, 'DIV').style='height:5px;';
  338. var maxBtn = craft(topBtns, 'A', 'notranslate');
  339. maxBtn.innerText = '[▢]';
  340. maxBtn.style = 'color:#175199;';
  341. maxBtn.title = '最大化';
  342. maxBtn.onclick = function(){
  343. }
  344. // the opacity button.
  345. craft(topBtns, 'DIV').style='height:5px;';
  346. var zenBtn = craft(topBtns, 'A', 'notranslate');
  347. zenBtn.innerText = '[⊥]';
  348. zenBtn.style = 'color:#175199;transform:rotate(180deg);position:absolute;';
  349. zenBtn.title = '透明背景';
  350. zenBtn.onclick = function(){
  351. if(tvPs.background=='rgb(255, 255, 255)')
  352. tvPs.background = 'rgb(255, 255, 255, 0.55)'
  353. else
  354. tvPs.background = 'rgb(255, 255, 255)'
  355. debug(tvPs.background);
  356. }
  357. // drag-resize the TextView, bindResize
  358. if(1) {
  359. var el = gc('drag_resizer', TextPane);
  360. el.style = 'position:absolute;top:0;right:0;height:6px;width:100%;padding:0;cursor:ns-resize';
  361. function y(e){
  362. if(e.clientY==undefined)
  363. return e.originalEvent.changedTouches[0].clientY;
  364. return e.clientY;
  365. }
  366. // drag-resie area on the top
  367. function mousedown(e){
  368. x = y(e) + tvP.offsetHeight;
  369. stop(e);
  370. addEvent("mousemove", mouseMove, 1, pdoc);
  371. addEvent("mouseup", mouseUp, 1, pdoc);
  372. };
  373. function mouseMove(e){
  374. var h = x - y(e);
  375. tvPs.height = Math.min(pdoc.documentElement.clientHeight, Math.max(minHeight, h)) + 'px';
  376. }
  377. function mouseUp(){
  378. delEvent("mousemove", mouseMove, 1, pdoc);
  379. delEvent("mouseup", mouseUp, 1, pdoc);
  380. }
  381. el.addEventListener("mousedown", mousedown);
  382. el.addEventListener("touchstart", mousedown);
  383. el.addEventListener("touchmove", mouseMove);
  384. el.addEventListener("touchend", mouseUp);
  385. // 右击拖拽缩放
  386. function fnDown(e){
  387. // debug("mousedown", e.target);
  388. if(tvShown()) {
  389. var p=e.path,t=e.target,d=!!gcp('_xxj_tv', t,5);
  390. if(gcp('_xxj_btn',t,5)) {
  391. playBtn.ctx(e);
  392. }
  393. else if(d && e.button==2) {
  394. debug('开启移动检测')
  395. moved = 7;
  396. delEvent("mousemove", fnMove, 1, pdoc);
  397. //pdoc.addEventListener("mousemove", fnMove)
  398. setTimeout(function(){addEvent("mousemove", fnMove, 1, pdoc)}, 64);
  399. }
  400. if(d ^ focused) {
  401. focused = d;
  402. tvPs.zIndex = zIndexes[d];
  403. if(!d && userScrollTm) {
  404. userScrollTm = 0;
  405. }
  406. }
  407. }
  408. }
  409. function fnMenu(e){
  410. // debug('contextmenu', moved, e.target);
  411. if(moved==-1) {
  412. stop(e);
  413. }
  414. else if(focused && e.target.tagName!=='A') {
  415. delEvent("mousemove", fnMove, 1, pdoc);
  416. if(window.getSelection().isCollapsed) {
  417. debug('该显示特别菜单啊!');
  418. fnAbort();
  419. moved = 0;
  420. }
  421. }
  422. }
  423. addEvent("contextmenu", fnMenu, 1, pdoc);
  424. addEvent("pointerdown", fnDown, 1, pdoc);
  425. function fnAbort(){
  426. debug('fnAbort');
  427. moved=-1;
  428. delEvent("mousemove", fnMove, 1, pdoc);
  429. delEvent("mouseup", fnAbort, 1, pdoc);
  430. }
  431. function fnMove(e){
  432. //debug('fnMove', e);
  433. if(moved==7) {
  434. debug('开始右击手势移动', e);
  435. moved = 1;
  436. x = y(e) + tvP.offsetHeight;
  437. delEvent("mouseup", fnAbort, 1, pdoc); addEvent("mouseup", fnAbort, 1, pdoc);
  438. }
  439. if(moved==1) {
  440. mouseMove(e);
  441. }
  442. }
  443. tvP.addEventListener("scroll", function(e){
  444. if(autoScroling) {
  445. var tmp=Math.ceil(autoScroling), now=tvP.scrollTop;
  446. if(now>=tmp-1 && now<=tmp+1) {
  447. return;
  448. }
  449. autoScroling = 0;
  450. }
  451. //debug('scroll!', autoScroling, tvP.scrollTop);
  452. userScrollTm = Date.now();
  453. });
  454. tvP.addEventListener("click", function(e){
  455. if(e.target.className==="ft-time") {
  456. stop(e);
  457. Vid.currentTime=parseFloat(e.target.getAttribute("data-tm"));
  458. if(!Vid.playing) {
  459. Vid.play();
  460. }
  461. var n = e.target.nextElementSibling;
  462. if(n && n.classList.contains('_xxj_ft_ln')) {
  463. if(lcE) {
  464. lcE.classList.remove("curr");
  465. }
  466. lcE = n;
  467. n.classList.add("curr");
  468. }
  469. }
  470. });
  471. TextPane.ondblclickx = function(e) {
  472. debug(e, getSelection().isCollapsed);
  473. if((e.target==tv || e.target==tvP)
  474. && (e.offsetX<95 || e.offsetX>tvP.clientWidth+100)) {
  475. togglePlay();
  476. getSelection().empty();
  477. stop(e);
  478. }
  479. }
  480. TextPane.addEventListener('dblclick', TextPane.ondblclickx, 1)
  481. }
  482. function timeUpdate(e) {
  483. // lyrics scroll sync to time
  484. var tm=Vid.currentTime;
  485. if(lrcArr && (!lcN||tm>=lcN.endTime||tm<lcN.startTime)) {
  486. var n = reduce(tm,lrcArr,0,lrcArr.length);
  487. if(n && n!=lcN) {
  488. lcN = n;
  489. if(lcE) {
  490. lcE.classList.remove("curr");
  491. }
  492. n = n.ele;
  493. lcE = n;
  494. if(n) {
  495. n.classList.add("curr");
  496. }
  497. if(userScrollTm) {
  498. var scrollWait = 800;
  499. if(Date.now()-userScrollTm > scrollWait) {
  500. userScrollTm = 0;
  501. }
  502. }
  503. if(window.getSelection().isCollapsed
  504. && userScrollTm==0 && moved!=1
  505. && (n.offsetTop+n.offsetHeight+minHeight/2>tvP.scrollTop+tvP.offsetHeight
  506. ||n.offsetTop<tvP.scrollTop)) {
  507. autoScroling=n.offsetTop;
  508. if(tvP.offsetHeight > minHeight*1.7) {
  509. autoScroling -= minHeight/2;
  510. }
  511. // 自动滚动
  512. tvP.scrollTop=autoScroling;
  513. // tvP.scrollTo({ // todo 平滑滚动
  514. // top: autoScroling
  515. // ,behavior: 'smooth'
  516. // });
  517. }
  518. }
  519. }
  520. }
  521. // install timers to h5 video tag
  522. function installTimer() {
  523. if(Vid==null) {
  524. Vid=document.querySelector('video')
  525. if(Vid==null) {
  526. setTimeout(installTimer, 100)
  527. }
  528. else {
  529. Vid.addEventListener('timeupdate', timeUpdate);
  530. Vid.addEventListener('playing', e => {
  531. syncPlay(1);
  532. });
  533. Vid.addEventListener('play', e => {
  534. syncPlay(1);
  535. });
  536. Vid.addEventListener('pause', e => {
  537. syncPlay(0);
  538. });
  539. Vid.addEventListener('seeking', e => {
  540. userScrollTm = 0;
  541. timeUpdate(e);
  542. //debug('seeking...', Vid.currentTime, e)
  543. });
  544. if(Vid.playing==undefined) {
  545. Object.defineProperty(HTMLMediaElement.prototype, 'playing', {
  546. get: function(){
  547. return !!(this.currentTime > 0 && !this.paused && !this.ended && this.readyState > 2);
  548. }
  549. })
  550. }
  551. syncPlay(Vid.playing);
  552. }
  553. }
  554. }
  555. installTimer();
  556. //var insertionLis = e => {
  557. // //console.log("DOMNodeInserted")
  558. // if(document.body.lastElementChild!=YFT){
  559. // document.body.removeChild(YFT);
  560. // document.body.appendChild(YFT);
  561. // }
  562. //};
  563. //document.body.addEventListener('DOMNodeInserted', insertionLis)
  564. }
  565. // ensure visibility
  566. if(H>0) {
  567. var tmp = TextPane.style;
  568. var h = parseFloat(tmp.height);
  569. if(h!=h||h<H) {
  570. tmp.height = H+"px"
  571. }
  572. if(tmp.display!=="") {
  573. tmp.display = ""
  574. }
  575. }
  576. focused = 1;
  577. return TextPane;
  578. }
  579.  
  580.  
  581. addEvent('keydown', e=>{
  582. // de('keydown', e)
  583. if(isYProxy())
  584. if(e.key=='Enter' && !editing() && !(e.altKey||e.shiftKey||e.ctrlKey)) {
  585. if(doc.fullscreenElement||parent.doc.fullscreenElement) {
  586. doc.exitFullscreen();
  587. parent.doc.exitFullscreen();
  588. stop(e)
  589. return
  590. }
  591. // doc.body.requestFullscreen();
  592. parent['player-container-inner'].requestFullscreen();
  593. stop(e)
  594. }
  595. }, 1, doc);
  596. /*via mdict-js*/
  597. function reduce(val,arr,st,ed) {
  598. var len = ed-st;
  599. if (len > 1) {
  600. len = len >> 1;
  601. return val > arr[st + len - 1].endTime
  602. ? reduce(val,arr,st+len,ed)
  603. : reduce(val,arr,st,st+len);
  604. } else {
  605. return arr[st];
  606. }
  607. }
  608. // http://qtdebug.com/fe-srt/
  609. function parseSrt(srt) {
  610. var parsed = [];
  611. var textSubtitles = srt.split('\n\n'); // 每条字幕的信息,包含了序号,时间,字幕内容
  612. for (var i = 0; i < textSubtitles.length; ++i) {
  613. var textSubtitle = textSubtitles[i].split('\n');
  614. if (textSubtitle.length >= 2) {
  615. var sn = textSubtitle[0];
  616. var tms = textSubtitle[1].split(' --> ');
  617. var startTime = toSeconds(tms[0]);
  618. var endTime = toSeconds(tms[1]);
  619. var content = textSubtitle[2];
  620. // 字幕可能有多行
  621. if (textSubtitle.length > 2) {
  622. for (var j = 3; j < textSubtitle.length; j++) {
  623. content += ' ' + textSubtitle[j];
  624. }
  625. }
  626. parsed.push({
  627. sn: sn,
  628. startTime: startTime,
  629. endTime: endTime,
  630. content: content
  631. });
  632. }
  633. }
  634. return parsed;
  635. }
  636. function toSeconds(t) {
  637. var s = 0.0;
  638. if (t) {
  639. var p = t.trim().split(':');
  640. for (var i = 0; i < p.length; i++) {
  641. s = s * 60 + parseFloat(p[i].replace(',', '.'));
  642. }
  643. }
  644. return s;
  645. }
  646. var tracks = win._xxj_tracks = []; // store all subtitle tracks
  647. var lrcArr;
  648. var lcN, lcE;
  649. function AppendFulltext(sub, d) {
  650. debug("APFT", sub, d);
  651. var lrc = sub.srt;
  652. if(d) {
  653. // var t=win.title;
  654. // if(t)t=t.innerText;
  655. var t=document.title;
  656. downloadString(lrc, "text/plain", t+"."+(sub.lang_code||"a")+".srt");
  657. return;
  658. }
  659. win.srtlrc=sub;
  660. // parse
  661. var lrcs = parseSrt(lrc);
  662. var span="";
  663. var lastTime=0;
  664. // concate
  665. for(var i=0;i<lrcs.length;i++){
  666. var lI=lrcs[i];
  667. var text = lI.content;
  668. var lnSep="<br><br>";
  669. var sepLn="";
  670. if(lI.startTime-lastTime>3){
  671. var idx = text.indexOf(".");
  672. // skip numberic dots
  673. while(idx>0) {
  674. if(idx+1>=text.length||text[idx+1]<=' ') {
  675. break;
  676. }
  677. idx = text.indexOf(".", idx+1);
  678. }
  679. if(idx<0) idx = text.indexOf("。");
  680. if(idx<0) idx = text.indexOf(",");
  681. if(idx<0) idx = text.indexOf(",");
  682. if(idx>=0) {
  683. text=" "+text.substring(0, idx+1)
  684. +lnSep+text.substring(idx+1);
  685. } else {
  686. sepLn = lnSep;
  687. }
  688. lnSep = " ";
  689. } else {
  690. // merge to previous line
  691. text="&nbsp;"+text;
  692. lnSep = "";
  693. }
  694. //console.log(lI.startTime-lastTime);
  695. var s = lI.startTime;
  696. var m = parseInt(lI.startTime/60);
  697. span+=sepLn+"<a class='ft-time' href='' data-val='" + " "
  698. +(m+":"+parseInt(s-m*60))+lnSep+"' data-tm='"+s+"'></a>"
  699. +"<span class='_xxj_ft_ln'>"+text+"</span>"
  700. lastTime = lI.startTime;
  701. }
  702. tv.innerHTML=html(span);
  703. // attach ele to array
  704. lrcArr = lrcs;
  705. lcN = 0;
  706. var cc=0;
  707. var sz = tv.childElementCount;
  708. for(var i=0;i<sz,cc<lrcArr.length;i++) {
  709. if(tv.children[i].className==="_xxj_ft_ln") {
  710. lrcArr[cc++].ele=tv.children[i];
  711. }
  712. }
  713. window.lrcArr=lrcArr;
  714. //console.log(lrcArr);
  715. }
  716. installBtn();
  717. win.APFT = AppendFulltext;
  718. // trigger when loading new page
  719. // (actually this would also trigger when first loading, that's not what we want, that's why we need to use firsr_load === false)
  720. // (new Material design version would trigger this "yt-navigate-finish" event. old version would not.)
  721. var body = document.getElementsByTagName("body")[0];
  722. body.addEventListener("yt-navigate-finish", function (event) {
  723. if (is_video_page()&&autoFTM) {
  724. if(build_cc_menu()) {
  725. var st = MenuSty;
  726. if(st.display!="") {
  727. st.display=""
  728. }
  729. }
  730. }
  731. });
  732. // trigger when loading new page
  733. // (old version would trigger "spfdone" event. new Material design version not sure yet.)
  734. window.addEventListener("spfdone", function (e) {
  735. //if (is_video_page()) {
  736. // remove_dwnld_btn();
  737. // var checkExist = setInterval(function () {
  738. // if (unsafeWindow.watch7_headline) {
  739. // init();
  740. // clearInterval(checkExist);
  741. // }
  742. // }, 330);
  743. //}
  744. });
  745. function is_video_page() {
  746. return get_vid() !== null;
  747. }
  748. function get_vid() {
  749. if(isBY==1) {
  750. Data = (gt('ytd-app')||gt('ytd-app', parent.document)).data.playerResponse;
  751. return Data.videoDetails.videoId;
  752. }
  753. return lastVid;
  754. }
  755. //https://stackoverflow.com/questions/11582512/how-to-get-url-parameters-with-javascript/11582513#11582513
  756. function getURLParameter(name) {
  757. return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
  758. }
  759. // https://stackoverflow.com/questions/32142656/get-youtube-captions#58435817
  760. function buildXmlurl(videoId, loc) {
  761. return `${baseUrl}?lang=${loc}&v=${videoId}`;//&fmt=json3
  762. }
  763. // pull the selected caption.
  764. function pullLyrics(e, d) {
  765. //var url;
  766. // if(e==0) {
  767. // console.log("auto");
  768. // url = get_auto_xml_url();
  769. // console.log("auto", url);
  770. // }
  771. // e = tracks[e]
  772. // if(e) {
  773. // if(!e.srt)
  774. // fetch(url||buildXmlurl(get_vid(), e.lang_code))
  775. // .then(v => v.text())
  776. // .then(v => (new window.DOMParser()).parseFromString(v, "text/xml"))
  777. // .then(v => {
  778. // v = buildSrtFromXML(v);
  779. // e.srt = v;
  780. // appendFulltext(e, d)
  781. // })
  782. // else appendFulltext(e, d)
  783. // }
  784. var track = tracks[e];
  785. var subtitle_url = track.subtitle_url||track.baseUrl;
  786. debug('fetching caption track url=', subtitle_url, track);
  787. if(!subtitle_url) {
  788. throw '字幕地址为空';
  789. }
  790. if(!subtitle_url.includes('://')) {
  791. // bilibili new api does not have https prefix
  792. subtitle_url = "https:" + subtitle_url;
  793. }
  794. var url = subtitle_url;
  795. if(bank[url]) {
  796. track.srt = bank[url];
  797. AppendFulltext(track, d)
  798. } else {
  799. fetch(url)
  800. .then(v => v.text())
  801. .then(v => {
  802. debug('fetched caption track=', v);
  803. var srt;
  804. if(isBY==0) {
  805. srt = buildSrtFromJson(v);
  806. } else {
  807. const trusted = html(v);
  808. srt = new window.DOMParser().parseFromString(trusted, 'text/html');
  809. // win._src = srt;
  810. srt = buildSrtFromXML(srt);
  811. debug('parsed=', srt);
  812. // srt = buildSrtFromXML((new window.DOMParser()).parseFromString(v, "text/xml"));
  813. }
  814. bank[url] = track.srt = srt;
  815. AppendFulltext(track, d)
  816. })
  817. }
  818. }
  819. function buildMenu(e, cid){
  820. return (`<div class="ytp-menuitem" aria-haspopup="true" role="menuitem" tabindex="${e.cid||cid}">
  821. <div class="ytp-menuitem-icon"></div>
  822. <div class="ytp-menuitem-label">
  823. ${e.lan_doc||e.name.simpleText}
  824. </div>
  825. <div class="ytp-menuitem-content">
  826. 下载
  827. </div>
  828. </div>`);
  829. }
  830. function menuClick(e){
  831. debug('menuClick', e);
  832. var t = e.target;
  833. var i = parseInt(t.parentNode.getAttribute("tabindex"));
  834. if(i==i) {
  835. if(t.className==="ytp-menuitem-content") {
  836. // 下载
  837. pullLyrics(i, 1);
  838. } else {
  839. // 查看
  840. installTextPane(120);
  841. pullLyrics(i);
  842. }
  843. }
  844. MenuSty.display="none";
  845. setTimeout(()=>{
  846. MenuSty.display="none";
  847. ;debug('消失了吗', MenuSty, MenuSty.display);
  848. }, 1);
  849. t.blur();
  850. }
  851. function build_cc_menu(src) {
  852. var vid = get_vid();
  853. if(vid==Btn.parsedVid && Menu && Menu.children.length) {
  854. return false;
  855. }
  856. Btn.parsedVid=vid;
  857. if(loadOnStart) {
  858. src=1;
  859. }
  860. function onMenuLoad(tmp) {
  861. Menu.innerHTML=html(tmp);
  862. if(Menu && Menu.children) {
  863. for (var i=0,ch=Menu.children,len=ch.length; i < len; i++) {
  864. ch[i].onclick = menuClick;
  865. // if(autosel==i) {
  866. // initYFT(120);
  867. // pullLyrics(i);
  868. // }
  869. }
  870. }
  871. }
  872. var ibf = Btn; // unsafeWindow.movie_player
  873. // todo check auto caption exists
  874. if((!Menu||!Menu.parentNode) && ibf) {
  875. var tmp = document.createElement("div");
  876. ibf.appendChild(tmp);
  877. // menuData
  878. tmp.innerHTML = html(`<div class="ytp-popup ytp-fulltext-menu" data-layer="6" id="yft-select"
  879. style="width: 251px; height: 137px; display: block;">
  880. <div class="ytp-panel _xxj_menu" style="min-width: 250px; width: 251px; height: 137px;">
  881. <div class="ytp-panel-menu" role="menu" style="height: 137px;"></div>
  882. </div>
  883. </div>`);
  884.  
  885.  
  886. MenuSty = tmp.firstElementChild.style;
  887. MenuSty.position='absolute';
  888. MenuSty.background='#000000cf';
  889. if(isBY==0) {
  890. MenuSty.left='-100px';
  891. }
  892. Menu = gc('_xxj_menu', tmp);
  893. // if(src==1 && !autoFTM) {
  894. // MenuSty.display = "none";
  895. // }
  896. debug('Menu', Menu);
  897. }
  898. if(Menu) {
  899. try{
  900. // bilibili 需要根据视频aid&cid获取字幕列表
  901. if(isBY==0) {
  902. Menu.innerHTML = "";
  903. var url = `https://api.bilibili.com/x/player/v2?${vid}`;
  904. debug("loading_list, url=", url);
  905. function onload(res, xhr) {
  906. debug('得到', res, xhr)
  907. try{
  908. bank[vid] = res;
  909. var autosel=-1
  910. , arr=res.data.subtitle.subtitles
  911. , tmp=""
  912. ;
  913. tracks.length = 0;
  914. for (var i=0, len=arr.length;i<len;i++) {
  915. tracks.push(arr[i]);
  916. tmp+=buildMenu(arr[i], i);
  917. }
  918. if(src==1) {
  919. autosel=0;
  920. }
  921. debug('tmp::', arr.length);
  922. onMenuLoad(tmp)
  923. } catch(e) {
  924. console.log(e);
  925. }
  926. // todo ... load from file
  927. }
  928. if(bank[vid]) {
  929. onload(bank[vid]);
  930. } else {
  931. loadJson(url, onload);
  932. }
  933. }
  934. // youtube 字幕列表直接给我们了,无需解析api
  935. else {
  936. var autosel=-1
  937. , arr=Data.captions.playerCaptionsTracklistRenderer.captionTracks
  938. , tmp="", xml
  939. ;
  940. tracks.length = 0;
  941. for (var i=0, len=arr.length;i<len;i++) {
  942. tracks.push(arr[i]);
  943. tmp += buildMenu(arr[i], i)
  944. }
  945. onMenuLoad(tmp)
  946. }
  947. } catch(e) {
  948. debug('获取字幕列表失败!', e)
  949. Btn.parsedVid="";
  950. }
  951. } else {
  952. Btn.parsedVid="";
  953. }
  954. debug('tracks', arr);
  955. debug("autosel", autosel);
  956. return true;
  957. }
  958. // 处理时间. 比如 start="671.33" start="37.64" start="12" start="23.029"
  959. // 处理成 srt 时间, 比如 00:00:00,090 00:00:08,460 00:10:29,350
  960. function process_time(s) {
  961. s = s.toFixed(3);
  962. // 671.33 -> 671.330
  963. // 671 -> 671.000
  964. var array = s.split('.');
  965. // 把开始时间根据句号分割
  966. // 671.330 会分割成数组: [671, 330]
  967. var Hour = 0;
  968. var Minute = 0;
  969. var Second = array[0]; // 671
  970. var MilliSecond = array[1]; // 330
  971. // 先声明下变量, 待会把这几个拼好就行了
  972. // 我们来处理秒数. 把"分钟"和"小时"除出来
  973. if (Second >= 60) {
  974. Minute = Math.floor(Second / 60);
  975. Second = Second - Minute * 60;
  976. // 把 秒 拆成 分钟和秒, 比如121秒, 拆成2分钟1秒
  977. Hour = Math.floor(Minute / 60);
  978. Minute = Minute - Hour * 60;
  979. // 把 分钟 拆成 小时和分钟, 比如700分钟, 拆成11小时40分钟
  980. }
  981. // 分钟,如果位数不够两位就变成两位,下面两个if语句的作用也是一样。
  982. if (Minute < 10) {
  983. Minute = '0' + Minute;
  984. }
  985. // 小时
  986. if (Hour < 10) {
  987. Hour = '0' + Hour;
  988. }
  989. // 秒
  990. if (Second < 10) {
  991. Second = '0' + Second;
  992. }
  993. return Hour + ':' + Minute + ':' + Second + ',' + MilliSecond;
  994. }
  995. // copy from: https://gist.github.com/danallison/3ec9d5314788b337b682
  996. // Thanks! https://github.com/danallison
  997. // work in Chrome 66
  998. // test passed: 2018-5-19
  999. function downloadString(text, fileType, fileName) {
  1000. var blob = new Blob([text], {type: fileType});
  1001. var a = document.createElement('a');
  1002. a.download = fileName;
  1003. a.href = URL.createObjectURL(blob);
  1004. a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
  1005. a.style.display = "none";
  1006. document.body.appendChild(a);
  1007. a.click();
  1008. document.body.removeChild(a);
  1009. setTimeout(function () {
  1010. URL.revokeObjectURL(a.href);
  1011. }, 1500);
  1012. }
  1013. // https://css-tricks.com/snippets/javascript/unescape-html-in-js/
  1014. // turn HTML entity back to text, example: &quot; should be "
  1015. function htmlDecode(input) {
  1016. var e = document.createElement('div');
  1017. const trusted = html(input);
  1018. e.innerHTML = trusted;
  1019. return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
  1020. // var e = document.createElement('div');
  1021. // e.innerHTML = input;
  1022. // return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
  1023. }
  1024. // return URL or null;
  1025. // later we can send a AJAX and get XML subtitle
  1026. function get_auto_xml_url() {
  1027. try {
  1028. var captionTracks = get_captionTracks()
  1029. for (var index in captionTracks) {
  1030. var caption = captionTracks[index];
  1031. if (caption.kind === 'asr') {
  1032. return captionTracks[index].baseUrl;
  1033. }
  1034. // ASR – A caption track generated using automatic speech recognition.
  1035. // https://developers.google.com/youtube/v3/docs/captions
  1036. }
  1037. return false;
  1038. } catch (e) {
  1039. console.log(e);
  1040. return false;
  1041. }
  1042. }
  1043. async function get_auto_subtitle() {
  1044. var url = get_auto_xml_url();
  1045. console.log("dwnld_auto_url::", url);
  1046. if (url == false) {
  1047. return false;
  1048. }
  1049. var result = await getUrl(url)
  1050. return result
  1051. }
  1052. // Youtube return XML.
  1053. // input: Youtube XML format
  1054. // output: SRT format
  1055. function buildSrtFromXML(youtube_xml_string) {
  1056. if (youtube_xml_string === '') {
  1057. return false;
  1058. }
  1059. var text = youtube_xml_string.getElementsByTagName('text');
  1060. var result = '\uFEFF';
  1061. var len = text.length;
  1062. for (var i = 0; i < len; i++) {
  1063. var content = text[i].textContent.toString();
  1064. content = content.replace(/(<([^>]+)>)/ig, ""); // remove all html tag.
  1065. var start = text[i].getAttribute('start');
  1066. var end = parseFloat(text[i].getAttribute('start')) + parseFloat(text[i].getAttribute('dur'));
  1067. result = result + (i + 1) + "\n";
  1068. // 1
  1069. if (i + 1 >= len) {
  1070. end = parseFloat(text[i].getAttribute('start')) + parseFloat(text[i].getAttribute('dur'));
  1071. } else {
  1072. end = text[i + 1].getAttribute('start');
  1073. }
  1074. var start_time = process_time(parseFloat(start));
  1075. var end_time = process_time(parseFloat(end));
  1076. result = result + start_time;
  1077. result = result + ' --> ';
  1078. result = result + end_time + "\n";
  1079. // 00:00:01,939 --> 00:00:04,350
  1080. content = htmlDecode(content);
  1081. // turn HTML entity back to text. example: &#39; back to apostrophe (')
  1082. result = result + content + "\n" + "\n";
  1083. }
  1084. return result;
  1085. }
  1086. // bilibili return JSON.
  1087. function buildSrtFromJson(bilibili_json_string) {
  1088. var json = JSON.parse(bilibili_json_string);
  1089. debug('buildSrtFromJson, json=', json);
  1090. var arr = json.body, result = '\uFEFF';
  1091. for (var i = 0, len=arr.length; i < len; i++) {
  1092. var content = arr[i].content;
  1093. content = content.replace(/(<([^>]+)>)/ig, ""); // remove all html tag.
  1094. var start = arr[i].from;
  1095. var end = arr[i].to;
  1096. // 1
  1097. result = result + (i + 1) + "\n";
  1098. var start_time = process_time(parseFloat(start));
  1099. var end_time = process_time(parseFloat(end));
  1100. result = result + start_time;
  1101. result = result + ' --> ';
  1102. result = result + end_time + "\n";
  1103. // 00:00:01,939 --> 00:00:04,350
  1104. // content = htmlDecode(content);
  1105. // turn HTML entity back to text. example: &#39; back to apostrophe (')
  1106. result = result + content + "\n" + "\n";
  1107. }
  1108. return result;
  1109. }
  1110. function get_captionTracks() {
  1111. var json = null
  1112. if (win.youtube_playerResponse_1c7) {
  1113. json = youtube_playerResponse_1c7;
  1114. } else if(ytplayer.config.args.player_response) {
  1115. let raw_string = ytplayer.config.args.player_response;
  1116. json = JSON.parse(raw_string);
  1117. } else if (ytplayer.config.args.raw_player_response) {
  1118. json = ytplayer.config.args.raw_player_response;
  1119. }
  1120. let captionTracks = json.captions.playerCaptionsTracklistRenderer.captionTracks;
  1121. return captionTracks
  1122. }
  1123. function loadJson(url,cb,parm){
  1124. //debug('loadJson!!!', url,parm)
  1125. var req = new XMLHttpRequest();
  1126. req.open(parm?'POST':'GET', url, true);
  1127. req.responseType = 'json';
  1128. // bilibili API need SESSDATA key from browser's cookies, carry cookies of session for it
  1129. req.withCredentials = true;
  1130. if(cb){
  1131. req.onload = function() {
  1132. cb(req.response, req);
  1133. };
  1134. req.onerror = function() {
  1135. cb(0, req);
  1136. };
  1137. }
  1138. //req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
  1139. //x.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  1140. req.send(parm);
  1141. }
  1142. // https://stackoverflow.com/questions/48969495/in-javascript-how-do-i-should-i-use-async-await-with-xmlhttprequest
  1143. function makeRequest(method, url, load, type) {
  1144. return new Promise(function (resolve, reject) {
  1145. let xhr = new XMLHttpRequest();
  1146. xhr.responseType = type;
  1147. //xhr.timeout = 2000;
  1148. xhr.onload = function () {
  1149. debug('makeRequest, onload::', this.status, xhr.statusText);
  1150. if (this.status >= 200 && this.status < 300) {
  1151. if(load) {
  1152. load(xhr);
  1153. resolve('');
  1154. } else {
  1155. resolve(xhr);
  1156. }
  1157. } else {
  1158. debug('makeRequest, 发生错误::', this.status, xhr.statusText);
  1159. reject({
  1160. status: this.status,
  1161. statusText: xhr.statusText
  1162. });
  1163. }
  1164. };
  1165. xhr.onerror = function () {
  1166. debug('makeRequest, 发生错误::', this.status, xhr.statusText);
  1167. reject({
  1168. status: this.status,
  1169. statusText: xhr.statusText
  1170. });
  1171. };
  1172. xhr.open(method, url, true); // set async to true to avoid 'sync responseType error'
  1173. xhr.send();
  1174. });
  1175. }
  1176. async function getUrl(url) {
  1177. return makeRequest("GET", url);
  1178. }
  1179. // de
  1180. // var hack =1;
  1181. // if(parent!=win) {
  1182. // win.originalRemove_ ||= Element.prototype.removeChild;
  1183. // // Override the remove method
  1184. // Element.prototype.removeChild = function(e) {
  1185. // de('removing...', e.className, e)
  1186. // win.originalRemove_.call(this, e);
  1187. // };
  1188. // }
  1189. // })();

QingJ © 2025

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