Bilibili-Evolved/min/download-video.min.js
2019-10-31 23:23:20 +08:00

1 line
16 KiB
JavaScript

(()=>{return(t,e)=>{const{getFriendlyTitle:a}=e.import("title");const{VideoInfo:s,DanmakuInfo:i}=e.import("video-info");class o{async getDashUrl(t){throw new Error("video dash is not supported")}async getUrl(t){if(t){return`https://api.bilibili.com/x/player/playurl?avid=${r.aid}&cid=${r.cid}&qn=${t}&otype=json`}else{return`https://api.bilibili.com/x/player/playurl?avid=${r.aid}&cid=${r.cid}&otype=json`}}}class n extends o{async getDashUrl(t){if(t){return`https://api.bilibili.com/pgc/player/web/playurl?avid=56995872&cid=99547543&qn=${t}&otype=json&fourk=1&fnval=16`}else{return`https://api.bilibili.com/pgc/player/web/playurl?avid=56995872&cid=99547543&otype=json&fourk=1&fnval=16`}}async getUrl(t){if(t){return`https://api.bilibili.com/pgc/player/web/playurl?avid=${r.aid}&cid=${r.cid}&qn=${t}&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 o,aid:"",cid:""};let l=[];let c=null;class d{constructor(t,e,a){this.quality=t;this.internalName=e;this.displayName=a}async downloadInfo(t=false){const e=new h(this);await e.fetchVideoInfo(t);return e}static parseFormats(t){const e=t.accept_quality;const a=t.accept_format.split(",");const s=t.accept_description;const i=e.map((t,e)=>{return new d(t,a[e],s[e])});return i}static async getAvailableDashFormats(){const t=await r.entity.getDashUrl();const e=await Ajax.getJsonWithCredentials(t);if(e.code!==0){throw new Error("获取清晰度信息失败.")}return d.parseFormats(e.result)}static async getAvailableFormats(){const t=await r.entity.getUrl();const e=await Ajax.getJsonWithCredentials(t);if(e.code!==0){throw new Error("获取清晰度信息失败.")}const a=e.data||e.result||e;return d.parseFormats(a)}}class h{constructor(t,e){this.fragmentSplitFactor=6*2;this.workingXhr=null;this.progressMap=new Map;this.format=t;this.fragments=e||[];this.videoSpeed=new p(this)}get totalSize(){return this.fragments.map(t=>t.size).reduce((t,e)=>t+e)}async fetchVideoInfo(t=false){if(!t){const t=await r.entity.getUrl(this.format.quality);const e=await Ajax.getTextWithCredentials(t);const a=JSON.parse(e.replace(/http:/g,"https:"));const s=a.data||a.result||a;if(s.quality!==this.format.quality){throw new Error("获取下载链接失败, 请确认当前账号有下载权限后重试.")}const i=s.durl;this.fragments=i.map(t=>{return{length:t.length,size:t.size,url:t.url,backupUrls:t.backup_url}})}else{const{dashToFragment:t,getDashInfo:a}=await e.importAsync("video-dash");const s=await a(r.aid,r.cid,this.format.quality);const i=s.videoDashes.sort(descendingSort(t=>t.bandWidth))[0];const o=s.audioDashes.sort(descendingSort(t=>t.bandWidth))[0];this.fragments=[t(i),t(o)]}return this.fragments}updateProgress(){const t=this.progressMap?[...this.progressMap.values()].reduce((t,e)=>t+e,0)/this.totalSize:0;if(t>1||t<0){console.error(`[下载视频] 进度异常: ${t}`,this.progressMap.values())}this.progress&&this.progress(t)}cancelDownload(){this.videoSpeed.stopMeasure();if(this.workingXhr!==null){this.workingXhr.forEach(t=>t.abort())}else{logError("Cancel Download Failed: forEach in this.workingXhr not found.")}}downloadFragment(t){const e=[];this.workingXhr=[];this.progressMap=new Map;this.updateProgress();let a;if(t.size<=96*1024*1024){a=t.size/this.fragmentSplitFactor}else{a=16*1024*1024}let s=0;const i=t=>[...this.progressMap.keys()].indexOf(t)+1;while(s<t.size){const o=Math.min(t.size-1,Math.round(s+a));const n=`bytes=${s}-${o}`;const r=o-s+1;e.push(new Promise((e,a)=>{const s=new XMLHttpRequest;s.open("GET",t.url);s.responseType="arraybuffer";s.withCredentials=false;s.addEventListener("progress",t=>{console.log(`[下载视频] 视频片段${i(s)}下载进度: ${t.loaded}/${r} bytes loaded, ${n}`);this.progressMap.set(s,t.loaded);this.updateProgress()});s.addEventListener("load",()=>{if((""+s.status)[0]==="2"){console.log(`[下载视频] 视频片段${i(s)}下载完成`);e(s.response)}else{a(`视频片段${i(s)}请求失败, response = ${s.status}`)}});s.addEventListener("abort",()=>a("canceled"));s.addEventListener("error",()=>{console.error(`[下载视频] 视频片段${i(s)}下载失败: ${n}`);this.progressMap.set(s,0);this.updateProgress();s.open("GET",t.url);s.setRequestHeader("Range",n);s.send()});s.setRequestHeader("Range",n);this.progressMap.set(s,0);s.send();this.workingXhr.push(s)}));s=Math.round(s+a)+1}return Promise.all(e)}async copyUrl(){const t=this.fragments.map(t=>t.url).reduce((t,e)=>t+"\r\n"+e);GM.setClipboard(t,"text")}async showUrl(){const t=this.fragments.map(t=>`\n<a class="download-link" href="${t.url}">${t.url}</a>\n`).reduce((t,e)=>t+"\r\n"+e);Toast.success(t+`<a class="link" id="copy-link" style="cursor: pointer;margin: 8px 0 0 0;">复制全部</a>`,"显示链接");const e=await SpinQuery.select("#copy-link");e.addEventListener("click",async()=>{await this.copyUrl()})}async exportData(t=false){const s=JSON.stringify([{fragments:this.fragments,title:a(),totalSize:this.fragments.map(t=>t.size).reduce((t,e)=>t+e),referer:document.URL.replace(window.location.search,"")}]);if(t){GM.setClipboard(s,"text")}else{const t=new Blob([s],{type:"text/json"});const i=await this.downloadDanmaku();const{DownloadVideoPackage:o}=await e.importAsync("download-video-package");const n=new o;n.add(`${a()}.json`,t);n.add(a()+"."+this.danmakuOption.toLowerCase(),i);await n.emit(`${a()}.zip`)}}async exportAria2(s=false){if(s){const s=await this.downloadDanmaku();if(s!==null){const{DownloadVideoPackage:t}=await e.importAsync("download-video-package");t.single(`${a()}.${this.danmakuOption==="ASS"?"ass":"xml"}`,s)}const i=t.aria2RpcOption;const o=this.fragments.map((t,e)=>{let s="";if(this.fragments.length>1){s=" - "+(e+1)}const o=[];if(i.secretKey!==""){o.push(`token:${i.secretKey}`)}o.push([t.url]);o.push({referer:document.URL.replace(window.location.search,""),"user-agent":UserAgent,out:`${a()}${s}${this.extension(t)}`,split:this.fragmentSplitFactor,dir:i.baseDir+i.dir||undefined,"max-download-limit":i.maxDownloadLimit||undefined});const n=encodeURIComponent(`${a()}${s}`);return{params:o,id:n}});const{sendRpc:n}=await e.importAsync("aria2-rpc");await n(o)}else{const t=`\n# Generated by Bilibili Evolved Video Export\n# https://github.com/the1812/Bilibili-Evolved/\n${this.fragments.map((t,e)=>{let s="";if(this.fragments.length>1){s=" - "+(e+1)}return`\n${t.url}\n referer=${document.URL.replace(window.location.search,"")}\n user-agent=${UserAgent}\n out=${a()}${s}${this.extension(t)}\n split=${this.fragmentSplitFactor}\n`.trim()}).join("\n")}\n`.trim();const s=new Blob([t],{type:"text/plain"});const i=await this.downloadDanmaku();const{DownloadVideoPackage:o}=await e.importAsync("download-video-package");const n=new o;n.add(`${a()}.txt`,s);n.add(a()+"."+this.danmakuOption.toLowerCase(),i);await n.emit(`${a()}.zip`)}}extension(t){const e=t||this.fragments[0];const a=[".flv",".mp4",".m4s"].find(t=>e.url.includes(t));if(a){return a}else{console.warn("No extension detected.");return".flv"}}makeBlob(t,e){return new Blob(Array.isArray(t)?t:[t],{type:this.extension(e)===".flv"?"video/x-flv":"video/mp4"})}cleanUpOldBlobUrl(){const t=dq("a#video-complete").getAttribute("href");if(t&&!dq(`.link[href="${t}"]`)){URL.revokeObjectURL(t)}dqa(".toast-card-header").filter(t=>t.innerText.includes("下载视频")).forEach(t=>t.querySelector(".toast-card-dismiss").click())}async downloadDanmaku(){if(this.danmakuOption!=="无"){const t=new i(r.cid);await t.fetchInfo();if(this.danmakuOption==="XML"){return t.rawXML}else{const{convertToAss:a}=await e.importAsync("download-danmaku");return a(t.rawXML)}}else{return null}}async downloadSingle(t){const e=await this.downloadDanmaku();const[s]=t;if(e===null){const t=this.makeBlob(s);const e=a()+this.extension();return{blob:t,filename:e}}else{const t=new JSZip;t.file(a()+this.extension(),this.makeBlob(s));t.file(a()+"."+this.danmakuOption.toLowerCase(),e);const i=await t.generateAsync({type:"blob"});const o=a()+".zip";return{blob:i,filename:o}}}async downloadMultiple(t){const e=new JSZip;const s=a();if(t.length>1){t.forEach((t,a)=>{const i=this.fragments[a];e.file(`${s} - ${a+1}${this.extension(i)}`,this.makeBlob(t,i))})}else{const[a]=t;e.file(`${s}${this.extension()}`,this.makeBlob(a))}const i=await this.downloadDanmaku();if(i!==null){e.file(a()+"."+this.danmakuOption.toLowerCase(),i)}const o=await e.generateAsync({type:"blob"});const n=s+".zip";return{blob:o,filename:n}}async download(){const t=[];this.videoSpeed.startMeasure();for(const e of this.fragments){const a=await this.downloadFragment(e);t.push(a)}if(t.length<1){throw new Error("下载失败.")}let{blob:e,filename:a}=await(async()=>{if(t.length===1){return await this.downloadSingle(t)}else{return await this.downloadMultiple(t)}})();this.cleanUpOldBlobUrl();const s=URL.createObjectURL(e);this.progress&&this.progress(0);this.videoSpeed.stopMeasure();return{url:s,filename:a}}}class p{constructor(t){this.lastProgress=0;this.measureInterval=1e3;this.workingDownloader=t}startMeasure(){this.intervalTimer=setInterval(()=>{const t=this.workingDownloader.progressMap?[...this.workingDownloader.progressMap.values()].reduce((t,e)=>t+e,0):0;const e=t-this.lastProgress;if(this.speedUpdate!==undefined){this.speedUpdate(formatFileSize(e)+"/s")}this.lastProgress=t},this.measureInterval)}stopMeasure(){clearInterval(this.intervalTimer)}}async function u(){const t=await SpinQuery.select(()=>(unsafeWindow||window).aid);const e=await SpinQuery.select(()=>(unsafeWindow||window).cid);if(!(t&&e)){return false}r.aid=t;r.cid=e;if(document.URL.indexOf("bangumi")!==-1){r.entity=new n}else{r.entity=new o}try{l=await d.getAvailableFormats()}catch(t){return false}return true}async function w(){c=l[0];e.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",e.import("downloadVideoHtml"));f()},{once:true})}async function f(){let o;const n=new Map;const h=new Vue({el:".download-video",components:{VDropdown:()=>e.importAsync("v-dropdown.vue"),VCheckbox:()=>e.importAsync("v-checkbox.vue"),RpcProfiles:()=>e.importAsync("aria2-rpc-profiles.vue")},data:{downloadSingle:true,coverUrl:EmptyImageUrl,aid:r.aid,cid:r.cid,dashModel:{value:t.downloadVideoFormat,items:["flv","dash"]},qualityModel:{value:c.displayName,items:l.map(t=>t.displayName)},danmakuModel:{value:t.downloadVideoDefaultDanmaku,items:["无","XML","ASS"]},progressPercent:0,size:"获取大小中",blobUrl:"",episodeList:[],downloading:false,speed:"",batch:false,rpcSettings:t.aria2RpcOption,showRpcSettings:false,busy:false,saveRpcSettingsText:"保存配置",enableDash:t.enableDashDownload},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(t=>t.checked).length},dash(){return this.dashModel.value==="dash"}},methods:{close(){this.$el.classList.remove("opened")},danmakuOptionChange(){t.downloadVideoDefaultDanmaku=this.danmakuModel.value},async dashChange(){const t=this.dashModel.value;let e=[];if(t==="flv"){e=await d.getAvailableFormats()}else{e=await d.getAvailableDashFormats()}l=e;[c]=t;this.qualityModel.items=e.map(t=>t.displayName);[this.qualityModel.value]=this.qualityModel.items},async formatChange(){const t=this.getFormat();const e=n.get(t);if(e){this.size=e;return}try{this.size="获取大小中";const e=await t.downloadInfo(this.dash);this.size=e.totalSize;n.set(t,this.size)}catch(t){this.size="获取大小失败"}},getFormat(){const t=l.find(t=>t.displayName===this.qualityModel.value);if(!t){console.error(`No format found. model value = ${this.qualityModel.value}`);return null}return t},async exportData(t){if(this.busy===true){return}try{this.busy=true;if(!this.downloadSingle){await this.exportBatchData(t);return}const e=this.getFormat();const a=await e.downloadInfo(this.dash);a.danmakuOption=this.danmakuModel.value;switch(t){case"copyLink":await a.copyUrl();Toast.success("已复制链接到剪贴板.","下载视频",3e3);break;case"showLink":await a.showUrl();break;case"aria2":await a.exportAria2(false);break;case"aria2RPC":await a.exportAria2(true);break;case"copyVLD":await a.exportData(true);Toast.success("已复制VLD数据到剪贴板.","下载视频",3e3);break;case"exportVLD":await a.exportData(false);break;default:break}}catch(t){logError(t)}finally{this.busy=false}},async exportBatchData(t){const s=this.episodeList;if(s.every(t=>t.checked===false)){Toast.info("请至少选择1集或以上的数量!","批量导出",3e3);return}const o=t=>{const e=s.find(e=>e.cid===t.cid);if(e===undefined){return false}return e.checked};const n=this.getFormat();const{DownloadVideoPackage:r}=await e.importAsync("download-video-package");if(this.danmakuModel.value!=="无"){const t=Toast.info("下载弹幕中...","批量导出");const a=new r;try{if(this.danmakuModel.value==="XML"){for(const t of s.filter(o)){const e=new i(t.cid);await e.fetchInfo();a.add(t.title+".xml",e.rawXML)}}else{const{convertToAss:t}=await e.importAsync("download-danmaku");for(const e of s.filter(o)){const s=new i(e.cid);await s.fetchInfo();a.add(e.title+".ass",await t(s.rawXML))}}await a.emit(this.cid+".danmakus.zip")}catch(t){logError(`弹幕下载失败`)}finally{t.dismiss()}}const l=Toast.info("获取链接中...","批量导出");this.batchExtractor.itemFilter=o;let c;try{switch(t){case"aria2":c=await this.batchExtractor.collectAria2(n,l);await r.single(a(false)+".txt",new Blob([c],{type:"text/plain"}));return;case"aria2RPC":await this.batchExtractor.collectAria2(n,l,true);Toast.success(`成功发送了批量请求.`,"aria2 RPC",3e3);return;case"copyVLD":GM.setClipboard(await this.batchExtractor.collectData(n,l),{mimetype:"text/plain"});Toast.success("已复制批量vld数据到剪贴板.","批量导出",3e3);return;case"exportVLD":c=await this.batchExtractor.collectData(n,l);await r.single(a(false)+".json",new Blob([c],{type:"text/json"}));return;default:return}}catch(t){logError(t)}finally{l.dismiss()}},async checkBatch(){const t=["/www.bilibili.com/bangumi","/www.bilibili.com/video/av"];if(!t.some(t=>document.URL.includes(t))){this.batch=false;this.episodeList=[];return}const{BatchExtractor:a}=await e.importAsync("batch-download");if(await a.test()!==true){this.batch=false;this.episodeList=[];return}this.batchExtractor=new a;this.batch=true;this.episodeList=(await this.batchExtractor.getItemList()).map((t,e)=>{return{aid:t.aid,cid:t.cid,title:t.title,index:e,checked:true}})},cancelDownload(){if(o){o.cancelDownload()}},async startDownload(){const t=this.getFormat();try{this.downloading=true;const e=await t.downloadInfo(this.dash);e.videoSpeed.speedUpdate=(t=>this.speed=t);e.danmakuOption=this.danmakuModel.value;e.progress=(t=>{this.progressPercent=Math.trunc(t*100)});o=e;const a=await e.download();const s=document.getElementById("video-complete");s.setAttribute("href",a.url);s.setAttribute("download",a.filename);s.click();Toast.success(`下载完成: ${a.filename} <a class="link" href="${a.url}" download="${a.filename.replace(/"/g,"&quot;")}">再次保存</a>`,"下载视频")}catch(t){if(t!=="canceled"){logError(t)}this.progressPercent=0}finally{this.downloading=false;this.speed=""}},selectAllEpisodes(){this.episodeList.forEach(t=>t.checked=true)},unselectAllEpisodes(){this.episodeList.forEach(t=>t.checked=false)},inverseAllEpisodes(){this.episodeList.forEach(t=>t.checked=!t.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"}t.aria2RpcOption=this.rpcSettings;const e=t.aria2RpcOptionProfiles.find(e=>e.name===t.aria2RpcOptionSelectedProfile);if(e){Object.assign(e,this.rpcSettings);t.aria2RpcOptionProfiles=t.aria2RpcOptionProfiles}this.saveRpcSettingsText="已保存";setTimeout(()=>this.saveRpcSettingsText="保存配置",2e3)},updateProfile(e){t.aria2RpcOption=this.rpcSettings=_.omit(e,"name")}}});Observer.videoChange(async()=>{h.close();h.batch=false;h.downloadSingle=true;const t=dq("#download-video");const e=await u();t.style.display=e?"flex":"none";if(!e){return}h.aid=r.aid;h.cid=r.cid;const a=new s(r.aid);await a.fetchInfo();h.coverUrl=a.coverUrl.replace("http:","https:");l=await d.getAvailableFormats();[c]=l;h.qualityModel={value:c.displayName,items:l.map(t=>t.displayName)};h.formatChange();await h.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:u,success:w}}}})();