Skip to content

Instantly share code, notes, and snippets.

@wxupjack
Forked from KnIfER/B站学习机.user.js
Last active June 5, 2023 11:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wxupjack/3753b3d5b9df1ad3603b520f29761ac3 to your computer and use it in GitHub Desktop.
Save wxupjack/3753b3d5b9df1ad3603b520f29761ac3 to your computer and use it in GitHub Desktop.
[fix] 修复了b站更新api后无法使用等bug
// ==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 , true, u, p); // set async to true to avoid 'sync responseType error'
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 = 8 * (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(){
if (TextPane.style.height == '50%'){
TextPane.style.height = minHeight + 'px';
}else{
TextPane.style.height = '50%';
}
}
// 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(0, 0, 0, 0.75)';
tvPs.color = 'white';
}else{
tvPs.background = 'rgb(255, 255, 255)';
tvPs.color = 'black';
}
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];
// bilibili new api does not have https prefix
var url = (isBY==0) ? "https:" + 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(Menu && Menu.children) {
for (var i=0,ch=Menu.children,len=ch.length; i < len; i++) {
ch[i].onclick = menuClick;
}
}
}
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;
if(Menu && Menu.children) {
for (var i=0,ch=Menu.children,len=ch.length; i < len; i++) {
ch[i].onclick = menuClick;
}
}
}
} catch(e) {
debug('获取字幕列表失败!', e)
Btn.parsedVid="";
}
} else {
Btn.parsedVid="";
}
debug('tracks', arr);
debug("autosel", autosel);
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';
// bilibili API need SESSDATA key from browser's cookies, carry cookies of session for it
req.withCredentials = true;
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, true); // set async to true to avoid 'sync responseType error'
xhr.send();
});
}
async function getUrl(url) {
return makeRequest("GET", url);
}
})();
@wxupjack
Copy link
Author

wxupjack commented Jun 4, 2023

[fix] 修复了b站更新api后无法使用等bug

  • b站api更新后的subtitle_url地址需要手动添加https前缀
  • 同时,需要携带SESSDATA这个cookie才能获取到字幕信息,否则返回空数组
  • 修复了异步时,未能绑定 menuClick方法 到菜单按钮的bug
  • 修复了发送请求时指定 responseType,却没有在open函数中传入 async=true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment