mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
1 line
16 KiB
JavaScript
1 line
16 KiB
JavaScript
(()=>{return(e,t)=>{const{getFriendlyTitle:s}=t.import("title");const{VideoInfo:a,DanmakuInfo:i}=t.import("video-info");class n{constructor(){this.menuClasses=["quality","action","progress"];[this.currentMenuClass]=this.menuClasses}get menuPanel(){return document.querySelector(".download-video-panel")}addMenuClass(){this.menuPanel.classList.remove(...this.menuClasses);this.menuPanel.classList.add(this.currentMenuClass);return this.currentMenuClass}resetMenuClass(){[this.currentMenuClass]=this.menuClasses;this.addMenuClass()}nextMenuClass(){const e=this.menuClasses.indexOf(this.currentMenuClass)+1;const t=this.menuClasses[e>=this.menuClasses.length?0:e];this.currentMenuClass=t;this.addMenuClass();return t}closeMenu(){this.menuPanel.classList.remove("opened");setTimeout(()=>this.resetMenuClass(),200)}addError(){this.menuPanel.classList.add("error")}removeError(){this.menuPanel.classList.remove("error");this.resetMenuClass()}async getUrl(e){if(e){return`https://api.bilibili.com/x/player/playurl?avid=${r.aid}&cid=${r.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/x/player/playurl?avid=${r.aid}&cid=${r.cid}&otype=json`}}}class o extends n{async getUrl(e){if(e){return`https://api.bilibili.com/pgc/player/web/playurl?avid=${r.aid}&cid=${r.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/pgc/player/web/playurl?avid=${r.aid}&cid=${r.cid}&qn=&otype=json`}}}const r={entity:new n,aid:"",cid:""};let l=[];let c=null;class d{constructor(e,t,s){this.quality=e;this.internalName=t;this.displayName=s}async downloadInfo(){const e=new u(this);await e.fetchVideoInfo();return e}static get availableFormats(){return new Promise((e,t)=>{r.entity.getUrl().then(s=>{const a=new XMLHttpRequest;a.addEventListener("load",()=>{const s=JSON.parse(a.responseText);if(s.code!==0){t("获取清晰度信息失败.");return}const i=s.data||s.result||s;const n=i.accept_quality;const o=i.accept_format.split(",");const r=i.accept_description;const l=[];while(n.length>0){const e=new d(n.pop(),o.pop(),r.pop());l.push(e)}e(l.reverse())});a.addEventListener("error",()=>t(`获取清晰度信息失败.`));a.withCredentials=true;a.open("GET",s);a.send()})})}}class h{constructor(e,t,s,a){this.length=e;this.size=t;this.url=s;this.backupUrls=a}}class u{constructor(e,t){this.fragmentSplitFactor=6*5;this.workingXhr=null;this.progressMap=new Map;this.format=e;this.fragments=t||[];this.videoSpeed=new p(this)}get totalSize(){return this.fragments.map(e=>e.size).reduce((e,t)=>e+t)}fetchVideoInfo(){return new Promise((e,t)=>{r.entity.getUrl(this.format.quality).then(s=>{const a=new XMLHttpRequest;a.addEventListener("load",()=>{const s=JSON.parse(a.responseText.replace(/http:/g,"https:"));const i=s.data||s.result||s;if(i.quality!==this.format.quality){t("获取下载链接失败, 请确认当前账号有下载权限后重试.")}const n=i.durl;this.fragments=n.map(e=>new h(e.length,e.size,e.url,e.backup_url));e(this.fragments)});a.withCredentials=true;a.open("GET",s);a.send()})})}updateProgress(){const e=this.progressMap?[...this.progressMap.values()].reduce((e,t)=>e+t,0)/this.totalSize:0;if(e>1||e<0){console.error(`[下载视频] 进度异常: ${e}`,this.progressMap.values())}this.progress&&this.progress(e)}cancelDownload(){this.videoSpeed.stopMeasure();if(this.workingXhr!==null){this.workingXhr.forEach(e=>e.abort())}else{logError("Cancel Download Failed: forEach in this.workingXhr not found.")}}downloadFragment(e){const t=[];this.workingXhr=[];this.progressMap=new Map;this.updateProgress();const s=16*1024*1024;let a=0;const i=e=>[...this.progressMap.keys()].indexOf(e)+1;while(a<e.size){const n=Math.min(e.size-1,Math.round(a+s));const o=`bytes=${a}-${n}`;const r=n-a+1;t.push(new Promise((t,s)=>{const a=new XMLHttpRequest;a.open("GET",e.url);a.responseType="arraybuffer";a.withCredentials=false;a.addEventListener("progress",e=>{console.log(`[下载视频] 视频片段${i(a)}下载进度: ${e.loaded}/${r} bytes loaded, ${o}`);this.progressMap.set(a,e.loaded);this.updateProgress()});a.addEventListener("load",()=>{if((""+a.status)[0]==="2"){console.log(`[下载视频] 视频片段${i(a)}下载完成`);t(a.response)}else{s(`视频片段${i(a)}请求失败, response = ${a.status}`)}});a.addEventListener("abort",()=>s("canceled"));a.addEventListener("error",()=>{console.error(`[下载视频] 视频片段${i(a)}下载失败: ${o}`);this.progressMap.set(a,0);this.updateProgress();a.open("GET",e.url);a.setRequestHeader("Range",o);a.send()});a.setRequestHeader("Range",o);this.progressMap.set(a,0);a.send();this.workingXhr.push(a)}));a=Math.round(a+s)+1}return Promise.all(t)}async copyUrl(){const e=this.fragments.map(e=>e.url).reduce((e,t)=>e+"\r\n"+t);GM_setClipboard(e,"text")}static downloadBlob(e,t){const s=document.createElement("a");let a;if(typeof e==="string"){a=e}else{a=URL.createObjectURL(e)}s.setAttribute("href",a);s.setAttribute("download",t);document.body.appendChild(s);s.click();s.remove();URL.revokeObjectURL(a)}async exportData(e=false){const t=JSON.stringify([{fragments:this.fragments,title:s(),totalSize:this.fragments.map(e=>e.size).reduce((e,t)=>e+t),referer:document.URL.replace(window.location.search,"")}]);if(e){GM_setClipboard(t,"text")}else{const e=new Blob([t],{type:"text/json"});const a=await this.downloadDanmaku();if(a!==null){const t=new JSZip;t.file(`${s()}.json`,e);t.file(s()+"."+this.danmakuOption.toLowerCase(),a);u.downloadBlob(await t.generateAsync({type:"blob"}),`${s()}.zip`)}else{u.downloadBlob(e,`${s()}.json`)}}}async exportAria2(a=false){if(a){const a=await this.downloadDanmaku();if(a!==null){u.downloadBlob(new Blob([a]),`${s()}.${this.danmakuOption==="ASS"?"ass":"xml"}`)}const i=e.aria2RpcOption;const n=this.fragments.map((e,t)=>{let a="";if(this.fragments.length>1){a=" - "+(t+1)}const n=[];if(i.secretKey!==""){n.push(`token:${i.secretKey}`)}n.push([e.url]);n.push({referer:document.URL.replace(window.location.search,""),"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0",out:`${s()}${a}${this.extension(e)}`,split:this.fragmentSplitFactor,dir:i.dir||undefined,"max-download-limit":i.maxDownloadLimit||undefined});const o=encodeURIComponent(`${s()}${a}`);return{params:n,id:o}});const{sendRpc:o}=await t.importAsync("aria2-rpc");await o(n)}else{const e=`\n# Generated by Bilibili Evolved Video Export\n# https://github.com/the1812/Bilibili-Evolved/\n${this.fragments.map((e,t)=>{let a="";if(this.fragments.length>1){a=" - "+(t+1)}return`\n${e.url}\n referer=${document.URL.replace(window.location.search,"")}\n user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0\n out=${s()}${a}${this.extension(e)}\n split=${this.fragmentSplitFactor}\n `.trim()}).join("\n")}\n `.trim();const t=new Blob([e],{type:"text/plain"});const a=await this.downloadDanmaku();if(a!==null){const e=new JSZip;e.file(`${s()}.txt`,t);e.file(s()+"."+this.danmakuOption.toLowerCase(),a);u.downloadBlob(await e.generateAsync({type:"blob"}),`${s()}.zip`)}else{u.downloadBlob(t,`${s()}.txt`)}}}extension(e){return(e||this.fragments[0]).url.indexOf(".flv")!==-1?".flv":".mp4"}makeBlob(e,t){return new Blob(Array.isArray(e)?e:[e],{type:this.extension(t)===".flv"?"video/x-flv":"video/mp4"})}cleanUpOldBlobUrl(){const e=dq("a#video-complete").getAttribute("href");if(e&&!dq(`.link[href="${e}"]`)){URL.revokeObjectURL(e)}dqa(".toast-card-header").filter(e=>e.innerText.includes("下载视频")).forEach(e=>e.querySelector(".toast-card-dismiss").click())}async downloadDanmaku(){if(this.danmakuOption!=="无"){const e=new i(r.cid);await e.fetchInfo();if(this.danmakuOption==="XML"){return e.rawXML}else{const{convertToAss:s}=await t.importAsync("download-danmaku");return s(e.rawXML)}}else{return null}}async downloadSingle(e){const t=await this.downloadDanmaku();const[a]=e;if(t===null){const e=this.makeBlob(a);const t=s()+this.extension();return{blob:e,filename:t}}else{const e=new JSZip;e.file(s()+this.extension(),this.makeBlob(a));e.file(s()+"."+this.danmakuOption.toLowerCase(),t);const i=await e.generateAsync({type:"blob"});const n=s()+".zip";return{blob:i,filename:n}}}async downloadMultiple(e){const t=new JSZip;const a=s();if(e.length>1){e.forEach((e,s)=>{const i=this.fragments[s];t.file(`${a} - ${s+1}${this.extension(i)}`,this.makeBlob(e,i))})}else{const[s]=e;t.file(`${a}${this.extension()}`,this.makeBlob(s))}const i=await this.downloadDanmaku();if(i!==null){t.file(s()+"."+this.danmakuOption.toLowerCase(),i)}const n=await t.generateAsync({type:"blob"});const o=a+".zip";return{blob:n,filename:o}}async download(){const e=[];this.videoSpeed.startMeasure();for(const t of this.fragments){const s=await this.downloadFragment(t);e.push(s)}if(e.length<1){throw new Error("下载失败.")}let{blob:t,filename:s}=await(async()=>{if(e.length===1){return await this.downloadSingle(e)}else{return await this.downloadMultiple(e)}})();this.cleanUpOldBlobUrl();const a=URL.createObjectURL(t);this.progress&&this.progress(0);this.videoSpeed.stopMeasure();return{url:a,filename:s}}}class p{constructor(e){this.lastProgress=0;this.measureInterval=1e3;this.workingDownloader=e}startMeasure(){this.intervalTimer=setInterval(()=>{const e=this.workingDownloader.progressMap?[...this.workingDownloader.progressMap.values()].reduce((e,t)=>e+t,0):0;const t=e-this.lastProgress;if(this.speedUpdate!==undefined){this.speedUpdate(formatFileSize(t)+"/s")}this.lastProgress=e},this.measureInterval)}stopMeasure(){clearInterval(this.intervalTimer)}}async function w(){const e=await SpinQuery.select(()=>(unsafeWindow||window).aid);const t=await SpinQuery.select(()=>(unsafeWindow||window).cid);if(!(e&&t)){return false}r.aid=e;r.cid=t;if(document.URL.indexOf("bangumi")!==-1){r.entity=new o}else{r.entity=new n}try{l=await d.availableFormats}catch(e){return false}return true}async function f(){c=l[0];t.applyStyle("downloadVideoStyle");dq("#download-video").addEventListener("click",()=>{dq(".download-video").classList.toggle("opened");dq(".gui-settings-mask").click()});dq("#download-video").addEventListener("mouseover",()=>{document.body.insertAdjacentHTML("beforeend",t.import("downloadVideoHtml"));m()},{once:true})}async function m(){const n=await t.importAsync("v-dropdown.vue");const o=await t.importAsync("v-checkbox.vue");let h;const p=new Map;const f=new Vue({el:".download-video",components:{"v-dropdown":n,"v-checkbox":o},data:{downloadSingle:true,coverUrl:'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"></svg>',aid:r.aid,cid:r.cid,qualityModel:{value:c.displayName,items:l.map(e=>e.displayName)},danmakuModel:{value:e.downloadVideoDefaultDanmaku,items:["无","XML","ASS"]},progressPercent:0,size:"获取大小中",blobUrl:"",episodeList:[],downloading:false,speed:"",batch:false,rpcSettings:e.aria2RpcOption,showRpcSettings:false,busy:false},computed:{displaySize(){if(typeof this.size==="string"){return this.size}return formatFileSize(this.size)},sizeWarning(){if(typeof this.size==="string"){return false}return this.size>1073741824},selectedEpisodeCount(){return this.episodeList.filter(e=>e.checked).length}},methods:{close(){this.$el.classList.remove("opened")},danmakuOptionChange(){e.downloadVideoDefaultDanmaku=this.danmakuModel.value},async formatChange(){const e=this.getFormat();const t=p.get(e);if(t){this.size=t;return}try{this.size="获取大小中";const t=await e.downloadInfo();this.size=t.totalSize;p.set(e,this.size)}catch(e){this.size="获取大小失败"}},getFormat(){const e=l.find(e=>e.displayName===this.qualityModel.value);if(!e){console.error(`No format found. model value = ${this.qualityModel.value}`);return null}return e},async exportData(e){if(this.busy===true){return}try{this.busy=true;if(!this.downloadSingle){await this.exportBatchData(e);return}const t=this.getFormat();const s=await t.downloadInfo();s.danmakuOption=this.danmakuModel.value;switch(e){case"copyLink":await s.copyUrl();Toast.success("已复制链接到剪贴板.","下载视频",3e3);break;case"aria2":await s.exportAria2(false);break;case"aria2RPC":await s.exportAria2(true);break;case"copyVLD":await s.exportData(true);Toast.success("已复制VLD数据到剪贴板.","下载视频",3e3);break;case"exportVLD":await s.exportData(false);break;default:break}}catch(e){logError(e)}finally{this.busy=false}},async exportBatchData(e){const a=this.episodeList;if(a.every(e=>e.checked===false)){Toast.info("请至少选择1集或以上的数量!","批量导出",3e3);return}const n=e=>{const t=a.find(t=>t.cid===e.cid);if(t===undefined){return false}return t.checked};const o=this.getFormat();if(this.danmakuModel.value!=="无"){const e=Toast.info("下载弹幕中...","批量导出");const s=new JSZip;try{if(this.danmakuModel.value==="XML"){for(const e of a.filter(n)){const t=new i(e.cid);await t.fetchInfo();s.file(e.title+".xml",t.rawXML)}}else{const{convertToAss:e}=await t.importAsync("download-danmaku");for(const t of a.filter(n)){const a=new i(t.cid);await a.fetchInfo();s.file(t.title+".ass",await e(a.rawXML))}}u.downloadBlob(await s.generateAsync({type:"blob"}),this.cid+".danmakus.zip")}catch(e){logError(`弹幕下载失败`)}finally{e.dismiss()}}const r=Toast.info("获取链接中...","批量导出");this.batchExtractor.itemFilter=n;let l;try{switch(e){case"aria2":l=await this.batchExtractor.collectAria2(o,r);u.downloadBlob(new Blob([l],{type:"text/plain"}),s(false)+".txt");return;case"aria2RPC":await this.batchExtractor.collectAria2(o,r,true);Toast.success(`成功发送了批量请求.`,"aria2 RPC",3e3);return;case"copyVLD":GM_setClipboard(await this.batchExtractor.collectData(o,r),{mimetype:"text/plain"});Toast.success("已复制批量vld数据到剪贴板.","批量导出",3e3);return;case"exportVLD":l=await this.batchExtractor.collectData(o,r);u.downloadBlob(new Blob([l],{type:"text/json"}),s(false)+".json");return;default:return}}catch(e){logError(e)}finally{r.dismiss()}},async checkBatch(){const e=["/www.bilibili.com/bangumi","/www.bilibili.com/video/av"];if(!e.some(e=>document.URL.includes(e))){this.batch=false;this.episodeList=[];return}const{BatchExtractor:s}=await t.importAsync("batch-download");if(await s.test()!==true){this.batch=false;this.episodeList=[];return}this.batchExtractor=new s;this.batch=true;this.episodeList=(await this.batchExtractor.getItemList()).map((e,t)=>{return{aid:e.aid,cid:e.cid,title:e.title,index:t,checked:true}})},cancelDownload(){if(h){h.cancelDownload()}},async startDownload(){const e=this.getFormat();try{this.downloading=true;const t=await e.downloadInfo();t.videoSpeed.speedUpdate=(e=>this.speed=e);t.danmakuOption=this.danmakuModel.value;t.progress=(e=>{this.progressPercent=Math.trunc(e*100)});h=t;const s=await t.download();const a=document.getElementById("video-complete");a.setAttribute("href",s.url);a.setAttribute("download",s.filename);a.click();Toast.success(`下载完成: ${s.filename} <a class="link" href="${s.url}" download="${s.filename.replace(/"/g,""")}">再次保存</a>`,"下载视频")}catch(e){if(e!=="canceled"){logError(e)}this.progressPercent=0}finally{this.downloading=false;this.speed=""}},selectAllEpisodes(){this.episodeList.forEach(e=>e.checked=true)},unselectAllEpisodes(){this.episodeList.forEach(e=>e.checked=false)},inverseAllEpisodes(){this.episodeList.forEach(e=>e.checked=!e.checked)},toggleRpcSettings(){this.showRpcSettings=!this.showRpcSettings},saveRpcSettings(){if(this.rpcSettings.host===""){this.rpcSettings.host="127.0.0.1"}if(this.rpcSettings.port===""){this.rpcSettings.port="6800"}e.aria2RpcOption=this.rpcSettings}}});Observer.videoChange(async()=>{f.close();f.batch=false;f.downloadSingle=true;const e=dq("#download-video");const t=await w();e.style.display=t?"flex":"none";if(!t){return}f.aid=r.aid;f.cid=r.cid;const s=new a(r.aid);await s.fetchInfo();f.coverUrl=s.coverUrl.replace("http:","https:");l=await d.availableFormats;[c]=l;f.qualityModel={value:c.displayName,items:l.map(e=>e.displayName)};f.formatChange();await f.checkBatch()})}return{widget:{content:`\n <button class="gui-settings-flat-button" style="position: relative; z-index: 100;" id="download-video">\n <i class="icon-download"></i>\n <span>下载视频</span>\n </button>`,condition:w,success:f}}}})(); |