Greasy Fork is available in English.

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

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

// ==UserScript==
// @name         B站学习机 | Bilibili+Youtube字幕全文阅读 | coursera-like subtitles fulltext reader
// @namespace    https://gist.github.com/KnIfER/9e43ffa31c3b9831a500edf35595c1dc
// @version      4
// @description  在线字幕阅读或下载,B站油管秒变cousera! - Read & learn subtitles full text online!
// @author       KnIfER
// @match        https://*.bilibili.com/video/*
// @match        https://*.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @license MIT
// ==/UserScript==

(function() {
	'use strict';
	var lastVid='x';
	var win = window.unsafeWindow || window, doc=document
		, bank=win._xxj_bank
		, isBY = location.host.indexOf('bilibili')>=0?0:1
		, Data;
	if(!bank) {
		bank = win._xxj_bank = {};
	} else {
		lastVid = bank.unreg();
	}
	bank.unreg = uninstall;

	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])}
    
	var proto = XMLHttpRequest.prototype;
	if(isBY==0) {
		proto.realOpen = proto.open;
		proto.open = function(method, url, a, u, p) {
			//debug('request::open!!!', url);
			this.realOpen(method, url , a, u, p);
			if(url) {
				var tmp = new RegExp('(aid=[0-9]+&cid=[0-9]+)').exec(url);
				if(tmp) tmp = tmp[0];
				if(tmp && lastVid!=tmp) {
					lastVid = tmp;
					debug('正在播放='+lastVid);
				}
			}
		};
		proto.realSend = proto.send;
		proto.send = function(b) {
			//debug('request::send!!!', b);
			this.realSend(b);
		};
	}
	
	// 动态z-order,配合B站笔记窗口
	var zIndexes = ['1500', '10000'];
	if(isBY==1) {
		zIndexes = ['2030', '10000'];
	}

    var loadOnStart = false; /* true false 是否自动分析字幕 */
    var autoFTM = false; /* true false 是否自动打开字幕列表 */
 
	// the panel, textview, and the button
    var TextPane, tv, Btn, installTryCnt=0
		, autoScroling, userScrollTm=0
		, moved, focused=0
		// the menu
		, Menu, MenuSty
		// video tag
		, Vid
		;
 
	function ge(e,p){return (p||doc).getElementById(e)};
	function gc(e,p){return (p||doc).getElementsByClassName(e)[0]};
	function craft(p, t, c) {
		var e=doc.createElement(t||'DIV');
		if(c)e.className=c;
		if(p)p.appendChild(e);
		return e;
	}
 
    function installBtn(){
        if(!Btn){
			var ibf = 0, tmp;
			if(isBY==0) {
				ibf = doc.getElementsByClassName("bpx-player-ctrl-subtitle")[0];
				if(!ibf) ibf = doc.getElementsByClassName("bpx-player-ctrl-volume")[0];
				if(ibf) ibf = ibf.nextElementSibling;
			} else {
				ibf = doc.getElementsByClassName("ytp-settings-button")[0];
				if(!ibf) {
					tmp = doc.getElementsByClassName("slim_video_action_bar_renderer_button");
					ibf = tmp[tmp.length-1];
				}
			}
			debug('insertBefore', ibf, installTryCnt);
            if(ibf) {
				// insert a control BUTTON
				tmp = craft(doc.head, "STYLE");
				tmp.id = "_xxj_sty"
				tmp.innerHTML = ".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}";
                if(isBY==0) {
					tmp.innerHTML+=".ytp-menuitem>div{display:inline-block;font-size:medium}.ytp-menuitem-label{cursor:pointer}";
				}
				if(isBY==1) {
					tmp.innerHTML+="._xxj_menu .ytp-menuitem-label{width:65%;white-space:nowrap;font-size:100%;}._xxj_menu .ytp-menuitem-content{white-space:nowrap;font-size:100%;}";
				}
				tmp = craft(0, isBY==1?'BUTTON':'DIV', "ytp-fulltext-button ytp-button bpx-player-ctrl-btn");
                tmp.id = "_xxj_btn"
                tmp.title="字幕学习机 (x)";
				// button svg icon
                tmp.innerHTML = '<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>';
				tmp.onclick = function() {
					if(MenuSty) {
						tmp = MenuSty;
						if(tmp.display!="none") {
							tmp.display="none"
						} else {
							tmp.display="";
							build_cc_menu()
						}
					} else {
						build_cc_menu()
					}
                }
				var ts = tmp.style;
				if(isBY==0) {
					ts.maxHeight='30px'
					tmp.firstElementChild.style = "transform:scale(1.5);"
				}
				if(isBY==1) {
					if(location.host[0]=='m') {
						ts.marginTop='.5%';
						ts.minWidth='25px';
						gc('ytp-svg-fill', tmp).style.fill='#000';
					}
				}
                ibf.parentNode.insertBefore(tmp, ibf);
				Btn=tmp;
				debug('成功安装按钮:', tmp);
				// if(autoFTM) {
				// 	build_cc_menu()
				// }
				// if(loadOnStart) {
				// 	// todo load initial lyrics
				// 	build_cc_menu(1);
				// 	initYFT();
				// }
            } else if(installTryCnt++<15){
                setTimeout(installBtn, 500);
            }
        }
    }
	
	
    function tvShown(){
		return TextPane && TextPane.style.display!='none';
	}
	
	var keysDwn=[];
	function fnKeydown(e){
		//debug('fnKeydown', tvShown(), e.code, e.code==="KeyX", e.altKey)
		if(!keysDwn[e.code]) {
			keysDwn[e.code] = e;
			if(focused || tvShown()) {
				if(focused) {
					if(e.code==="Escape") {
						TextPane.close();
						e.preventDefault();
					}
				}
				// if(userScrollTm && e.code==="ArrowRight" && e.code==="ArrowLeft") {
				// 	userScrollTm = 0;
				// 	debug('userScrollTm = 0');
				// }
				if(e.code==="KeyX"/*  && e.altKey */) {
					TextPane.close();
				}
			}
			else if(e.code==="KeyX"/*  && e.altKey */) {
				installTextPane().style.display = "";
			}
		}
	}
	function fnKeyup(e){
		delete keysDwn[e.code];
	}
	doc.addEventListener("keydown", fnKeydown);
	doc.addEventListener("keyup", fnKeyup);
	
    function installTextPane(H){
        if(!TextPane) {
            craft(doc.head, "STYLE").innerHTML = "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: 2px 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%);}"
				+ ".bpx-player-container[data-screen=full], .bpx-player-container[data-screen=web] {z-index: 1500!important;}"
				+ "#bilibili-player.mode-webscreen {z-index: 1500!important;}"
				;
			// the main dialog.
            TextPane=craft(doc.body,0,"_xxj_tv");
			TextPane.innerHTML='<p class="drag_resizer"></p><div class="_xxj_tvp"><p class="_xxj_ftv">FETCHING……</p></div>';
			tv = gc('_xxj_ftv', TextPane);
			tv.style = 'margin-left:5px;font-size:x-large;padding:9px 100px 0 100px;';
			var tvP = gc('_xxj_tvp', TextPane)
				, tvPs = TextPane.style
				, x = 0
				, minHeight = 1.35 * (parseInt(getComputedStyle(tv).lineHeight)||tv.offsetHeight);
				;
			tvPs.zIndex = zIndexes[1];
			tvP.style = 'overflow-y:scroll;height:100%;';
			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';
			// the play button.
			var playBtn = craft(TextPane);
			playBtn.style = 'position:fixed;height:70px;width:80px;bottom:0;';
			playBtn.innerHTML = '<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>';
			var btnPause = gc('btnPause', playBtn), btnPlay = gc('btnPlay', playBtn);
			function syncPlay(p) {
				btnPause.style.display=p?'':'none';
				btnPlay.style.display=p?'none':'';
				playBtn.title = p?'暂停':'播放';
			}
			playBtn = playBtn.children[0];
			function togglePlay(){
				var p = Vid.playing;
				if(p) Vid.pause();
				else Vid.play();
				syncPlay(Vid.playing);
			}
			playBtn.onclick = togglePlay;
			playBtn.oncontextmenu = function(e) {
				lcN=userScrollTm=0;
				timeUpdate();
				e.preventDefault();
			}
			//
			// windows-like handy buttons.
			//
			var topBtns = craft(TextPane);
			topBtns.style='user-select:none;padding-right:0.25em;font-weight:600;text-decoration:none;position:absolute;top:0;right:20px;font-size:17px;';
			// the close button.
			var closeBtn = craft(topBtns, 'A');
			closeBtn.innerText = '[X]';
			closeBtn.style = 'color:#175199;';
			closeBtn.title = '关闭';
			closeBtn.onclick = TextPane.close = function(){
				tvPs.display = 'none';
				focused = 0;
			}
			// the maximise button.
			craft(topBtns, 'DIV').style='height:5px;';
			var maxBtn = craft(topBtns, 'A');
			maxBtn.innerText = '[▢]';
			maxBtn.style = 'color:#175199;';
			maxBtn.title = '最大化';
			maxBtn.onclick = function(){
			}
			// the opacity button.
			craft(topBtns, 'DIV').style='height:5px;';
			var zenBtn = craft(topBtns, 'A');
			zenBtn.innerText = '[⊥]';
			zenBtn.style = 'color:#175199;transform:rotate(180deg);position:absolute;';
			zenBtn.title = '透明背景';
			zenBtn.onclick = function(){
				if(tvPs.background=='rgb(255, 255, 255)')
					tvPs.background = 'rgb(255, 255, 255, 0.55)'
				else
					tvPs.background = 'rgb(255, 255, 255)'
				debug(tvPs.background);
			}
			
            // drag-resize the TextView, bindResize
            if(1) {
                var el = gc('drag_resizer', TextPane);
				el.style = 'position:absolute;top:0;right:0;height:6px;width:100%;padding:0;cursor:ns-resize';
                function y(e){
                    if(e.clientY==undefined)
                        return e.originalEvent.changedTouches[0].clientY;
					return e.clientY;
				}
                function mousedown(e){
                    x = y(e) + tvP.offsetHeight;
                    e.preventDefault();
					debug('mousedown', e);
                    document.addEventListener("mousemove", mouseMove); document.addEventListener("mouseup", mouseUp);
					
                };
                function mouseMove(e){
					var h = x - y(e);
                    tvPs.height = Math.min(document.documentElement.clientHeight, Math.max(minHeight, h)) + 'px';
                }
                function mouseUp(){
                    document.removeEventListener("mousemove", mouseMove); document.removeEventListener("mouseup", mouseUp);
                }
 
                el.addEventListener("mousedown", mousedown);
                el.addEventListener("touchstart", mousedown);
                el.addEventListener("touchmove", mouseMove);
                el.addEventListener("touchend", mouseUp);
				
				// 右击拖拽缩放
				function fnDown(e){
					debug("mousedown", e);
					if(tvShown()) {
						var p=e.path,d=0,i=0,t;
						if(!p && e.composedPath) p=e.composedPath();
						if(p) for(var i=0;(t=p[i])&&i++<5;) if(t==TextPane) {d=1;break;}
						if(d && e.button==2) {
							debug('开启移动检测')
							moved = 7;
							doc.removeEventListener("mousemove", fnMove); 
							//doc.addEventListener("mousemove", fnMove)
							setTimeout(function(){doc.addEventListener("mousemove", fnMove)}, 64);
						}
						if(d ^ focused) {
							focused = d;
							tvPs.zIndex = zIndexes[d];
							if(!d && userScrollTm) {
								userScrollTm = 0;
							}
						}
					}
				}
				function fnMenu(e){
					debug('contextmenu', moved, e.target);
					if(moved==-1) {
						e.preventDefault();
					}
					else if(focused && e.target.tagName!=='A') {
						doc.removeEventListener("mousemove", fnMove);
						if(window.getSelection().isCollapsed) {
							debug('该显示特别菜单啊!');
							fnAbort();
							moved = 0;
						}
					}
				}
				doc.addEventListener("contextmenu", fnMenu);
				doc.addEventListener("mousedown", fnDown);
				unregs.push(function(){ 
					doc.removeEventListener("mousedown", fnDown); 
					doc.removeEventListener("contextmenu", fnMenu);
					doc.removeEventListener("keydown", fnKeydown);
					doc.removeEventListener("keyup", fnKeyup);
				});
				function fnAbort(){
					debug('fnAbort');
					moved=-1; 
					doc.removeEventListener("mousemove", fnMove);
					doc.removeEventListener("mouseup", fnAbort);
				}
				function fnMove(e){ 
					//debug('fnMove', e);
					if(moved==7) {
						debug('开始右击手势移动', e);
						moved = 1;
						x = y(e) + tvP.offsetHeight;
						doc.removeEventListener("mouseup", fnAbort); doc.addEventListener("mouseup", fnAbort);
					}
					if(moved==1) {
						mouseMove(e);
					}
				}
                tvP.addEventListener("scroll", function(e){
					if(autoScroling) {
						var tmp=Math.ceil(autoScroling), now=tvP.scrollTop;
						if(now>=tmp-1 && now<=tmp+1) {
							return;
						}
						autoScroling = 0;
					}
					//debug('scroll!', autoScroling, tvP.scrollTop);
					userScrollTm = Date.now();
				});
				tvP.addEventListener("click", function(e){
					if(e.target.className==="ft-time") {
						e.preventDefault();
						Vid.currentTime=parseFloat(e.target.getAttribute("data-tm"));
						if(!Vid.playing) {
							Vid.play();
						}
						var n = e.target.nextElementSibling;
						if(n && n.classList.contains('_xxj_ft_ln')) {
							if(lcE) {
								lcE.classList.remove("curr");
							}
							lcE = n;
							n.classList.add("curr");
						}
					}
				});
				TextPane.ondblclickx = function(e) {
					debug(e, getSelection().isCollapsed);
					if((e.target==tv || e.target==tvP)
						&& (e.offsetX<95 || e.offsetX>tvP.clientWidth+100)) {
						togglePlay();
						getSelection().empty();
						e.preventDefault();
						e.stopPropagation();
					}
				}
				TextPane.addEventListener('dblclick', TextPane.ondblclickx, 1)
            }
			
			function timeUpdate(e) {
				// lyrics scroll sync to time
				var tm=Vid.currentTime;
				if(lrcArr && (!lcN||tm>=lcN.endTime||tm<lcN.startTime)) {
					var n = reduce(tm,lrcArr,0,lrcArr.length);
					if(n && n!=lcN) {
						lcN = n;
						if(lcE) {
							lcE.classList.remove("curr");
						}
						n = n.ele;
						lcE = n;
						if(n) {
							n.classList.add("curr");
						}
						if(userScrollTm) {
							var scrollWait = 800;
							if(Date.now()-userScrollTm > scrollWait) {
								userScrollTm = 0;
							}
						}
						if(window.getSelection().isCollapsed
							&& userScrollTm==0 && moved!=1
							&& (n.offsetTop+n.offsetHeight+minHeight/2>tvP.scrollTop+tvP.offsetHeight
									||n.offsetTop<tvP.scrollTop)) {
							autoScroling=n.offsetTop;
							if(tvP.offsetHeight > minHeight*1.7) {
								autoScroling -= minHeight/2;
							}
							// 自动滚动
							tvP.scrollTop=autoScroling;
							// tvP.scrollTo({ // todo 平滑滚动
							// 	top: autoScroling
							// 	,behavior: 'smooth'
							// });
						}
					}
				}
			}
			
			// install timers to h5 video tag
			function installTimer() {
				if(Vid==null) {
					Vid=document.querySelector('video')
					if(Vid==null) {
						setTimeout(installTimer, 100)
					} 
					else {
						Vid.addEventListener('timeupdate', timeUpdate);
						Vid.addEventListener('playing', e => {
							syncPlay(1);
						});
						Vid.addEventListener('play', e => {
							syncPlay(1);
						});
						Vid.addEventListener('pause', e => {
							syncPlay(0);
						});
						Vid.addEventListener('seeking', e => {
							userScrollTm = 0;
							timeUpdate(e);
							//debug('seeking...', Vid.currentTime, e)
						});
						if(Vid.playing==undefined) {
							Object.defineProperty(HTMLMediaElement.prototype, 'playing', {
								get: function(){
									return !!(this.currentTime > 0 && !this.paused && !this.ended && this.readyState > 2);
								}
							})
						}
						syncPlay(Vid.playing);
					}
				}
			}
			installTimer();
			//var insertionLis = e => {
			//	//console.log("DOMNodeInserted")
			//	if(document.body.lastElementChild!=YFT){
			//		document.body.removeChild(YFT);
			//		document.body.appendChild(YFT);
			//	}
			//};
			//document.body.addEventListener('DOMNodeInserted', insertionLis)
        }
		// ensure visibility
		if(H>0) {
			var tmp = TextPane.style;
			var h = parseFloat(tmp.height);
			if(h!=h||h<H) {
				tmp.height = H+"px"
			}
			if(tmp.display!=="") {
				tmp.display = ""
			}
		}
		focused = 1;
		return TextPane;
    }
 
	/*via mdict-js*/
	function reduce(val,arr,st,ed) {
		var len = ed-st;
		if (len > 1) {
		  len = len >> 1;
		  return val > arr[st + len - 1].endTime
					? reduce(val,arr,st+len,ed)
					: reduce(val,arr,st,st+len);
		} else {
		  return arr[st];
		}
	}
 
	// http://qtdebug.com/fe-srt/
	function parseSrt(srt) {
		var parsed = [];
		var textSubtitles = srt.split('\n\n'); // 每条字幕的信息,包含了序号,时间,字幕内容
		for (var i = 0; i < textSubtitles.length; ++i) {
			var textSubtitle = textSubtitles[i].split('\n');
			if (textSubtitle.length >= 2) {
				var sn = textSubtitle[0];
				var tms = textSubtitle[1].split(' --> ');
				var startTime = toSeconds(tms[0]);
				var endTime   = toSeconds(tms[1]);
				var content   = textSubtitle[2];
				// 字幕可能有多行
				if (textSubtitle.length > 2) {
					for (var j = 3; j < textSubtitle.length; j++) {
						content += ' ' + textSubtitle[j];
					}
				}
				parsed.push({
					sn: sn,
					startTime: startTime,
					endTime: endTime,
					content: content
				});
			}
		}
		return parsed;
	}
 
	function toSeconds(t) {
		var s = 0.0;
		if (t) {
			var p = t.trim().split(':');
			for (var i = 0; i < p.length; i++) {
				s = s * 60 + parseFloat(p[i].replace(',', '.'));
			}
		}
		return s;
	}
 
	var tracks = win._xxj_tracks = []; // store all subtitle tracks
	var lrcArr;
	var lcN, lcE;
 
	function AppendFulltext(sub, d) {
		debug("APFT", sub, d);
		var lrc = sub.srt;
		if(d) {
			var t=document.getElementsByTagName("H1")[0];
			if(t)t=t.innerText;
			else t=document.title;
			downloadString(lrc, "text/plain", t+"."+(sub.lang_code||"a")+".srt");
			return;
		}
		win.srtlrc=sub;
		// parse
		var lrcs = parseSrt(lrc);
		var span="";
		var lastTime=0;
		// concate
		for(var i=0;i<lrcs.length;i++){
			var lI=lrcs[i];
			var text = lI.content;
			var lnSep="<br><br>";
			var sepLn="";
			if(lI.startTime-lastTime>3){
				var idx = text.indexOf(".");
				// skip numberic dots
				while(idx>0) {
					if(idx+1>=text.length||text[idx+1]<=' ') {
						break;
					}
					idx = text.indexOf(".", idx+1);
				}
				if(idx<0) idx = text.indexOf("。");
				if(idx<0) idx = text.indexOf(",");
				if(idx<0) idx = text.indexOf(",");
				if(idx>=0) {
					text=" "+text.substring(0, idx+1)
						+lnSep+text.substring(idx+1);
				} else {
					sepLn = lnSep;
				}
				lnSep = " ";
			} else {
				// merge to previous line
				text="&nbsp;"+text;
				lnSep = "";
			}
 
			//console.log(lI.startTime-lastTime);
			var s = lI.startTime;
			var m = parseInt(lI.startTime/60);
			span+=sepLn+"<a class='ft-time' href='' data-val='" + " "
				+(m+":"+parseInt(s-m*60))+lnSep+"' data-tm='"+s+"'></a>"
				+"<span class='_xxj_ft_ln'>"+text+"</span>"
			lastTime = lI.startTime;
		}
		tv.innerHTML=span;
 
		// attach ele to array
		lrcArr = lrcs;
		lcN = 0;
		var cc=0;
		var sz = tv.childElementCount;
		for(var i=0;i<sz,cc<lrcArr.length;i++) {
			if(tv.children[i].className==="_xxj_ft_ln") {
				lrcArr[cc++].ele=tv.children[i];
			}
		}
		window.lrcArr=lrcArr;
		//console.log(lrcArr);
	}
 
    installBtn();
 
	win.APFT = AppendFulltext;
	
	// unregister the script for hot reload
	var unregs = [];
	function uninstall() {
		if(Btn) Btn.remove();
		if(TextPane) TextPane.remove();
		if(isBY==0) {
			proto.open = proto.realOpen;
			proto.send = proto.realSend;
		}
		var tmp = ge('_xxj_sty');
		if(tmp) tmp.remove();
		for(var i=0;i<unregs.length;i++) {
			unregs[i]();
		}
		return lastVid;
	}
	
	// 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)
	// (new Material design version would trigger this "yt-navigate-finish" event. old version would not.)
	var body = document.getElementsByTagName("body")[0];
	body.addEventListener("yt-navigate-finish", function (event) {
		if (is_video_page()&&autoFTM) {
			if(build_cc_menu()) {
				var st = MenuSty;
				if(st.display!="") {
					st.display=""
				}
			}
		}
	});
 
	// trigger when loading new page
	// (old version would trigger "spfdone" event. new Material design version not sure yet.)
	window.addEventListener("spfdone", function (e) {
		//if (is_video_page()) {
		//	remove_dwnld_btn();
		//	var checkExist = setInterval(function () {
		//		if (unsafeWindow.watch7_headline) {
		//			init();
		//			clearInterval(checkExist);
		//		}
		//	}, 330);
		//}
	});
 
	function is_video_page() {
		return get_vid() !== null;
	}
 
	function get_vid() {
		if(isBY==1) {
			Data = document.getElementsByTagName('ytd-app')[0].data.playerResponse;
			return Data.videoDetails.videoId;
		}
		return lastVid;
	}
 
	//https://stackoverflow.com/questions/11582512/how-to-get-url-parameters-with-javascript/11582513#11582513
	function getURLParameter(name) {
		return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
	}
 
	// https://stackoverflow.com/questions/32142656/get-youtube-captions#58435817
	function buildXmlurl(videoId, loc) {
		return `${baseUrl}?lang=${loc}&v=${videoId}`;//&fmt=json3
	}
 
	// pull the selected caption.
	function pullLyrics(e, d) {
		//var url;
		// if(e==0) {
		// 	console.log("auto");
		// 	url = get_auto_xml_url();
		// 	console.log("auto", url);
		// }
		// e = tracks[e]
		// if(e) {
		// 	if(!e.srt)
		// 	fetch(url||buildXmlurl(get_vid(), e.lang_code))
		// 	.then(v => v.text())
		// 	.then(v => (new window.DOMParser()).parseFromString(v, "text/xml"))
		// 	.then(v => {
		// 		v = buildSrtFromXML(v);
		// 		e.srt = v;
		// 		appendFulltext(e, d)
		// 	})
		// 	else appendFulltext(e, d)
		// }
		var track = tracks[e];
		var url = track.subtitle_url || track.baseUrl;
		debug('fetching caption track url=', url);
		if(bank[url]) {
			track.srt = bank[url];
			AppendFulltext(track, d)
		} else {
			fetch(url)
				.then(v => v.text())
				.then(v => {
					debug('fetched caption track=', v);
					var srt;
					if(isBY==0) {
						srt = buildSrtFromJson(v);
					} else {
						srt = buildSrtFromXML((new window.DOMParser()).parseFromString(v, "text/xml"));
					}
					bank[url] = track.srt = srt;
					AppendFulltext(track, d)
				})
		}
	}
 
	function buildMenu(e, cid){
return `<div class="ytp-menuitem" aria-haspopup="true" role="menuitem" tabindex="${e.cid||cid}">
	<div class="ytp-menuitem-icon"></div>
	<div class="ytp-menuitem-label">
		${e.lan_doc||e.name.simpleText}
	</div>
	<div class="ytp-menuitem-content">
		下载
	</div>
</div>`;
	}
 
	function menuClick(e){
		debug('menuClick', e);
		var t = e.target;
		var i = parseInt(t.parentNode.getAttribute("tabindex"));
		if(i==i) {
			if(t.className==="ytp-menuitem-content") {
				// 下载
				pullLyrics(i, 1);
			} else {
				// 查看
				installTextPane(120);
				pullLyrics(i);
			}
		}
		MenuSty.display="none";
		setTimeout(()=>{
			MenuSty.display="none";
			;debug('消失了吗', MenuSty, MenuSty.display);
		}, 1);
		t.blur();
	}
 
	function build_cc_menu(src) {
		var vid = get_vid();
		if(vid==Btn.parsedVid) {
			return false;
		}
		Btn.parsedVid=vid;
		if(loadOnStart) {
			src=1;
		}
		var ibf = Btn; // unsafeWindow.movie_player
		// todo validify auto caption exists
		if(!Menu && ibf) {
			var tmp = document.createElement("div");
			ibf.appendChild(tmp);
			// menuData
			tmp.innerHTML = `<div class="ytp-popup ytp-fulltext-menu" data-layer="6" id="yft-select"
			style="width: 251px; height: 137px; display: block;">
				<div class="ytp-panel _xxj_menu" style="min-width: 250px; width: 251px; height: 137px;">
					<div class="ytp-panel-menu" role="menu" style="height: 137px;"></div>
				</div>
			</div>`;
			MenuSty = tmp.firstElementChild.style;
			MenuSty.position='absolute';
			MenuSty.background='#000000cf';
			if(isBY==0) {
				MenuSty.left='-100px';
			}
			Menu = gc('_xxj_menu', tmp);
			// if(src==1 && !autoFTM) {
			// 	MenuSty.display = "none";
			// }
			debug('Menu', Menu);
		}
		if(Menu) {
			try{
				// bilibili 需要根据视频aid&cid获取字幕列表
				if(isBY==0) {
					Menu.innerHTML = "";
					var url = `https://api.bilibili.com/x/player/v2?${vid}`;
					debug("loading_list, url=", url);
					function onload(res, xhr) {
						debug('得到', res, xhr)
						try{
							bank[vid] = res;
							var autosel=-1
								, arr=res.data.subtitle.subtitles
								, tmp=""
								;
							tracks.length = 0;
							for (var i=0, len=arr.length;i<len;i++) {
								tracks.push(arr[i]);
								tmp+=buildMenu(arr[i], i);
							}
							if(src==1) {
								autosel=0;
							}
							debug('tmp', tmp);
							Menu.innerHTML=tmp;
						} catch(e) {
							debug(e);
						}
						// todo ... load from file
					}
					if(bank[vid]) {
						onload(bank[vid]);
					} else {
						loadJson(url, onload);
					}
				} 
				// youtube 字幕列表直接给我们了,无需解析api
				else {
					var autosel=-1
						, arr=Data.captions.playerCaptionsTracklistRenderer.captionTracks
						, tmp="", xml
					;
					tracks.length = 0;
					for (var i=0, len=arr.length;i<len;i++) {
						tracks.push(arr[i]);
						tmp+=buildMenu(arr[i], i);
					}
					Menu.innerHTML=tmp;
				}
			} catch(e) {
				debug('获取字幕列表失败!', e)
				Btn.parsedVid="";
			}
		} else {
			Btn.parsedVid="";
		}
		debug('tracks', arr);
		debug("autosel", autosel);
		if(Menu && Menu.children) {
			for (var i=0,ch=Menu.children,len=ch.length; i < len; i++) {
				ch[i].onclick = menuClick;
				// if(autosel==i) {
				// 	initYFT(120);
				// 	pullLyrics(i);
				// }
			}
		}
		return true;
	}
 
	// 处理时间. 比如 start="671.33"  start="37.64"  start="12" start="23.029"
	// 处理成 srt 时间, 比如 00:00:00,090    00:00:08,460    00:10:29,350
	function process_time(s) {
		s = s.toFixed(3);
		// 671.33 -> 671.330
		// 671 -> 671.000
 
		var array = s.split('.');
		// 把开始时间根据句号分割
		// 671.330 会分割成数组: [671, 330]
 
		var Hour = 0;
		var Minute = 0;
		var Second = array[0]; // 671
		var MilliSecond = array[1]; // 330
		// 先声明下变量, 待会把这几个拼好就行了
 
		// 我们来处理秒数.  把"分钟"和"小时"除出来
		if (Second >= 60) {
			Minute = Math.floor(Second / 60);
			Second = Second - Minute * 60;
			// 把 秒 拆成 分钟和秒, 比如121秒, 拆成2分钟1秒
 
			Hour = Math.floor(Minute / 60);
			Minute = Minute - Hour * 60;
			// 把 分钟 拆成 小时和分钟, 比如700分钟, 拆成11小时40分钟
		}
		// 分钟,如果位数不够两位就变成两位,下面两个if语句的作用也是一样。
		if (Minute < 10) {
			Minute = '0' + Minute;
		}
		// 小时
		if (Hour < 10) {
			Hour = '0' + Hour;
		}
		// 秒
		if (Second < 10) {
			Second = '0' + Second;
		}
		return Hour + ':' + Minute + ':' + Second + ',' + MilliSecond;
	}
 
	// copy from: https://gist.github.com/danallison/3ec9d5314788b337b682
	// Thanks! https://github.com/danallison
	// work in Chrome 66
	// test passed: 2018-5-19
	function downloadString(text, fileType, fileName) {
		var blob = new Blob([text], {type: fileType});
		var a = document.createElement('a');
		a.download = fileName;
		a.href = URL.createObjectURL(blob);
		a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
		a.style.display = "none";
		document.body.appendChild(a);
		a.click();
		document.body.removeChild(a);
		setTimeout(function () {
			URL.revokeObjectURL(a.href);
		}, 1500);
	}
 
	// https://css-tricks.com/snippets/javascript/unescape-html-in-js/
	// turn HTML entity back to text, example: &quot; should be "
	function htmlDecode(input) {
		var e = document.createElement('div');
		e.class = 'dummy-element-for-tampermonkey-Youtube-Subtitle-Downloader-script-to-decode-html-entity';
		e.innerHTML = input;
		return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
	}
 
	// return URL or null;
	// later we can send a AJAX and get XML subtitle
	function get_auto_xml_url() {
		try {
			var captionTracks = get_captionTracks()
			for (var index in captionTracks) {
				var caption = captionTracks[index];
				if (caption.kind === 'asr') {
					return captionTracks[index].baseUrl;
				}
				// ASR – A caption track generated using automatic speech recognition.
				// https://developers.google.com/youtube/v3/docs/captions
			}
			return false;
		} catch (e) {
			console.log(e);
			return false;
		}
	}
	async function get_auto_subtitle() {
		var url = get_auto_xml_url();
		console.log("dwnld_auto_url::", url);
		if (url == false) {
			return false;
		}
		var result = await getUrl(url)
		return result
	}
 
	// Youtube return XML.
	// input: Youtube XML format
	// output: SRT format
	function buildSrtFromXML(youtube_xml_string) {
		if (youtube_xml_string === '') {
			return false;
		}
		var text = youtube_xml_string.getElementsByTagName('text');
		var result = '\uFEFF';
		var len = text.length;
		for (var i = 0; i < len; i++) {
			var content = text[i].textContent.toString();
			content = content.replace(/(<([^>]+)>)/ig, ""); // remove all html tag.
			var start = text[i].getAttribute('start');
			var end = parseFloat(text[i].getAttribute('start')) + parseFloat(text[i].getAttribute('dur'));
			result = result + (i + 1) + "\n";
			// 1
			if (i + 1 >= len) {
			  end = parseFloat(text[i].getAttribute('start')) + parseFloat(text[i].getAttribute('dur'));
			} else {
			  end = text[i + 1].getAttribute('start');
			}
 
			var start_time = process_time(parseFloat(start));
			var end_time = process_time(parseFloat(end));
			result = result + start_time;
			result = result + ' --> ';
			result = result + end_time + "\n";
			// 00:00:01,939 --> 00:00:04,350
 
			content = htmlDecode(content);
			// turn HTML entity back to text. example: &#39; back to apostrophe (')
 
			result = result + content + "\n" + "\n";
		}
		return result;
	}
	
	// bilibili return JSON.
	function buildSrtFromJson(bilibili_json_string) {
		var json = JSON.parse(bilibili_json_string);
		debug('buildSrtFromJson, json=', json);
		var arr = json.body, result = '\uFEFF';
		for (var i = 0, len=arr.length; i < len; i++) {
			var content = arr[i].content;
			content = content.replace(/(<([^>]+)>)/ig, ""); // remove all html tag.
			var start = arr[i].from;
			var end = arr[i].to;
			// 1
			result = result + (i + 1) + "\n";
 
			var start_time = process_time(parseFloat(start));
			var end_time = process_time(parseFloat(end));
			result = result + start_time;
			result = result + ' --> ';
			result = result + end_time + "\n";
			// 00:00:01,939 --> 00:00:04,350
 
			// content = htmlDecode(content);
			// turn HTML entity back to text. example: &#39; back to apostrophe (')
 
			result = result + content + "\n" + "\n";
		}
		return result;
	}
 
	function get_captionTracks() {
		var json = null
		if (win.youtube_playerResponse_1c7) {
			json = youtube_playerResponse_1c7;
		} else if(ytplayer.config.args.player_response) {
			let raw_string = ytplayer.config.args.player_response;
			json = JSON.parse(raw_string);
		} else if (ytplayer.config.args.raw_player_response) {
			json = ytplayer.config.args.raw_player_response;
		}
		let captionTracks = json.captions.playerCaptionsTracklistRenderer.captionTracks;
		return captionTracks
	}
 
	function loadJson(url,cb,parm){
		//debug('loadJson!!!', url,parm)
		var req = new XMLHttpRequest();
		req.open(parm?'POST':'GET', url, true);
		req.responseType = 'json';
		if(cb){
			req.onload = function() {
				cb(req.response, req);
			};
			req.onerror = function() {
				cb(0, req);
			};
		}
		//req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
		//x.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
		req.send(parm);
	}
	
	// https://stackoverflow.com/questions/48969495/in-javascript-how-do-i-should-i-use-async-await-with-xmlhttprequest
	function makeRequest(method, url, load, type) {
		return new Promise(function (resolve, reject) {
			let xhr = new XMLHttpRequest();
			xhr.responseType = type;
			//xhr.timeout = 2000;
			xhr.onload = function () {
				debug('makeRequest, onload::', this.status, xhr.statusText);
				if (this.status >= 200 && this.status < 300) {
					if(load) {
						load(xhr);
						resolve('');
					} else {
						resolve(xhr);
					}
				} else {
					debug('makeRequest, 发生错误::', this.status, xhr.statusText);
					reject({
						status: this.status,
						statusText: xhr.statusText
					});
				}
			};
			xhr.onerror = function () {
				debug('makeRequest, 发生错误::', this.status, xhr.statusText);
				reject({
					status: this.status,
					statusText: xhr.statusText
				});
			};
			xhr.open(method, url);
			xhr.send();
		});
	}
	async function getUrl(url) {
		return makeRequest("GET", url);
	}
	
	
})();