Bilibili-Evolved/min/download-video.min.js
2019-07-15 14:11:13 +08:00

1 line
12 KiB
JavaScript

(()=>{return(e,t)=>{const{getFriendlyTitle:s}=t.import("title");const{VideoInfo:n}=t.import("video-info");class o{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=${a.aid}&cid=${a.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/x/player/playurl?avid=${a.aid}&cid=${a.cid}&otype=json`}}}class i extends o{async getUrl(e){if(e){return`https://api.bilibili.com/pgc/player/web/playurl?avid=${a.aid}&cid=${a.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/pgc/player/web/playurl?avid=${a.aid}&cid=${a.cid}&qn=&otype=json`}}}const a={entity:new o,aid:"",cid:""};let r=[];let l=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)=>{a.entity.getUrl().then(s=>{const n=new XMLHttpRequest;n.addEventListener("load",()=>{const s=JSON.parse(n.responseText);if(s.code!==0){t("获取清晰度信息失败.");return}const o=s.data||s.result||s;const i=o.accept_quality;const a=o.accept_format.split(",");const r=o.accept_description;const l=[];while(i.length>0){const e=new d(i.pop(),a.pop(),r.pop());l.push(e)}e(l.reverse())});n.addEventListener("error",()=>t(`获取清晰度信息失败.`));n.withCredentials=true;n.open("GET",s);n.send()})})}}class c{constructor(e,t,s,n){this.length=e;this.size=t;this.url=s;this.backupUrls=n}}class u{constructor(e,t){this.fragmentSplitFactor=6*5;this.workingXhr=null;this.progressMap=new Map;this.format=e;this.fragments=t||[]}get totalSize(){return this.fragments.map(e=>e.size).reduce((e,t)=>e+t)}fetchVideoInfo(){return new Promise((e,t)=>{a.entity.getUrl(this.format.quality).then(s=>{const n=new XMLHttpRequest;n.addEventListener("load",()=>{const s=JSON.parse(n.responseText.replace(/http:/g,"https:"));const o=s.data||s.result||s;if(o.quality!==this.format.quality){t("获取下载链接失败, 请确认当前账号有下载权限后重试.")}const i=o.durl;this.fragments=i.map(e=>new c(e.length,e.size,e.url,e.backup_url));e(this.fragments)});n.withCredentials=true;n.open("GET",s);n.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(){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=Math.round(e.size/this.fragmentSplitFactor);let n=0;const o=e=>[...this.progressMap.keys()].indexOf(e)+1;while(n<e.size){const i=Math.min(e.size-1,Math.round(n+s));const a=`bytes=${n}-${i}`;const r=i-n+1;t.push(new Promise((t,s)=>{const n=new XMLHttpRequest;n.open("GET",e.url);n.responseType="arraybuffer";n.withCredentials=false;n.addEventListener("progress",e=>{console.log(`[下载视频] 视频片段${o(n)}下载进度: ${e.loaded}/${r} bytes loaded, ${a}`);this.progressMap.set(n,e.loaded);this.updateProgress()});n.addEventListener("load",()=>{if((""+n.status)[0]==="2"){t(n.response)}else{s(`请求失败.`)}});n.addEventListener("abort",()=>s("下载已取消."));n.addEventListener("error",()=>{console.error(`[下载视频] 视频片段${o(n)}下载失败: ${a}`);this.progressMap.set(n,0);this.updateProgress();n.open("GET",e.url);n.setRequestHeader("Range",a);n.send()});n.setRequestHeader("Range",a);this.progressMap.set(n,0);n.send();this.workingXhr.push(n)}));n=Math.round(n+s)+1}return Promise.all(t)}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 n;if(typeof e==="string"){n=e}else{n=URL.createObjectURL(e)}s.setAttribute("href",n);s.setAttribute("download",t);document.body.appendChild(s);s.click();s.remove();URL.revokeObjectURL(n)}exportData(e=false){const t=JSON.stringify([{fragments:this.fragments,title:s(true),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"});u.downloadBlob(e,`cid${unsafeWindow.cid}.json`)}}exportAria2(e=false){if(e){}else{const e=`\n# Generated by Bilibili Evolved Video Export\n# https://github.com/the1812/Bilibili-Evolved/\n${this.fragments.map((e,t)=>{let n="";if(this.fragments.length>1){n=" - "+(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(true)}${n}.flv\n split=${this.fragmentSplitFactor}\n `.trim()}).join("\n")}\n `.trim();const t=new Blob([e],{type:"text/plain"});u.downloadBlob(t,`${s(true)}.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())}downloadSingle(e){const[t]=e;const n=this.makeBlob(t);const o=s()+this.extension();return{blob:n,filename:o}}async downloadMultiple(e){const t=new JSZip;const n=s();if(e.length>1){e.forEach((e,s)=>{const o=this.fragments[s];t.file(`${n} - ${s+1}${this.extension(o)}`,this.makeBlob(e,o))})}else{const[s]=e;t.file(`${n}${this.extension()}`,this.makeBlob(s))}const o=await t.generateAsync({type:"blob"});const i=n+".zip";return{blob:o,filename:i}}async download(){const e=[];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 this.downloadSingle(e)}else{return await this.downloadMultiple(e)}})();this.cleanUpOldBlobUrl();const n=URL.createObjectURL(t);this.progress&&this.progress(0);return{url:n,filename:s}}}async function p(){const e=["/www.bilibili.com/bangumi","/www.bilibili.com/video/av"];if(!e.some(e=>document.URL.includes(e))){return}const{BatchExtractor:n}=await t.importAsync("batch-download");if(await n.test()!==true){return}const o=new n;document.getElementById("download-video").classList.add("batch");document.getElementById("video-action-batch-data").addEventListener("click",async()=>{if(!l){return}a.entity.closeMenu();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(l,e);if(!t){return}GM_setClipboard(t,{type:"text/json"});Toast.success("已复制批量数据到剪贴板.","复制批量数据",3e3)});document.getElementById("video-action-batch-download-data").addEventListener("click",async()=>{if(!l){return}a.entity.closeMenu();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(l,e);if(!t){return}const s=new Blob([t],{type:"text/json"});u.downloadBlob(s,"export.json")});document.getElementById("video-action-aria2-batch").addEventListener("click",async()=>{if(!l){return}a.entity.closeMenu();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectAria2(l,e);if(!t){return}const n=new Blob([t],{type:"text/plain"});u.downloadBlob(n,`${s(false)}.txt`)})}async function h(){const e=await SpinQuery.select(()=>(unsafeWindow||window).aid);const t=await SpinQuery.select(()=>(unsafeWindow||window).cid);if(!(e&&t)){return false}a.aid=e;a.cid=t;if(document.URL.indexOf("bangumi")!==-1){a.entity=new i}else{a.entity=new o}try{r=await d.availableFormats}catch(e){return false}return true}async function m(){l=r[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"));f()},{once:true});p()}async function f(){Vue.component("v-dropdown",{template:`\n <div class="v-dropdown" v-on:click="toggleDropDown()">\n <span class="selected">{{value}}</span>\n <ul class="dropdown-menu" v-bind:class="{opened: dropdownOpen}">\n <li v-for="item in model.items" v-bind:key="item" v-bind:data-value="item" v-on:click="select(item)">{{item}}</li>\n </ul>\n <i class="mdi mdi-chevron-down"></i>\n </div>\n `,props:["model"],data(){return{dropdownOpen:false}},computed:{value(){return this.model.value}},methods:{toggleDropDown(){this.dropdownOpen=!this.dropdownOpen},select(e){this.model.value=e;this.$emit("change")}}});let t;const o=new Map;const i=new Vue({el:".download-video",data:{downloadSingle:true,coverUrl:"",qualityModel:{value:l.displayName,items:r.map(e=>e.displayName)},danmakuModel:{value:e.downloadVideoDefaultDanmaku,items:["无","XML","ASS"]},progressPercent:0,title:s(false),size:"",blobUrl:"",episodeList:[],downloading:false},mounted(){this.formatChange()},methods:{close(){this.$el.classList.remove("opened")},async formatChange(){const e=this.getFormat();const t=o.get(e);if(t){this.size=t;return}try{this.size="获取大小中";const t=await e.downloadInfo();const s=t.totalSize;const n=["B","KB","MB","GB","TB","PB","EB","ZB","YB"];let i=s;let a=0;while(i>=1024){i/=1024;a++}const r=`${Math.round(i*10)/10}${n[a]}`;this.size=r;o.set(e,r)}catch(e){this.size="获取大小失败"}},getFormat(){const e=r.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){const t=this.getFormat();const s=await t.downloadInfo();switch(e){case"copyLink":s.copyUrl();Toast.success("已复制链接到剪贴板.","复制链接",3e3);break;case"aria2":s.exportAria2(false);break;case"copyVLD":s.exportData(true);break;case"exportVLD":s.exportData(false);break;default:break}},cancelDownload(){if(t){t.cancelDownload()}},async startDownload(){const e=this.getFormat();try{this.downloading=true;const s=await e.downloadInfo();s.progress=(e=>{this.progressPercent=Math.trunc(e*100)});t=s;const n=await s.download();const o=document.getElementById("video-complete");o.setAttribute("href",n.url);o.setAttribute("download",n.filename);o.click();Toast.success(`下载完成: ${n.filename} <a class="link" href="${n.url}" download="${n.filename.replace(/"/g,"&quot;")}">再次保存</a>`,"下载视频")}catch(e){if(e!=="下载已取消."){logError(e)}this.progressPercent=0}finally{this.downloading=false}}}});const d=new n(parseInt(a.aid));await d.fetchInfo();i.coverUrl=d.coverUrl}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:h,success:m}}}})();