-
-
Save wxupjack/e0ca8455f4fafebe8d46cbc9f4a910b1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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=" "+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: " 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: ' 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: ' 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); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment