diff --git a/bilibili-evolved.offline.user.js b/bilibili-evolved.offline.user.js index 87ff51496..c24c4e620 100644 --- a/bilibili-evolved.offline.user.js +++ b/bilibili-evolved.offline.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name Bilibili Evolved (Offline) -// @version 348.87 +// @version 349.00 // @description Bilibili Evolved 的离线版, 所有功能都已内置于脚本中. // @author Grant Howard, Coulomb-G // @copyright 2019, Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G) @@ -1012,7 +1012,7 @@ offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/m offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/about.min.js"] = (()=>{return(t,e)=>{(async()=>{const i=await e.importAsync("aboutHtml");document.body.insertAdjacentHTML("beforeend",i);const n=(t,e)=>t.charCodeAt(0)-e.charCodeAt(0);const o=(t,e)=>n(t.name,e.name);const s=GM_info.script.name.match(/Bilibili Evolved \((.*)\)/);const r=s?s[1]:"Stable";new Vue({el:".bilibili-evolved-about",data:{version:t.currentVersion,clientType:r,branch:null,authors:[{name:"Grant Howard",link:"https://github.com/the1812"},{name:"Coulomb-G",link:"https://github.com/Coulomb-G"}],contributors:[{name:"PleiadeSubaru",link:"https://github.com/Etherrrr"}].sort(o),fetching:true,participants:[],supporters:["*飞","N*v","*博睿","*杨","*泽鹏"].sort(n),websites:[{name:"GitHub",link:"https://github.com/the1812/Bilibili-Evolved/"},{name:"Greasy Fork",link:"https://greasyfork.org/zh-CN/scripts/373563-bilibili-evolved"}],components:[{name:"Vue.js",link:"https://cn.vuejs.org/index.html"},{name:"JSZip",link:"https://stuk.github.io/jszip/"},{name:"jQuery",link:"http://jquery.com/"},{name:"debounce",link:"https://github.com/component/debounce/"},{name:"Slip.js",link:"https://github.com/kornelski/slip"}]},mounted(){document.querySelector(".bilibili-evolved-about").addEventListener("be:about-load",()=>{this.init()},{once:true})},methods:{async init(){this.branch=/Preview|Local/.test(r)?"preview":"master";const t=new Set;let e=[];let i=1;do{e=await Ajax.getJson(`https://api.github.com/repos/the1812/Bilibili-Evolved/issues?state=all&direction=asc&per_page=100&page=${i}`).catch(()=>{e=[{name:"电波无法到达(´・_・`)",link:null}]});i++;for(const i of e){t.add(i.user.login)}}while(e.length>0);this.participants=[...t].map(t=>{return{name:t,link:`https://github.com/${t}`}}).filter(({link:t})=>{return!this.authors.some(e=>e.link===t)&&!this.contributors.some(e=>e.link===t)}).sort(o);this.fetching=false}}})})()}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/auto-continue.min.js"] = (()=>{return(e,i)=>{if(typeof isEmbeddedPlayer!=="undefined"&&isEmbeddedPlayer()){return}function t(i){const t=i.text();if(/第(\d+)话/.test(t)){if(e.allowJumpContinue){i.parent().find(".bilibili-player-video-toast-item-jump").click()}return}const n=/((\d)*:)?(\d)*:(\d)*/g;const r=t.match(n);if(!r){return}const o=r[0].split(":");const l=(()=>{if(o.length===3){const[e,i,t]=o.map(e=>parseInt(e));return e*60*60+i*60+t}else if(o.length===2){const[e,i]=o.map(e=>parseInt(e));return e*60+i}else{logError(`解析历史时间发生错误: historyTime=${JSON.stringify(o)}`);return NaN}})();const s=i.parent();const a=document.querySelector("video");if(l从头开始`);e.appendTo(s).on("click",()=>{a.currentTime=0;s.find(".bilibili-player-video-toast-item-close").get(0).click()});i.html(`已跳转到上次历史记录${r[0]}`)}else{s.find(".bilibili-player-video-toast-item-close").get(0).click()}}function n(){SpinQuery.condition(()=>$(".bilibili-player-video-toast-item-text"),e=>e.text().indexOf("上次看到")!==-1,e=>t(e.filter((e,i)=>i.innerText.indexOf("上次看到")!==-1)))}Observer.videoChange(n)}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/auto-play.min.js"] = (()=>{return(e,d)=>{if(typeof isEmbeddedPlayer!=="undefined"&&isEmbeddedPlayer()){return}SpinQuery.condition(()=>document.querySelector(".bilibili-player-video video"),e=>e&&e.paused===true,e=>e.play())}})(); -offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/batch-download.min.js"] = (()=>{return(t,e)=>{class i{static async test(){if(!document.URL.includes("/www.bilibili.com/video/av")){return false}return await SpinQuery.select("#multi_page")!==null}async collectData(t){const e=`https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`;const i=await Ajax.getJson(e);if(i.code!==0){Toast.error(`获取视频选集列表失败, message=${i.message}`,"批量下载");return""}const s=i.data.pages;if(s===undefined){Toast.error(`获取视频选集列表失败, 没有找到选集信息.`,"批量下载");return""}const a=[];for(const e of s){const i=`https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${e.cid}&qn=${t}&otype=json`;const s=await Ajax.getJsonWithCredentials(i);const r=s.data||s.result||s;if(r.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=r.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});a.push({fragments:n,title:`${e.page} - ${e.part}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(a)}}class s{static async test(){return document.URL.includes("/www.bilibili.com/bangumi")}async collectData(t){const e=document.querySelector("meta[property='og:url']");if(e===null){Toast.error("获取番剧数据失败: 无法找到 Season ID","批量下载");return""}const i=e.getAttribute("content").match(/play\/ss(\d+)/)[1];if(i===undefined){Toast.error("获取番剧数据失败: 无法解析 Season ID","批量下载");return""}const s=await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${i}`);if(s.code!==0){Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${s.message}`,"批量下载");return""}const a=s.result.main_section.episodes.map(t=>{return{aid:t.aid,cid:t.cid,number:t.title,title:t.long_title}});const r=[];for(const e of a){const i=`https://api.bilibili.com/pgc/player/web/playurl?avid=${e.aid}&cid=${e.cid}&qn=${t}&otype=json`;const s=await Ajax.getJsonWithCredentials(i);const a=s.data||s.result||s;if(a.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=a.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});r.push({fragments:n,title:`${e.number} - ${e.title}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(r)}}const a=[s,i];let r=null;class n{static async test(){for(const t of a){if(await t.test()===true){r=t;return true}}r=null;return false}async collectData(t,e){if(r===null){logError("[批量下载] 未找到合适的解析模块.");return null}const i=new r;const s=await i.collectData(t.quality);e.dismiss();return s}}return{export:{BatchExtractor:n}}}})(); +offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/batch-download.min.js"] = (()=>{return(t,e)=>{class i{static async test(){if(!document.URL.includes("/www.bilibili.com/video/av")){return false}return await SpinQuery.select("#multi_page")!==null}async collectData(t){const e=`https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`;const i=await Ajax.getJson(e);if(i.code!==0){Toast.error(`获取视频选集列表失败, message=${i.message}`,"批量下载");return""}const r=i.data.pages;if(r===undefined){Toast.error(`获取视频选集列表失败, 没有找到选集信息.`,"批量下载");return""}const a=[];for(const e of r){const i=`https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${e.cid}&qn=${t}&otype=json`;const r=await Ajax.getJsonWithCredentials(i);const s=r.data||r.result||r;if(s.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=s.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});a.push({fragments:n,title:`${e.page} - ${e.part}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid,referer:document.URL.replace(window.location.search,"")})}return JSON.stringify(a)}}class r{static async test(){return document.URL.includes("/www.bilibili.com/bangumi")}async collectData(t){const e=document.querySelector("meta[property='og:url']");if(e===null){Toast.error("获取番剧数据失败: 无法找到 Season ID","批量下载");return""}const i=e.getAttribute("content").match(/play\/ss(\d+)/)[1];if(i===undefined){Toast.error("获取番剧数据失败: 无法解析 Season ID","批量下载");return""}const r=await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${i}`);if(r.code!==0){Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${r.message}`,"批量下载");return""}const a=r.result.main_section.episodes.map(t=>{return{aid:t.aid,cid:t.cid,number:t.title,title:t.long_title}});const s=[];for(const e of a){const i=`https://api.bilibili.com/pgc/player/web/playurl?avid=${e.aid}&cid=${e.cid}&qn=${t}&otype=json`;const r=await Ajax.getJsonWithCredentials(i);const a=r.data||r.result||r;if(a.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=a.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});s.push({fragments:n,title:`${e.number} - ${e.title}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(s)}}const a=[r,i];let s=null;class n{static async test(){for(const t of a){if(await t.test()===true){s=t;return true}}s=null;return false}async collectData(t,e){if(s===null){logError("[批量下载] 未找到合适的解析模块.");return null}const i=new s;const r=await i.collectData(t.quality);e.dismiss();return r}}return{export:{BatchExtractor:n}}}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/biliplus-redirect.min.js"] = (()=>{return(i,e)=>{const n=`hd.biliplus.com`;const c=["bilibili.com/video/av","bilibili.com/bangumi/play","bilibili.com/bangumi/media","space.bilibili.com"];return{widget:{condition:()=>{return c.some(i=>document.URL.includes(i))},content:`\n `,success:()=>{const i=document.querySelector("#biliplus-redirect");i.addEventListener("click",()=>{if(location.host==="space.bilibili.com"){location.assign(document.URL.replace("space.bilibili.com/",`${n}/space/`))}else if(document.URL.includes("/bangumi/")){const i=unsafeWindow.aid||document.querySelector(".av-link,.info-sec-av").innerText.replace(/[aAvV]/g,"");location.assign(`https://${n}/video/av${i}/`)}else{location.host=n}})}}}}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/blur-video-control.min.css"] = `.video-control-blur-layer{width:100%;height:100%;position:absolute;-webkit-backdrop-filter:blur(48px);backdrop-filter:blur(48px);z-index:-1;top:0}.bilibili-player-video-control-mask{background:0 0!important}.bilibili-player-video-control-bottom,.bui-slider .bui-track.bui-track-video-progress .bui-bar-wrap{background-color:transparent!important}.bilibili-player-video-control-bottom{position:relative}.bilibili-player-area .bilibili-player-video-control-bottom,.bilibili-player-area .bilibili-player-video-control-wrap{transition:none!important}`; offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/blur-video-control.min.js"] = (()=>{return(o,e)=>{const l=async()=>{const o=await SpinQuery.count(".bui-slider .bui-track.bui-track-video-progress,.bilibili-player-video-control-bottom",2);o.forEach(o=>{if(!o.classList.contains("video-control-blur-container")){o.classList.add("video-control-blur-container");o.insertAdjacentHTML("afterbegin",`
`)}})};e.applyStyle("blurVideoControlStyle");Observer.videoChange(l);return{reload:()=>{document.querySelectorAll(".video-control-blur-layer").forEach(o=>o.style.display="block");e.applyStyle("blurVideoControlStyle")},unload:()=>{document.querySelectorAll(".video-control-blur-layer").forEach(o=>o.style.display="none");e.removeStyle("blurVideoControlStyle")}}}})(); @@ -1044,7 +1044,7 @@ offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/m offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/download-danmaku.min.js"] = (()=>{return(t,e)=>{const{getFriendlyTitle:n}=e.import("title");const{DanmakuInfo:i}=e.import("video-info");const{DanmakuConverter:o}=e.import("danmaku-converter");async function a(t,e){const a=n();const r=new i((unsafeWindow||window).cid);await r.fetchInfo();const l=await(async()=>{if(e===true){let t={title:a};try{await loadLazyPanel(".bilibili-player-video-danmaku-setting");const e=t=>{const e=parseFloat(document.querySelector(t).style.transform.replace(/translateX\(([\d\.]+)/,"$1"));const n={0:0,44:1,94:2,144:3,188:4}[e];return n};t.font=document.querySelector(".bilibili-player-video-danmaku-setting-right-font .bui-select-result").innerText;t.alpha=parseFloat(document.querySelector(".bilibili-player-setting-opacity .bui-bar").style.transform.replace(/scaleX\(([\d\.]+)\)/,"$1"));t.duration=(()=>{const t=[10,8,6,4,2][e(".bilibili-player-setting-speedplus .bui-thumb")];return e=>{switch(e.type){case 4:case 5:return 4;default:return t}}})();t.blockTypes=(()=>{let t=[];const e={".bilibili-player-block-filter-type[ftype=scroll]":[1,2,3],".bilibili-player-block-filter-type[ftype=top]":[5],".bilibili-player-block-filter-type[ftype=bottom]":[4],".bilibili-player-block-filter-type[ftype=color]":["color"]};for(const[n,i]of Object.entries(e)){if(document.querySelector(n).classList.contains("disabled")){t=t.concat(i)}}return t.concat(7,8)})();const n=[1.4,1.2,1,.8,.6][e(".bilibili-player-setting-fontsize .bui-thumb")];t.resolution={x:1920*n,y:1080*n};t.bottomMarginPercent=[.75,.5,.25,0,0][e(".bilibili-player-setting-area .bui-thumb")];if(t.bottomMarginPercent===0&&document.querySelector(".bilibili-player-video-danmaku-setting-left-preventshade input").checked){t.bottomMarginPercent=.15}t.bold=document.querySelector(".bilibili-player-video-danmaku-setting-right-font-bold input").checked}catch(e){t={font:"微软雅黑",alpha:.6,duration:t=>{switch(t.type){case 4:case 5:return 4;default:return 6}},blockTypes:[7,8],resolution:{x:1920,y:1080},bottomMarginPercent:.15,bold:false}}const e=new o(t);const n=e.convertToAssDocument(r.rawXML);return new Blob([n.generateAss()],{type:"text/plain"})}else{return new Blob([r.rawXML],{type:"text/plain"})}})();const c=URL.createObjectURL(l);const s=$("#danmaku-link");const u=s.attr("href");if(u){URL.revokeObjectURL(u)}clearTimeout(t);document.querySelector("#download-danmaku>span").innerHTML="下载弹幕";s.attr("download",`${a}.${e?"ass":"xml"}`).attr("href",c).get(0).click()}return{widget:{content:`\n \n \n 下载弹幕\n \n `,condition:async()=>{let t=await SpinQuery.select(()=>(unsafeWindow||window).cid);return Boolean(t)},success:()=>{const t=document.querySelector("#danmaku-link");$("#download-danmaku").on("click",e=>{if(e.target!==t){const t=setTimeout(()=>document.querySelector("#download-danmaku>span").innerHTML="请稍侯...",200);a(t,e.shiftKey)}})}}}}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/download-video.min.css"] = `.download-video-panel.opened{transform:scaleY(1) translateX(-50%);transition-timing-function:cubic-bezier(.18,.89,.32,1.28)}.download-video-panel{position:absolute;top:calc(100% + 2px);transition:.2s cubic-bezier(.6,-.28,.74,.05);transform-origin:top;transform:scaleY(0) translateX(-50%);background:#fff;box-shadow:0 3px 20px 2px #0004;z-index:100;left:50%}.video-action>div,ol.video-quality li{cursor:pointer;align-items:center;margin:0;padding:8px 16px;transition:.2s ease-out;text-align:center;white-space:nowrap}.video-action>div{padding:12px 24px}.video-action>div:hover,ol.video-quality li:hover{background:rgba(0,0,0,.16)}.video-action,.video-error,.video-progress,.video-quality{display:none}.download-video-panel.action:not(.error) .video-action,.download-video-panel.error .video-error,.download-video-panel.progress:not(.error) .video-progress,.download-video-panel.quality:not(.error) .video-quality{display:block}.download-progress-text{display:flex}.download-progress-background{background-color:#8884;height:4px;width:100%;margin-top:4px}.download-progress-foreground{background-color:var(--theme-color);transform-origin:left;transition:.2s ease-out;transform:scaleX(0);height:100%;width:100%}.download-progress-cancel{margin-top:8px;display:flex;justify-content:center}.download-progress-cancel>span{background-color:#8884;padding:4px 8px;cursor:pointer}.download-progress-cancel>span:hover{opacity:.9}.download-progress-cancel>span:active{opacity:.8}.video-error,.video-progress{padding:12px}.video-progress{min-width:120px}#video-action-batch-data,#video-action-batch-download-data,#video-complete{display:none}.batch #video-action-batch-data,.batch #video-action-batch-download-data{display:block}`; offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/download-video.min.html"] = `
下载视频
`; -offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/download-video.min.js"] = (()=>{return(e,t)=>{const{getFriendlyTitle:s}=t.import("title");const n={entity:null,aid:undefined,cid:undefined};let o=[];let r=null;class a{constructor(){this.menuClasses=["quality","action","progress"];this.currentMenuClass="quality"}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}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=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/x/player/playurl?avid=${n.aid}&cid=${n.cid}&otype=json`}}}class i extends a{async getUrl(e){if(e){return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=&otype=json`}}}class l{constructor(e,t,s){this.quality=e;this.internalName=t;this.displayName=s}async downloadInfo(){const e=new d(this);await e.fetchVideoInfo();return e}static get availableFormats(){return new Promise((e,t)=>{n.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 r=o.accept_quality;const a=o.accept_format.split(",");const i=o.accept_description;const c=[];while(r.length>0){const e=new l(r.pop(),a.pop(),i.pop());c.push(e)}e(c)});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 d{constructor(e,t){this.format=e;this.fragments=t||[];this.progress=null;this.totalSize=null;this.workingXhr=null;this.fragmentSplitFactor=6*5}fetchVideoInfo(){return new Promise((e,t)=>{n.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 r=o.durl;this.fragments=r.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("forEach"in this.workingXhr){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{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}/${i} 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")}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)}]);if(e){GM_setClipboard(t,"text")}else{const e=document.createElement("a");const s=new Blob([t],{type:"text/json"});const n=URL.createObjectURL(s);e.setAttribute("href",n);e.setAttribute("download",`cid${unsafeWindow.cid}.json`);document.body.appendChild(e);e.click();e.remove();URL.revokeObjectURL(n)}}extension(e){return(e||this.fragments[0]).url.indexOf(".flv")!==-1?".flv":".mp4"}makeBlob(e,t=null){return new Blob(Array.isArray(e)?e:[e],{type:this.extension(t)===".flv"?"video/x-flv":"video/mp4"})}cleanUpOldBlobUrl(){const e=document.querySelector("a#video-complete").getAttribute("href");if(e&&!document.querySelector(`.link[href="${e}"]`)){URL.revokeObjectURL(e)}[...document.querySelectorAll(".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[n,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 r=n+".zip";return[o,r]}async download(){const e=[];this.totalSize=this.fragments.map(e=>e.size).reduce((e,t)=>e+t);for(const t of this.fragments){const s=await this.downloadFragment(t);e.push(s)}if(e.length<1){throw new Error("下载失败.")}let t=null;let s=null;if(e.length===1){[t,s]=this.downloadSingle(e)}else{[t,s]=await this.downloadMultiple(e)}this.cleanUpOldBlobUrl();const n=URL.createObjectURL(t);this.progress&&this.progress(0);return{url:n,filename:s}}}async function u(){const e=["/www.bilibili.com/bangumi","/www.bilibili.com/video/av"];if(!e.some(e=>document.URL.includes(e))){return}const{BatchExtractor:s}=await t.importAsync("batchDownload");if(await s.test()!==true){return}const o=new s;document.getElementById("download-video").classList.add("batch");document.getElementById("video-action-batch-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}GM_setClipboard(t,{type:"text/json"});Toast.success("已复制批量数据到剪贴板.","复制批量数据",3e3)});document.getElementById("video-action-batch-download-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}const s=document.createElement("a");const a=new Blob([t],{type:"text/json"});const i=URL.createObjectURL(a);s.setAttribute("href",i);s.setAttribute("download",`export.json`);document.body.appendChild(s);s.click();s.remove();URL.revokeObjectURL(i)})}async function h(){const e=await SpinQuery.select(()=>(unsafeWindow||window).aid);const t=await SpinQuery.select(()=>(unsafeWindow||window).cid);n.aid=e;n.cid=t;if(document.URL.indexOf("bangumi")!==-1){n.entity=new i}else{n.entity=new a}try{o=await l.availableFormats}catch(e){return false}return Boolean(e&&t)}async function p(){r=o[0];const e=async()=>{const e=await h();document.querySelector("#download-video").style.display=e?"flex":"none";if(e===false){return}const t=document.querySelector("ol.video-quality");t.childNodes.forEach(t.removeChild);o.forEach(e=>{const s=document.createElement("li");s.innerHTML=e.displayName;s.addEventListener("click",()=>{r=e;n.entity.nextMenuClass()});t.insertAdjacentElement("afterbegin",s)})};Observer.videoChange(e);const s=()=>r.downloadInfo().catch(e=>{n.entity.addError();$(".video-error").text(e)});async function a(){if(!r){return}n.entity.nextMenuClass();const e=await s();e.progress=(e=>{$(".download-progress-value").text(`${fixed(e*100)}`);$(".download-progress-foreground").css("transform",`scaleX(${e})`)});document.querySelector(".download-progress-cancel>span").onclick=(()=>e.cancelDownload());const t=await e.download().catch(e=>{n.entity.addError();$(".video-error").text(e)});if(!t){return}const o=document.getElementById("video-complete");o.setAttribute("href",t.url);o.setAttribute("download",t.filename);o.click();const a=`下载完成. 再次保存`;Toast.success(a,"下载视频");n.entity.resetMenuClass()}async function i(){if(!r){return}const e=await s();e.copyUrl();Toast.success("已复制链接到剪贴板.","复制链接",3e3);n.entity.resetMenuClass()}document.querySelector("#video-action-download").addEventListener("click",a);document.querySelector("#video-action-copy").addEventListener("click",i);document.querySelector("#video-action-copy-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(true);Toast.success("已复制数据到剪贴板.","复制数据",3e3);n.entity.resetMenuClass()});document.querySelector("#video-action-download-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(false);n.entity.resetMenuClass()});t.applyStyle("downloadVideoStyle");const l=document.querySelector(".download-video-panel");const c=()=>$(".download-video-panel").toggleClass("opened");document.querySelector("#download-video").addEventListener("click",e=>{if(!l.contains(e.target)){c()}});document.querySelector(".video-error").addEventListener("click",()=>{document.querySelector(".video-error").innerHTML="";n.entity.removeError()});await SpinQuery.select(".download-video-panel");n.entity.addMenuClass();u()}return{widget:{content:t.data.downloadVideoHtml.text,condition:h,success:p}}}})(); +offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/download-video.min.js"] = (()=>{return(e,t)=>{const{getFriendlyTitle:s}=t.import("title");const n={entity:null,aid:undefined,cid:undefined};let o=[];let r=null;class a{constructor(){this.menuClasses=["quality","action","progress"];this.currentMenuClass="quality"}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}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=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/x/player/playurl?avid=${n.aid}&cid=${n.cid}&otype=json`}}}class i extends a{async getUrl(e){if(e){return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=&otype=json`}}}class l{constructor(e,t,s){this.quality=e;this.internalName=t;this.displayName=s}async downloadInfo(){const e=new d(this);await e.fetchVideoInfo();return e}static get availableFormats(){return new Promise((e,t)=>{n.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 r=o.accept_quality;const a=o.accept_format.split(",");const i=o.accept_description;const c=[];while(r.length>0){const e=new l(r.pop(),a.pop(),i.pop());c.push(e)}e(c)});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 d{constructor(e,t){this.format=e;this.fragments=t||[];this.progress=null;this.totalSize=null;this.workingXhr=null;this.fragmentSplitFactor=6*5}fetchVideoInfo(){return new Promise((e,t)=>{n.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 r=o.durl;this.fragments=r.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("forEach"in this.workingXhr){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{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}/${i} 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")}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=document.createElement("a");const s=new Blob([t],{type:"text/json"});const n=URL.createObjectURL(s);e.setAttribute("href",n);e.setAttribute("download",`cid${unsafeWindow.cid}.json`);document.body.appendChild(e);e.click();e.remove();URL.revokeObjectURL(n)}}extension(e){return(e||this.fragments[0]).url.indexOf(".flv")!==-1?".flv":".mp4"}makeBlob(e,t=null){return new Blob(Array.isArray(e)?e:[e],{type:this.extension(t)===".flv"?"video/x-flv":"video/mp4"})}cleanUpOldBlobUrl(){const e=document.querySelector("a#video-complete").getAttribute("href");if(e&&!document.querySelector(`.link[href="${e}"]`)){URL.revokeObjectURL(e)}[...document.querySelectorAll(".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[n,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 r=n+".zip";return[o,r]}async download(){const e=[];this.totalSize=this.fragments.map(e=>e.size).reduce((e,t)=>e+t);for(const t of this.fragments){const s=await this.downloadFragment(t);e.push(s)}if(e.length<1){throw new Error("下载失败.")}let t=null;let s=null;if(e.length===1){[t,s]=this.downloadSingle(e)}else{[t,s]=await this.downloadMultiple(e)}this.cleanUpOldBlobUrl();const n=URL.createObjectURL(t);this.progress&&this.progress(0);return{url:n,filename:s}}}async function u(){const e=["/www.bilibili.com/bangumi","/www.bilibili.com/video/av"];if(!e.some(e=>document.URL.includes(e))){return}const{BatchExtractor:s}=await t.importAsync("batchDownload");if(await s.test()!==true){return}const o=new s;document.getElementById("download-video").classList.add("batch");document.getElementById("video-action-batch-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}GM_setClipboard(t,{type:"text/json"});Toast.success("已复制批量数据到剪贴板.","复制批量数据",3e3)});document.getElementById("video-action-batch-download-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}const s=document.createElement("a");const a=new Blob([t],{type:"text/json"});const i=URL.createObjectURL(a);s.setAttribute("href",i);s.setAttribute("download",`export.json`);document.body.appendChild(s);s.click();s.remove();URL.revokeObjectURL(i)})}async function h(){const e=await SpinQuery.select(()=>(unsafeWindow||window).aid);const t=await SpinQuery.select(()=>(unsafeWindow||window).cid);n.aid=e;n.cid=t;if(document.URL.indexOf("bangumi")!==-1){n.entity=new i}else{n.entity=new a}try{o=await l.availableFormats}catch(e){return false}return Boolean(e&&t)}async function p(){r=o[0];const e=async()=>{const e=await h();document.querySelector("#download-video").style.display=e?"flex":"none";if(e===false){return}const t=document.querySelector("ol.video-quality");t.childNodes.forEach(t.removeChild);o.forEach(e=>{const s=document.createElement("li");s.innerHTML=e.displayName;s.addEventListener("click",()=>{r=e;n.entity.nextMenuClass()});t.insertAdjacentElement("afterbegin",s)})};Observer.videoChange(e);const s=()=>r.downloadInfo().catch(e=>{n.entity.addError();$(".video-error").text(e)});async function a(){if(!r){return}n.entity.nextMenuClass();const e=await s();e.progress=(e=>{$(".download-progress-value").text(`${fixed(e*100)}`);$(".download-progress-foreground").css("transform",`scaleX(${e})`)});document.querySelector(".download-progress-cancel>span").onclick=(()=>e.cancelDownload());const t=await e.download().catch(e=>{n.entity.addError();$(".video-error").text(e)});if(!t){return}const o=document.getElementById("video-complete");o.setAttribute("href",t.url);o.setAttribute("download",t.filename);o.click();const a=`下载完成. 再次保存`;Toast.success(a,"下载视频");n.entity.resetMenuClass()}async function i(){if(!r){return}const e=await s();e.copyUrl();Toast.success("已复制链接到剪贴板.","复制链接",3e3);n.entity.resetMenuClass()}document.querySelector("#video-action-download").addEventListener("click",a);document.querySelector("#video-action-copy").addEventListener("click",i);document.querySelector("#video-action-copy-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(true);Toast.success("已复制数据到剪贴板.","复制数据",3e3);n.entity.resetMenuClass()});document.querySelector("#video-action-download-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(false);n.entity.resetMenuClass()});t.applyStyle("downloadVideoStyle");const l=document.querySelector(".download-video-panel");const c=()=>$(".download-video-panel").toggleClass("opened");document.querySelector("#download-video").addEventListener("click",e=>{if(!l.contains(e.target)){c()}});document.querySelector(".video-error").addEventListener("click",()=>{document.querySelector(".video-error").innerHTML="";n.entity.removeError()});await SpinQuery.select(".download-video-panel");n.entity.addMenuClass();u()}return{widget:{content:t.data.downloadVideoHtml.text,condition:h,success:p}}}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/expand-danmaku.min.js"] = (()=>{return(e,a)=>{if(typeof isEmbeddedPlayer!=="undefined"&&isEmbeddedPlayer()){return}Observer.videoChange(async()=>{const e=await SpinQuery.select(".bui-collapse-wrap");if(e&&e.classList.contains("bui-collapse-wrap-folded")){const e=await SpinQuery.select(".bui-collapse-header");e.click()}})}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/expand-description.min.css"] = `.play-up-info .play-up-self,.video-desc .info{height:auto!important}.play-up-info .play-up-self-btn,.video-desc .btn{display:none!important}`; offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/expand-description.min.js"] = (()=>{return(e,p)=>{p.applyStyle("expandDescriptionStyle")}})(); diff --git a/bilibili-evolved.preview-offline.user.js b/bilibili-evolved.preview-offline.user.js index b2782c0c2..9f0e20c2e 100644 --- a/bilibili-evolved.preview-offline.user.js +++ b/bilibili-evolved.preview-offline.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name Bilibili Evolved (Preview Offline) -// @version 348.87 +// @version 349.00 // @description Bilibili Evolved 的预览离线版, 可以抢先体验新功能, 并且所有功能都已内置于脚本中. // @author Grant Howard, Coulomb-G // @copyright 2019, Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G) @@ -1012,7 +1012,7 @@ offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/m offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/about.min.js"] = (()=>{return(t,e)=>{(async()=>{const i=await e.importAsync("aboutHtml");document.body.insertAdjacentHTML("beforeend",i);const n=(t,e)=>t.charCodeAt(0)-e.charCodeAt(0);const o=(t,e)=>n(t.name,e.name);const s=GM_info.script.name.match(/Bilibili Evolved \((.*)\)/);const r=s?s[1]:"Stable";new Vue({el:".bilibili-evolved-about",data:{version:t.currentVersion,clientType:r,branch:null,authors:[{name:"Grant Howard",link:"https://github.com/the1812"},{name:"Coulomb-G",link:"https://github.com/Coulomb-G"}],contributors:[{name:"PleiadeSubaru",link:"https://github.com/Etherrrr"}].sort(o),fetching:true,participants:[],supporters:["*飞","N*v","*博睿","*杨","*泽鹏"].sort(n),websites:[{name:"GitHub",link:"https://github.com/the1812/Bilibili-Evolved/"},{name:"Greasy Fork",link:"https://greasyfork.org/zh-CN/scripts/373563-bilibili-evolved"}],components:[{name:"Vue.js",link:"https://cn.vuejs.org/index.html"},{name:"JSZip",link:"https://stuk.github.io/jszip/"},{name:"jQuery",link:"http://jquery.com/"},{name:"debounce",link:"https://github.com/component/debounce/"},{name:"Slip.js",link:"https://github.com/kornelski/slip"}]},mounted(){document.querySelector(".bilibili-evolved-about").addEventListener("be:about-load",()=>{this.init()},{once:true})},methods:{async init(){this.branch=/Preview|Local/.test(r)?"preview":"master";const t=new Set;let e=[];let i=1;do{e=await Ajax.getJson(`https://api.github.com/repos/the1812/Bilibili-Evolved/issues?state=all&direction=asc&per_page=100&page=${i}`).catch(()=>{e=[{name:"电波无法到达(´・_・`)",link:null}]});i++;for(const i of e){t.add(i.user.login)}}while(e.length>0);this.participants=[...t].map(t=>{return{name:t,link:`https://github.com/${t}`}}).filter(({link:t})=>{return!this.authors.some(e=>e.link===t)&&!this.contributors.some(e=>e.link===t)}).sort(o);this.fetching=false}}})})()}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/auto-continue.min.js"] = (()=>{return(e,i)=>{if(typeof isEmbeddedPlayer!=="undefined"&&isEmbeddedPlayer()){return}function t(i){const t=i.text();if(/第(\d+)话/.test(t)){if(e.allowJumpContinue){i.parent().find(".bilibili-player-video-toast-item-jump").click()}return}const n=/((\d)*:)?(\d)*:(\d)*/g;const r=t.match(n);if(!r){return}const o=r[0].split(":");const l=(()=>{if(o.length===3){const[e,i,t]=o.map(e=>parseInt(e));return e*60*60+i*60+t}else if(o.length===2){const[e,i]=o.map(e=>parseInt(e));return e*60+i}else{logError(`解析历史时间发生错误: historyTime=${JSON.stringify(o)}`);return NaN}})();const s=i.parent();const a=document.querySelector("video");if(l从头开始`);e.appendTo(s).on("click",()=>{a.currentTime=0;s.find(".bilibili-player-video-toast-item-close").get(0).click()});i.html(`已跳转到上次历史记录${r[0]}`)}else{s.find(".bilibili-player-video-toast-item-close").get(0).click()}}function n(){SpinQuery.condition(()=>$(".bilibili-player-video-toast-item-text"),e=>e.text().indexOf("上次看到")!==-1,e=>t(e.filter((e,i)=>i.innerText.indexOf("上次看到")!==-1)))}Observer.videoChange(n)}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/auto-play.min.js"] = (()=>{return(e,d)=>{if(typeof isEmbeddedPlayer!=="undefined"&&isEmbeddedPlayer()){return}SpinQuery.condition(()=>document.querySelector(".bilibili-player-video video"),e=>e&&e.paused===true,e=>e.play())}})(); -offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/batch-download.min.js"] = (()=>{return(t,e)=>{class i{static async test(){if(!document.URL.includes("/www.bilibili.com/video/av")){return false}return await SpinQuery.select("#multi_page")!==null}async collectData(t){const e=`https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`;const i=await Ajax.getJson(e);if(i.code!==0){Toast.error(`获取视频选集列表失败, message=${i.message}`,"批量下载");return""}const s=i.data.pages;if(s===undefined){Toast.error(`获取视频选集列表失败, 没有找到选集信息.`,"批量下载");return""}const a=[];for(const e of s){const i=`https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${e.cid}&qn=${t}&otype=json`;const s=await Ajax.getJsonWithCredentials(i);const r=s.data||s.result||s;if(r.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=r.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});a.push({fragments:n,title:`${e.page} - ${e.part}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(a)}}class s{static async test(){return document.URL.includes("/www.bilibili.com/bangumi")}async collectData(t){const e=document.querySelector("meta[property='og:url']");if(e===null){Toast.error("获取番剧数据失败: 无法找到 Season ID","批量下载");return""}const i=e.getAttribute("content").match(/play\/ss(\d+)/)[1];if(i===undefined){Toast.error("获取番剧数据失败: 无法解析 Season ID","批量下载");return""}const s=await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${i}`);if(s.code!==0){Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${s.message}`,"批量下载");return""}const a=s.result.main_section.episodes.map(t=>{return{aid:t.aid,cid:t.cid,number:t.title,title:t.long_title}});const r=[];for(const e of a){const i=`https://api.bilibili.com/pgc/player/web/playurl?avid=${e.aid}&cid=${e.cid}&qn=${t}&otype=json`;const s=await Ajax.getJsonWithCredentials(i);const a=s.data||s.result||s;if(a.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=a.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});r.push({fragments:n,title:`${e.number} - ${e.title}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(r)}}const a=[s,i];let r=null;class n{static async test(){for(const t of a){if(await t.test()===true){r=t;return true}}r=null;return false}async collectData(t,e){if(r===null){logError("[批量下载] 未找到合适的解析模块.");return null}const i=new r;const s=await i.collectData(t.quality);e.dismiss();return s}}return{export:{BatchExtractor:n}}}})(); +offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/batch-download.min.js"] = (()=>{return(t,e)=>{class i{static async test(){if(!document.URL.includes("/www.bilibili.com/video/av")){return false}return await SpinQuery.select("#multi_page")!==null}async collectData(t){const e=`https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`;const i=await Ajax.getJson(e);if(i.code!==0){Toast.error(`获取视频选集列表失败, message=${i.message}`,"批量下载");return""}const r=i.data.pages;if(r===undefined){Toast.error(`获取视频选集列表失败, 没有找到选集信息.`,"批量下载");return""}const a=[];for(const e of r){const i=`https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${e.cid}&qn=${t}&otype=json`;const r=await Ajax.getJsonWithCredentials(i);const s=r.data||r.result||r;if(s.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=s.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});a.push({fragments:n,title:`${e.page} - ${e.part}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid,referer:document.URL.replace(window.location.search,"")})}return JSON.stringify(a)}}class r{static async test(){return document.URL.includes("/www.bilibili.com/bangumi")}async collectData(t){const e=document.querySelector("meta[property='og:url']");if(e===null){Toast.error("获取番剧数据失败: 无法找到 Season ID","批量下载");return""}const i=e.getAttribute("content").match(/play\/ss(\d+)/)[1];if(i===undefined){Toast.error("获取番剧数据失败: 无法解析 Season ID","批量下载");return""}const r=await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${i}`);if(r.code!==0){Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${r.message}`,"批量下载");return""}const a=r.result.main_section.episodes.map(t=>{return{aid:t.aid,cid:t.cid,number:t.title,title:t.long_title}});const s=[];for(const e of a){const i=`https://api.bilibili.com/pgc/player/web/playurl?avid=${e.aid}&cid=${e.cid}&qn=${t}&otype=json`;const r=await Ajax.getJsonWithCredentials(i);const a=r.data||r.result||r;if(a.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=a.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});s.push({fragments:n,title:`${e.number} - ${e.title}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(s)}}const a=[r,i];let s=null;class n{static async test(){for(const t of a){if(await t.test()===true){s=t;return true}}s=null;return false}async collectData(t,e){if(s===null){logError("[批量下载] 未找到合适的解析模块.");return null}const i=new s;const r=await i.collectData(t.quality);e.dismiss();return r}}return{export:{BatchExtractor:n}}}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/biliplus-redirect.min.js"] = (()=>{return(i,e)=>{const n=`hd.biliplus.com`;const c=["bilibili.com/video/av","bilibili.com/bangumi/play","bilibili.com/bangumi/media","space.bilibili.com"];return{widget:{condition:()=>{return c.some(i=>document.URL.includes(i))},content:`\n `,success:()=>{const i=document.querySelector("#biliplus-redirect");i.addEventListener("click",()=>{if(location.host==="space.bilibili.com"){location.assign(document.URL.replace("space.bilibili.com/",`${n}/space/`))}else if(document.URL.includes("/bangumi/")){const i=unsafeWindow.aid||document.querySelector(".av-link,.info-sec-av").innerText.replace(/[aAvV]/g,"");location.assign(`https://${n}/video/av${i}/`)}else{location.host=n}})}}}}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/blur-video-control.min.css"] = `.video-control-blur-layer{width:100%;height:100%;position:absolute;-webkit-backdrop-filter:blur(48px);backdrop-filter:blur(48px);z-index:-1;top:0}.bilibili-player-video-control-mask{background:0 0!important}.bilibili-player-video-control-bottom,.bui-slider .bui-track.bui-track-video-progress .bui-bar-wrap{background-color:transparent!important}.bilibili-player-video-control-bottom{position:relative}.bilibili-player-area .bilibili-player-video-control-bottom,.bilibili-player-area .bilibili-player-video-control-wrap{transition:none!important}`; offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/blur-video-control.min.js"] = (()=>{return(o,e)=>{const l=async()=>{const o=await SpinQuery.count(".bui-slider .bui-track.bui-track-video-progress,.bilibili-player-video-control-bottom",2);o.forEach(o=>{if(!o.classList.contains("video-control-blur-container")){o.classList.add("video-control-blur-container");o.insertAdjacentHTML("afterbegin",`
`)}})};e.applyStyle("blurVideoControlStyle");Observer.videoChange(l);return{reload:()=>{document.querySelectorAll(".video-control-blur-layer").forEach(o=>o.style.display="block");e.applyStyle("blurVideoControlStyle")},unload:()=>{document.querySelectorAll(".video-control-blur-layer").forEach(o=>o.style.display="none");e.removeStyle("blurVideoControlStyle")}}}})(); @@ -1044,7 +1044,7 @@ offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/m offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/download-danmaku.min.js"] = (()=>{return(t,e)=>{const{getFriendlyTitle:n}=e.import("title");const{DanmakuInfo:i}=e.import("video-info");const{DanmakuConverter:o}=e.import("danmaku-converter");async function a(t,e){const a=n();const r=new i((unsafeWindow||window).cid);await r.fetchInfo();const l=await(async()=>{if(e===true){let t={title:a};try{await loadLazyPanel(".bilibili-player-video-danmaku-setting");const e=t=>{const e=parseFloat(document.querySelector(t).style.transform.replace(/translateX\(([\d\.]+)/,"$1"));const n={0:0,44:1,94:2,144:3,188:4}[e];return n};t.font=document.querySelector(".bilibili-player-video-danmaku-setting-right-font .bui-select-result").innerText;t.alpha=parseFloat(document.querySelector(".bilibili-player-setting-opacity .bui-bar").style.transform.replace(/scaleX\(([\d\.]+)\)/,"$1"));t.duration=(()=>{const t=[10,8,6,4,2][e(".bilibili-player-setting-speedplus .bui-thumb")];return e=>{switch(e.type){case 4:case 5:return 4;default:return t}}})();t.blockTypes=(()=>{let t=[];const e={".bilibili-player-block-filter-type[ftype=scroll]":[1,2,3],".bilibili-player-block-filter-type[ftype=top]":[5],".bilibili-player-block-filter-type[ftype=bottom]":[4],".bilibili-player-block-filter-type[ftype=color]":["color"]};for(const[n,i]of Object.entries(e)){if(document.querySelector(n).classList.contains("disabled")){t=t.concat(i)}}return t.concat(7,8)})();const n=[1.4,1.2,1,.8,.6][e(".bilibili-player-setting-fontsize .bui-thumb")];t.resolution={x:1920*n,y:1080*n};t.bottomMarginPercent=[.75,.5,.25,0,0][e(".bilibili-player-setting-area .bui-thumb")];if(t.bottomMarginPercent===0&&document.querySelector(".bilibili-player-video-danmaku-setting-left-preventshade input").checked){t.bottomMarginPercent=.15}t.bold=document.querySelector(".bilibili-player-video-danmaku-setting-right-font-bold input").checked}catch(e){t={font:"微软雅黑",alpha:.6,duration:t=>{switch(t.type){case 4:case 5:return 4;default:return 6}},blockTypes:[7,8],resolution:{x:1920,y:1080},bottomMarginPercent:.15,bold:false}}const e=new o(t);const n=e.convertToAssDocument(r.rawXML);return new Blob([n.generateAss()],{type:"text/plain"})}else{return new Blob([r.rawXML],{type:"text/plain"})}})();const c=URL.createObjectURL(l);const s=$("#danmaku-link");const u=s.attr("href");if(u){URL.revokeObjectURL(u)}clearTimeout(t);document.querySelector("#download-danmaku>span").innerHTML="下载弹幕";s.attr("download",`${a}.${e?"ass":"xml"}`).attr("href",c).get(0).click()}return{widget:{content:`\n \n \n 下载弹幕\n \n `,condition:async()=>{let t=await SpinQuery.select(()=>(unsafeWindow||window).cid);return Boolean(t)},success:()=>{const t=document.querySelector("#danmaku-link");$("#download-danmaku").on("click",e=>{if(e.target!==t){const t=setTimeout(()=>document.querySelector("#download-danmaku>span").innerHTML="请稍侯...",200);a(t,e.shiftKey)}})}}}}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/download-video.min.css"] = `.download-video-panel.opened{transform:scaleY(1) translateX(-50%);transition-timing-function:cubic-bezier(.18,.89,.32,1.28)}.download-video-panel{position:absolute;top:calc(100% + 2px);transition:.2s cubic-bezier(.6,-.28,.74,.05);transform-origin:top;transform:scaleY(0) translateX(-50%);background:#fff;box-shadow:0 3px 20px 2px #0004;z-index:100;left:50%}.video-action>div,ol.video-quality li{cursor:pointer;align-items:center;margin:0;padding:8px 16px;transition:.2s ease-out;text-align:center;white-space:nowrap}.video-action>div{padding:12px 24px}.video-action>div:hover,ol.video-quality li:hover{background:rgba(0,0,0,.16)}.video-action,.video-error,.video-progress,.video-quality{display:none}.download-video-panel.action:not(.error) .video-action,.download-video-panel.error .video-error,.download-video-panel.progress:not(.error) .video-progress,.download-video-panel.quality:not(.error) .video-quality{display:block}.download-progress-text{display:flex}.download-progress-background{background-color:#8884;height:4px;width:100%;margin-top:4px}.download-progress-foreground{background-color:var(--theme-color);transform-origin:left;transition:.2s ease-out;transform:scaleX(0);height:100%;width:100%}.download-progress-cancel{margin-top:8px;display:flex;justify-content:center}.download-progress-cancel>span{background-color:#8884;padding:4px 8px;cursor:pointer}.download-progress-cancel>span:hover{opacity:.9}.download-progress-cancel>span:active{opacity:.8}.video-error,.video-progress{padding:12px}.video-progress{min-width:120px}#video-action-batch-data,#video-action-batch-download-data,#video-complete{display:none}.batch #video-action-batch-data,.batch #video-action-batch-download-data{display:block}`; offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/download-video.min.html"] = `
下载视频
`; -offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/download-video.min.js"] = (()=>{return(e,t)=>{const{getFriendlyTitle:s}=t.import("title");const n={entity:null,aid:undefined,cid:undefined};let o=[];let r=null;class a{constructor(){this.menuClasses=["quality","action","progress"];this.currentMenuClass="quality"}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}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=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/x/player/playurl?avid=${n.aid}&cid=${n.cid}&otype=json`}}}class i extends a{async getUrl(e){if(e){return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=&otype=json`}}}class l{constructor(e,t,s){this.quality=e;this.internalName=t;this.displayName=s}async downloadInfo(){const e=new d(this);await e.fetchVideoInfo();return e}static get availableFormats(){return new Promise((e,t)=>{n.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 r=o.accept_quality;const a=o.accept_format.split(",");const i=o.accept_description;const c=[];while(r.length>0){const e=new l(r.pop(),a.pop(),i.pop());c.push(e)}e(c)});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 d{constructor(e,t){this.format=e;this.fragments=t||[];this.progress=null;this.totalSize=null;this.workingXhr=null;this.fragmentSplitFactor=6*5}fetchVideoInfo(){return new Promise((e,t)=>{n.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 r=o.durl;this.fragments=r.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("forEach"in this.workingXhr){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{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}/${i} 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")}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)}]);if(e){GM_setClipboard(t,"text")}else{const e=document.createElement("a");const s=new Blob([t],{type:"text/json"});const n=URL.createObjectURL(s);e.setAttribute("href",n);e.setAttribute("download",`cid${unsafeWindow.cid}.json`);document.body.appendChild(e);e.click();e.remove();URL.revokeObjectURL(n)}}extension(e){return(e||this.fragments[0]).url.indexOf(".flv")!==-1?".flv":".mp4"}makeBlob(e,t=null){return new Blob(Array.isArray(e)?e:[e],{type:this.extension(t)===".flv"?"video/x-flv":"video/mp4"})}cleanUpOldBlobUrl(){const e=document.querySelector("a#video-complete").getAttribute("href");if(e&&!document.querySelector(`.link[href="${e}"]`)){URL.revokeObjectURL(e)}[...document.querySelectorAll(".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[n,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 r=n+".zip";return[o,r]}async download(){const e=[];this.totalSize=this.fragments.map(e=>e.size).reduce((e,t)=>e+t);for(const t of this.fragments){const s=await this.downloadFragment(t);e.push(s)}if(e.length<1){throw new Error("下载失败.")}let t=null;let s=null;if(e.length===1){[t,s]=this.downloadSingle(e)}else{[t,s]=await this.downloadMultiple(e)}this.cleanUpOldBlobUrl();const n=URL.createObjectURL(t);this.progress&&this.progress(0);return{url:n,filename:s}}}async function u(){const e=["/www.bilibili.com/bangumi","/www.bilibili.com/video/av"];if(!e.some(e=>document.URL.includes(e))){return}const{BatchExtractor:s}=await t.importAsync("batchDownload");if(await s.test()!==true){return}const o=new s;document.getElementById("download-video").classList.add("batch");document.getElementById("video-action-batch-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}GM_setClipboard(t,{type:"text/json"});Toast.success("已复制批量数据到剪贴板.","复制批量数据",3e3)});document.getElementById("video-action-batch-download-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}const s=document.createElement("a");const a=new Blob([t],{type:"text/json"});const i=URL.createObjectURL(a);s.setAttribute("href",i);s.setAttribute("download",`export.json`);document.body.appendChild(s);s.click();s.remove();URL.revokeObjectURL(i)})}async function h(){const e=await SpinQuery.select(()=>(unsafeWindow||window).aid);const t=await SpinQuery.select(()=>(unsafeWindow||window).cid);n.aid=e;n.cid=t;if(document.URL.indexOf("bangumi")!==-1){n.entity=new i}else{n.entity=new a}try{o=await l.availableFormats}catch(e){return false}return Boolean(e&&t)}async function p(){r=o[0];const e=async()=>{const e=await h();document.querySelector("#download-video").style.display=e?"flex":"none";if(e===false){return}const t=document.querySelector("ol.video-quality");t.childNodes.forEach(t.removeChild);o.forEach(e=>{const s=document.createElement("li");s.innerHTML=e.displayName;s.addEventListener("click",()=>{r=e;n.entity.nextMenuClass()});t.insertAdjacentElement("afterbegin",s)})};Observer.videoChange(e);const s=()=>r.downloadInfo().catch(e=>{n.entity.addError();$(".video-error").text(e)});async function a(){if(!r){return}n.entity.nextMenuClass();const e=await s();e.progress=(e=>{$(".download-progress-value").text(`${fixed(e*100)}`);$(".download-progress-foreground").css("transform",`scaleX(${e})`)});document.querySelector(".download-progress-cancel>span").onclick=(()=>e.cancelDownload());const t=await e.download().catch(e=>{n.entity.addError();$(".video-error").text(e)});if(!t){return}const o=document.getElementById("video-complete");o.setAttribute("href",t.url);o.setAttribute("download",t.filename);o.click();const a=`下载完成. 再次保存`;Toast.success(a,"下载视频");n.entity.resetMenuClass()}async function i(){if(!r){return}const e=await s();e.copyUrl();Toast.success("已复制链接到剪贴板.","复制链接",3e3);n.entity.resetMenuClass()}document.querySelector("#video-action-download").addEventListener("click",a);document.querySelector("#video-action-copy").addEventListener("click",i);document.querySelector("#video-action-copy-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(true);Toast.success("已复制数据到剪贴板.","复制数据",3e3);n.entity.resetMenuClass()});document.querySelector("#video-action-download-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(false);n.entity.resetMenuClass()});t.applyStyle("downloadVideoStyle");const l=document.querySelector(".download-video-panel");const c=()=>$(".download-video-panel").toggleClass("opened");document.querySelector("#download-video").addEventListener("click",e=>{if(!l.contains(e.target)){c()}});document.querySelector(".video-error").addEventListener("click",()=>{document.querySelector(".video-error").innerHTML="";n.entity.removeError()});await SpinQuery.select(".download-video-panel");n.entity.addMenuClass();u()}return{widget:{content:t.data.downloadVideoHtml.text,condition:h,success:p}}}})(); +offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/download-video.min.js"] = (()=>{return(e,t)=>{const{getFriendlyTitle:s}=t.import("title");const n={entity:null,aid:undefined,cid:undefined};let o=[];let r=null;class a{constructor(){this.menuClasses=["quality","action","progress"];this.currentMenuClass="quality"}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}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=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/x/player/playurl?avid=${n.aid}&cid=${n.cid}&otype=json`}}}class i extends a{async getUrl(e){if(e){return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=&otype=json`}}}class l{constructor(e,t,s){this.quality=e;this.internalName=t;this.displayName=s}async downloadInfo(){const e=new d(this);await e.fetchVideoInfo();return e}static get availableFormats(){return new Promise((e,t)=>{n.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 r=o.accept_quality;const a=o.accept_format.split(",");const i=o.accept_description;const c=[];while(r.length>0){const e=new l(r.pop(),a.pop(),i.pop());c.push(e)}e(c)});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 d{constructor(e,t){this.format=e;this.fragments=t||[];this.progress=null;this.totalSize=null;this.workingXhr=null;this.fragmentSplitFactor=6*5}fetchVideoInfo(){return new Promise((e,t)=>{n.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 r=o.durl;this.fragments=r.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("forEach"in this.workingXhr){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{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}/${i} 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")}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=document.createElement("a");const s=new Blob([t],{type:"text/json"});const n=URL.createObjectURL(s);e.setAttribute("href",n);e.setAttribute("download",`cid${unsafeWindow.cid}.json`);document.body.appendChild(e);e.click();e.remove();URL.revokeObjectURL(n)}}extension(e){return(e||this.fragments[0]).url.indexOf(".flv")!==-1?".flv":".mp4"}makeBlob(e,t=null){return new Blob(Array.isArray(e)?e:[e],{type:this.extension(t)===".flv"?"video/x-flv":"video/mp4"})}cleanUpOldBlobUrl(){const e=document.querySelector("a#video-complete").getAttribute("href");if(e&&!document.querySelector(`.link[href="${e}"]`)){URL.revokeObjectURL(e)}[...document.querySelectorAll(".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[n,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 r=n+".zip";return[o,r]}async download(){const e=[];this.totalSize=this.fragments.map(e=>e.size).reduce((e,t)=>e+t);for(const t of this.fragments){const s=await this.downloadFragment(t);e.push(s)}if(e.length<1){throw new Error("下载失败.")}let t=null;let s=null;if(e.length===1){[t,s]=this.downloadSingle(e)}else{[t,s]=await this.downloadMultiple(e)}this.cleanUpOldBlobUrl();const n=URL.createObjectURL(t);this.progress&&this.progress(0);return{url:n,filename:s}}}async function u(){const e=["/www.bilibili.com/bangumi","/www.bilibili.com/video/av"];if(!e.some(e=>document.URL.includes(e))){return}const{BatchExtractor:s}=await t.importAsync("batchDownload");if(await s.test()!==true){return}const o=new s;document.getElementById("download-video").classList.add("batch");document.getElementById("video-action-batch-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}GM_setClipboard(t,{type:"text/json"});Toast.success("已复制批量数据到剪贴板.","复制批量数据",3e3)});document.getElementById("video-action-batch-download-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}const s=document.createElement("a");const a=new Blob([t],{type:"text/json"});const i=URL.createObjectURL(a);s.setAttribute("href",i);s.setAttribute("download",`export.json`);document.body.appendChild(s);s.click();s.remove();URL.revokeObjectURL(i)})}async function h(){const e=await SpinQuery.select(()=>(unsafeWindow||window).aid);const t=await SpinQuery.select(()=>(unsafeWindow||window).cid);n.aid=e;n.cid=t;if(document.URL.indexOf("bangumi")!==-1){n.entity=new i}else{n.entity=new a}try{o=await l.availableFormats}catch(e){return false}return Boolean(e&&t)}async function p(){r=o[0];const e=async()=>{const e=await h();document.querySelector("#download-video").style.display=e?"flex":"none";if(e===false){return}const t=document.querySelector("ol.video-quality");t.childNodes.forEach(t.removeChild);o.forEach(e=>{const s=document.createElement("li");s.innerHTML=e.displayName;s.addEventListener("click",()=>{r=e;n.entity.nextMenuClass()});t.insertAdjacentElement("afterbegin",s)})};Observer.videoChange(e);const s=()=>r.downloadInfo().catch(e=>{n.entity.addError();$(".video-error").text(e)});async function a(){if(!r){return}n.entity.nextMenuClass();const e=await s();e.progress=(e=>{$(".download-progress-value").text(`${fixed(e*100)}`);$(".download-progress-foreground").css("transform",`scaleX(${e})`)});document.querySelector(".download-progress-cancel>span").onclick=(()=>e.cancelDownload());const t=await e.download().catch(e=>{n.entity.addError();$(".video-error").text(e)});if(!t){return}const o=document.getElementById("video-complete");o.setAttribute("href",t.url);o.setAttribute("download",t.filename);o.click();const a=`下载完成. 再次保存`;Toast.success(a,"下载视频");n.entity.resetMenuClass()}async function i(){if(!r){return}const e=await s();e.copyUrl();Toast.success("已复制链接到剪贴板.","复制链接",3e3);n.entity.resetMenuClass()}document.querySelector("#video-action-download").addEventListener("click",a);document.querySelector("#video-action-copy").addEventListener("click",i);document.querySelector("#video-action-copy-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(true);Toast.success("已复制数据到剪贴板.","复制数据",3e3);n.entity.resetMenuClass()});document.querySelector("#video-action-download-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(false);n.entity.resetMenuClass()});t.applyStyle("downloadVideoStyle");const l=document.querySelector(".download-video-panel");const c=()=>$(".download-video-panel").toggleClass("opened");document.querySelector("#download-video").addEventListener("click",e=>{if(!l.contains(e.target)){c()}});document.querySelector(".video-error").addEventListener("click",()=>{document.querySelector(".video-error").innerHTML="";n.entity.removeError()});await SpinQuery.select(".download-video-panel");n.entity.addMenuClass();u()}return{widget:{content:t.data.downloadVideoHtml.text,condition:h,success:p}}}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/expand-danmaku.min.js"] = (()=>{return(e,a)=>{if(typeof isEmbeddedPlayer!=="undefined"&&isEmbeddedPlayer()){return}Observer.videoChange(async()=>{const e=await SpinQuery.select(".bui-collapse-wrap");if(e&&e.classList.contains("bui-collapse-wrap-folded")){const e=await SpinQuery.select(".bui-collapse-header");e.click()}})}})(); offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/expand-description.min.css"] = `.play-up-info .play-up-self,.video-desc .info{height:auto!important}.play-up-info .play-up-self-btn,.video-desc .btn{display:none!important}`; offlineData["https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/min/expand-description.min.js"] = (()=>{return(e,p)=>{p.applyStyle("expandDescriptionStyle")}})(); diff --git a/builder/dotnet/ResourceBuilder.cs b/builder/dotnet/ResourceBuilder.cs index c777807d8..0d271a70e 100644 --- a/builder/dotnet/ResourceBuilder.cs +++ b/builder/dotnet/ResourceBuilder.cs @@ -154,7 +154,7 @@ namespace BilibiliEvolved.Build } return source; }; - input = RegexReplacer.Replace(input, @"import (.*) from ([^;]*)", match => + input = RegexReplacer.Replace(input, @"import (.*) from ([^\r\n;]*)", match => { var imported = match.Groups[1].Value.Replace(" as ", ":"); var source = convertToRuntimeSource(match.Groups[2].Value); @@ -173,6 +173,7 @@ namespace BilibiliEvolved.Build }; })();"; } + // Console.WriteLine(input); return new UglifyJs().Run(input); } } diff --git a/builder/dotnet/publish/build.dll b/builder/dotnet/publish/build.dll index a2c3c35ae..0cb5ee727 100644 Binary files a/builder/dotnet/publish/build.dll and b/builder/dotnet/publish/build.dll differ diff --git a/extras/video-link-downloader/package.json b/extras/video-link-downloader/package.json index eaff18e31..5812b0fa8 100644 --- a/extras/video-link-downloader/package.json +++ b/extras/video-link-downloader/package.json @@ -1,6 +1,6 @@ { "name": "bilibili-evolved-video-link-downloader", - "version": "1.8.1", + "version": "1.8.2", "description": "Bilibili Evolved 视频链接下载器", "main": "video-link-downloader.js", "bin": { diff --git a/extras/video-link-downloader/video-link-downloader.js b/extras/video-link-downloader/video-link-downloader.js index 316d89b16..2565f017d 100644 --- a/extras/video-link-downloader/video-link-downloader.js +++ b/extras/video-link-downloader/video-link-downloader.js @@ -64,8 +64,8 @@ class Downloader { headers: { Range: range, Origin: "https://www.bilibili.com", - Referer: "https://www.bilibili.com", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36", + Referer: this.inputData.referer || "https://www.bilibili.com", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0", }, }).on("complete", response => { if (response.statusCode.toString()[0] === "2") { @@ -78,15 +78,15 @@ class Downloader { this.progressMap.set(req, this.progressMap.get(req) + data.length); this.updateProgress(); }).on("error", error => { - // stream.close(); // fs.unlinkSync(partFilename); - // this.progressMap.delete(req); - // this.progressMap.set(makeRequest(), 0); - // this.updateProgress(); + this.progressMap.delete(req); + this.progressMap.set(makeRequest(), 0); + this.updateProgress(); reject(`\n片段下载失败: ${error}`); }); - console.log(`created stream ${partFilename}`); - req.pipe(fs.createWriteStream(partFilename)); + req.pipe(fs.createWriteStream(partFilename, { + autoClose: true + })); return req; }; this.progressMap.set(makeRequest(), 0); diff --git a/extras/video-link-downloader/video-link-downloader.ts b/extras/video-link-downloader/video-link-downloader.ts index 5b6c12304..6dafe6d97 100644 --- a/extras/video-link-downloader/video-link-downloader.ts +++ b/extras/video-link-downloader/video-link-downloader.ts @@ -6,31 +6,29 @@ import fs = require("fs"); import ProgressBar = require("progress"); import "colors"; -interface Fragment -{ - length: number; - size: number; - url: string; - backupUrls: string[]; +interface Fragment { + length: number; + size: number; + url: string; + backupUrls: string[]; } -interface InputData -{ - fragments: Fragment[]; - title: string; - totalSize: number; +interface InputData { + fragments: Fragment[]; + title: string; + totalSize: number; + referer: string; } -interface Settings -{ - parts: number; - info: string; - output: string; +interface Settings { + parts: number; + info: string; + output: string; } const optionDefinitions = [ - { name: 'danmaku', alias: 'd', defaultValue: false, type: Boolean }, - { name: 'info', alias: 'i', defaultOption: true, type: String, defaultValue: undefined }, - { name: 'parts', alias: 'p', type: Number, defaultValue: 12 }, - { name: 'output', alias: 'o', type: String, defaultValue: '.' }, + { name: 'danmaku', alias: 'd', defaultValue: false, type: Boolean }, + { name: 'info', alias: 'i', defaultOption: true, type: String, defaultValue: undefined }, + { name: 'parts', alias: 'p', type: Number, defaultValue: 12 }, + { name: 'output', alias: 'o', type: String, defaultValue: '.' }, ]; const commandLineOptions = commandLineArgs(optionDefinitions) as Settings; let options = commandLineOptions; @@ -41,277 +39,224 @@ options.parts = Math.round(options.parts); // options = Object.assign(jsonOptions, options); // } -if (options.parts < 1) -{ - console.error("分段数不能小于1".red); - process.exit(); +if (options.parts < 1) { + console.error("分段数不能小于1".red); + process.exit(); } -class Downloader -{ - static workingDownloader: Downloader | null = null; - private progressMap = new Map(); - private progressBar = new ProgressBar(":percent [:bar]", { - total: this.inputData.totalSize, - width: 20, - incomplete: ' ', +class Downloader { + static workingDownloader: Downloader | null = null; + private progressMap = new Map(); + private progressBar = new ProgressBar(":percent [:bar]", { + total: this.inputData.totalSize, + width: 20, + incomplete: ' ', + }); + private extension: string; + private title: string | string[]; + + constructor( + private inputData: InputData, + ) { } + private getExtension(fragment: Fragment) { + this.extension = fragment.url.includes(".flv") ? ".flv" : ".mp4"; + } + private updateProgress() { + const progress = this.progressMap ? + [...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.inputData.totalSize : 0; + this.progressBar.update(progress); + } + cancelDownload() { + [...this.progressMap.keys()].forEach(it => { + if (typeof it !== "string") { + it.abort(); + } }); - private extension: string; - private title: string | string[]; - - constructor( - private inputData: InputData, - ) - { } - private getExtension(fragment: Fragment) - { - this.extension = fragment.url.includes(".flv") ? ".flv" : ".mp4"; - } - private updateProgress() - { - const progress = this.progressMap ? - [...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.inputData.totalSize : 0; - this.progressBar.update(progress); - } - cancelDownload() - { - [...this.progressMap.keys()].forEach(it => - { - if (typeof it !== "string") - { - it.abort(); - } + this.progressBar.terminate(); + const files = fs.readdirSync("."); + const parts = files.filter(it => it.includes(this.inputData.title)); + parts.forEach(file => fs.unlinkSync(file)); + console.log("已取消下载".blue); + } + private downloadFragmentPart(url: string, range: string, partFilename: string) { + return new Promise((resolve, reject) => { + const makeRequest = () => { + const req = request({ + url: url, + method: "GET", + headers: { + Range: range, + Origin: "https://www.bilibili.com", + Referer: this.inputData.referer || "https://www.bilibili.com", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0", + }, + }).on("complete", response => { + if (response.statusCode.toString()[0] === "2") { + resolve(response); + } + else { + reject(`请求失败: ${response.statusCode}`); + } + }).on("data", data => { + this.progressMap.set(req, this.progressMap.get(req)! + data.length); + this.updateProgress(); + }).on("error", error => { + // fs.unlinkSync(partFilename); + this.progressMap.delete(req); + this.progressMap.set(makeRequest(), 0); + this.updateProgress(); + reject(`\n片段下载失败: ${error}`); }); - this.progressBar.terminate(); - const files = fs.readdirSync("."); - const parts = files.filter(it => it.includes(this.inputData.title)); - parts.forEach(file => fs.unlinkSync(file)); - console.log("已取消下载".blue); + req.pipe(fs.createWriteStream(partFilename, { + autoClose: true + })) + return req + }; + this.progressMap.set(makeRequest(), 0); + }); + } + private async downloadFragment(fragment: Fragment, index: number = -1) { + const partialLength = Math.round(fragment.size / options.parts); + const title = (index === -1 ? this.inputData.title : this.inputData.title + " - " + index.toString()); + if (fs.existsSync(title + this.extension)) { + this.progressBar.terminate(); + console.log(`跳过了已存在的文件 ${title + this.extension}`); + return title; } - private downloadFragmentPart(url: string, range: string, partFilename: string) - { - return new Promise((resolve, reject) => - { - const makeRequest = () => - { - const req = request({ - url: url, - method: "GET", - headers: { - Range: range, - Origin: "https://www.bilibili.com", - Referer: "https://www.bilibili.com", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36", - }, - // maxAttempts: 8, - // retryDelay: 1000, - }).on("complete", response => - { - if (response.statusCode.toString()[0] === "2") - { - resolve(response); - } - else - { - reject(`请求失败: ${response.statusCode}`); - } - }).on("data", data => - { - this.progressMap.set(req, this.progressMap.get(req)! + data.length); - this.updateProgress(); - }).on("error", error => - { - // stream.close(); - // fs.unlinkSync(partFilename); - // this.progressMap.delete(req); - // this.progressMap.set(makeRequest(), 0); - // this.updateProgress(); - reject(`\n片段下载失败: ${error}`); - }); - req.pipe(fs.createWriteStream(partFilename)) - return req - }; - this.progressMap.set(makeRequest(), 0); - }); + let startByte = 0; + let part = 0; + const promises = []; + while (startByte < fragment.size) { + const partFilename = `${title}.part${part}`; + // if (fs.existsSync(partFilename)) + // { + // this.progressMap.set(partFilename, partialLength); + // this.updateProgress(); + // } + // else + // { + const endByte = Math.min(fragment.size - 1, Math.round(startByte + partialLength)); + const range = `bytes=${startByte}-${endByte}`; + promises.push(this.downloadFragmentPart(fragment.url, range, partFilename)); + // } + startByte = Math.round(startByte + partialLength) + 1; + part++; } - private async downloadFragment(fragment: Fragment, index: number = -1) - { - const partialLength = Math.round(fragment.size / options.parts); - const title = (index === -1 ? this.inputData.title : this.inputData.title + " - " + index.toString()); - if (fs.existsSync(title + this.extension)) - { - this.progressBar.terminate(); - console.log(`跳过了已存在的文件 ${title + this.extension}`); - return title; - } - let startByte = 0; - let part = 0; - const promises = []; - while (startByte < fragment.size) - { - const partFilename = `${title}.part${part}`; - // if (fs.existsSync(partFilename)) - // { - // this.progressMap.set(partFilename, partialLength); - // this.updateProgress(); - // } - // else - // { - const endByte = Math.min(fragment.size - 1, Math.round(startByte + partialLength)); - const range = `bytes=${startByte}-${endByte}`; - promises.push(this.downloadFragmentPart(fragment.url, range, partFilename)); - // } - startByte = Math.round(startByte + partialLength) + 1; - part++; - } - await Promise.all(promises); - return title; + await Promise.all(promises); + return title; + } + async download() { + console.log(`正在下载: ${this.inputData.title}`.green); + Downloader.workingDownloader = this; + this.progressBar.render(); + const [fragment] = this.inputData.fragments; + this.getExtension(fragment); + if (this.inputData.fragments.length === 1) { + this.title = await this.downloadFragment(fragment); } - async download() - { - console.log(`正在下载: ${this.inputData.title}`.green); - Downloader.workingDownloader = this; - this.progressBar.render(); - const [fragment] = this.inputData.fragments; - this.getExtension(fragment); - if (this.inputData.fragments.length === 1) - { - this.title = await this.downloadFragment(fragment); - } - else - { - this.title = await Promise.all(this.inputData.fragments.map((f, i) => this.downloadFragment(f, i))); - } + else { + this.title = await Promise.all(this.inputData.fragments.map((f, i) => this.downloadFragment(f, i))); } - private async mergeFragment(title: string, index = -1) - { - const dest = title + this.extension; - if (!this.progressBar.complete) - { - this.progressBar.update(1); - this.progressBar.terminate(); - } - if (fs.existsSync(dest)) - { - return dest; - } - if (index !== -1) - { - console.log(`正在合并片段${index.toString()}...`.blue); - } - else - { - console.log("正在合并文件...".blue); - } - if (options.parts === 1) - { - fs.renameSync(title + ".part0", dest); - } - else - { - if (fs.existsSync(dest)) - { - fs.unlinkSync(dest); - } - const files = fs.readdirSync("."); - const parts = files.filter(it => it.includes(title + ".part")); - const partRegex = /.*\.part([\d]+)/; - parts.sort((a, b) => - { - const partA = parseInt(a.replace(partRegex, "$1")); - const partB = parseInt(b.replace(partRegex, "$1")); - return partA - partB; - }).forEach(file => - { - const buffer = fs.readFileSync(file); - fs.appendFileSync(dest, buffer); - }); - parts.forEach(file => fs.unlinkSync(file)); - } - return dest; + } + private async mergeFragment(title: string, index = -1) { + const dest = title + this.extension; + if (!this.progressBar.complete) { + this.progressBar.update(1); + this.progressBar.terminate(); } - async merge() - { - let result: string | string[]; - if (typeof this.title === "string") - { - result = await this.mergeFragment(this.title); - } - else - { - result = await Promise.all(this.title.map((t, i) => this.mergeFragment(t, i))); - } - console.log(`完成: `.green); - if (typeof result === "string") - { - console.log(result); - } - else - { - result.forEach(it => console.log(it)); - } + if (fs.existsSync(dest)) { + return dest; } + if (index !== -1) { + console.log(`正在合并片段${index.toString()}...`.blue); + } + else { + console.log("正在合并文件...".blue); + } + if (options.parts === 1) { + fs.renameSync(title + ".part0", dest); + } + else { + if (fs.existsSync(dest)) { + fs.unlinkSync(dest); + } + const files = fs.readdirSync("."); + const parts = files.filter(it => it.includes(title + ".part")); + const partRegex = /.*\.part([\d]+)/; + parts.sort((a, b) => { + const partA = parseInt(a.replace(partRegex, "$1")); + const partB = parseInt(b.replace(partRegex, "$1")); + return partA - partB; + }).forEach(file => { + const buffer = fs.readFileSync(file); + fs.appendFileSync(dest, buffer); + }); + parts.forEach(file => fs.unlinkSync(file)); + } + return dest; + } + async merge() { + let result: string | string[]; + if (typeof this.title === "string") { + result = await this.mergeFragment(this.title); + } + else { + result = await Promise.all(this.title.map((t, i) => this.mergeFragment(t, i))); + } + console.log(`完成: `.green); + if (typeof result === "string") { + console.log(result); + } + else { + result.forEach(it => console.log(it)); + } + } } -(async () => -{ - try - { - let jsonText = ''; - if (fs.existsSync(options.info)) - { - jsonText = fs.readFileSync(options.info).toString("utf-8"); - } - else - { - jsonText = await clipboardy.read(); - } - const inputData = JSON.parse(jsonText) as InputData | InputData[]; - if (!(function (data: any): data is InputData | InputData[] - { - if (Array.isArray(data) && data.length > 0) - { - return data[0].fragments !== undefined; - } - else - { - return data.fragments !== undefined; - } - })(inputData)) - { - throw new Error(); - } - try - { - process.chdir(options.output); - process.on("SIGINT", () => - { - Downloader.workingDownloader && Downloader.workingDownloader.cancelDownload(); - process.exit(); - }); - if (Array.isArray(inputData)) - { - const downloaders = inputData.map(data => new Downloader(data)); - for (const downloader of downloaders) - { - await downloader.download(); - await downloader.merge(); - } - } - else - { - const downloader = new Downloader(inputData); - await downloader.download(); - await downloader.merge(); - } - } - catch (error) - { - Downloader.workingDownloader && Downloader.workingDownloader.cancelDownload(); - console.error(`\n错误: ${error}`.red); - } +(async () => { + try { + let jsonText = ''; + if (fs.existsSync(options.info)) { + jsonText = fs.readFileSync(options.info).toString("utf-8"); } - catch (error) - { - console.log(`[无数据] 未在剪贴板检测到有效数据/没有指定输入文件/输入文件的数据无效.`.red); + else { + jsonText = await clipboardy.read(); } -})(); \ No newline at end of file + const inputData = JSON.parse(jsonText) as InputData | InputData[]; + if (!(function (data: any): data is InputData | InputData[] { + if (Array.isArray(data) && data.length > 0) { + return data[0].fragments !== undefined; + } + else { + return data.fragments !== undefined; + } + })(inputData)) { + throw new Error(); + } + try { + process.chdir(options.output); + process.on("SIGINT", () => { + Downloader.workingDownloader && Downloader.workingDownloader.cancelDownload(); + process.exit(); + }); + if (Array.isArray(inputData)) { + const downloaders = inputData.map(data => new Downloader(data)); + for (const downloader of downloaders) { + await downloader.download(); + await downloader.merge(); + } + } + else { + const downloader = new Downloader(inputData); + await downloader.download(); + await downloader.merge(); + } + } + catch (error) { + Downloader.workingDownloader && Downloader.workingDownloader.cancelDownload(); + console.error(`\n错误: ${error}`.red); + } + } + catch (error) { + console.log(`[无数据] 未在剪贴板检测到有效数据/没有指定输入文件/输入文件的数据无效.`.red); + } +})(); diff --git a/min/batch-download.min.js b/min/batch-download.min.js index 8b30e28f4..725ef0e05 100644 --- a/min/batch-download.min.js +++ b/min/batch-download.min.js @@ -1 +1 @@ -(()=>{return(t,e)=>{class i{static async test(){if(!document.URL.includes("/www.bilibili.com/video/av")){return false}return await SpinQuery.select("#multi_page")!==null}async collectData(t){const e=`https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`;const i=await Ajax.getJson(e);if(i.code!==0){Toast.error(`获取视频选集列表失败, message=${i.message}`,"批量下载");return""}const s=i.data.pages;if(s===undefined){Toast.error(`获取视频选集列表失败, 没有找到选集信息.`,"批量下载");return""}const a=[];for(const e of s){const i=`https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${e.cid}&qn=${t}&otype=json`;const s=await Ajax.getJsonWithCredentials(i);const r=s.data||s.result||s;if(r.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=r.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});a.push({fragments:n,title:`${e.page} - ${e.part}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(a)}}class s{static async test(){return document.URL.includes("/www.bilibili.com/bangumi")}async collectData(t){const e=document.querySelector("meta[property='og:url']");if(e===null){Toast.error("获取番剧数据失败: 无法找到 Season ID","批量下载");return""}const i=e.getAttribute("content").match(/play\/ss(\d+)/)[1];if(i===undefined){Toast.error("获取番剧数据失败: 无法解析 Season ID","批量下载");return""}const s=await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${i}`);if(s.code!==0){Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${s.message}`,"批量下载");return""}const a=s.result.main_section.episodes.map(t=>{return{aid:t.aid,cid:t.cid,number:t.title,title:t.long_title}});const r=[];for(const e of a){const i=`https://api.bilibili.com/pgc/player/web/playurl?avid=${e.aid}&cid=${e.cid}&qn=${t}&otype=json`;const s=await Ajax.getJsonWithCredentials(i);const a=s.data||s.result||s;if(a.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=a.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});r.push({fragments:n,title:`${e.number} - ${e.title}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(r)}}const a=[s,i];let r=null;class n{static async test(){for(const t of a){if(await t.test()===true){r=t;return true}}r=null;return false}async collectData(t,e){if(r===null){logError("[批量下载] 未找到合适的解析模块.");return null}const i=new r;const s=await i.collectData(t.quality);e.dismiss();return s}}return{export:{BatchExtractor:n}}}})(); \ No newline at end of file +(()=>{return(t,e)=>{class i{static async test(){if(!document.URL.includes("/www.bilibili.com/video/av")){return false}return await SpinQuery.select("#multi_page")!==null}async collectData(t){const e=`https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`;const i=await Ajax.getJson(e);if(i.code!==0){Toast.error(`获取视频选集列表失败, message=${i.message}`,"批量下载");return""}const r=i.data.pages;if(r===undefined){Toast.error(`获取视频选集列表失败, 没有找到选集信息.`,"批量下载");return""}const a=[];for(const e of r){const i=`https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${e.cid}&qn=${t}&otype=json`;const r=await Ajax.getJsonWithCredentials(i);const s=r.data||r.result||r;if(s.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=s.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});a.push({fragments:n,title:`${e.page} - ${e.part}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid,referer:document.URL.replace(window.location.search,"")})}return JSON.stringify(a)}}class r{static async test(){return document.URL.includes("/www.bilibili.com/bangumi")}async collectData(t){const e=document.querySelector("meta[property='og:url']");if(e===null){Toast.error("获取番剧数据失败: 无法找到 Season ID","批量下载");return""}const i=e.getAttribute("content").match(/play\/ss(\d+)/)[1];if(i===undefined){Toast.error("获取番剧数据失败: 无法解析 Season ID","批量下载");return""}const r=await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${i}`);if(r.code!==0){Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${r.message}`,"批量下载");return""}const a=r.result.main_section.episodes.map(t=>{return{aid:t.aid,cid:t.cid,number:t.title,title:t.long_title}});const s=[];for(const e of a){const i=`https://api.bilibili.com/pgc/player/web/playurl?avid=${e.aid}&cid=${e.cid}&qn=${t}&otype=json`;const r=await Ajax.getJsonWithCredentials(i);const a=r.data||r.result||r;if(a.quality!==t){Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.","批量下载");return""}const n=a.durl.map(t=>{return{length:t.length,size:t.size,url:t.url}});s.push({fragments:n,title:`${e.number} - ${e.title}`,totalSize:n.map(t=>t.size).reduce((t,e)=>t+e),cid:e.cid})}return JSON.stringify(s)}}const a=[r,i];let s=null;class n{static async test(){for(const t of a){if(await t.test()===true){s=t;return true}}s=null;return false}async collectData(t,e){if(s===null){logError("[批量下载] 未找到合适的解析模块.");return null}const i=new s;const r=await i.collectData(t.quality);e.dismiss();return r}}return{export:{BatchExtractor:n}}}})(); \ No newline at end of file diff --git a/min/download-video.min.js b/min/download-video.min.js index d20b5a159..3b7093ecf 100644 --- a/min/download-video.min.js +++ b/min/download-video.min.js @@ -1 +1 @@ -(()=>{return(e,t)=>{const{getFriendlyTitle:s}=t.import("title");const n={entity:null,aid:undefined,cid:undefined};let o=[];let r=null;class a{constructor(){this.menuClasses=["quality","action","progress"];this.currentMenuClass="quality"}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}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=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/x/player/playurl?avid=${n.aid}&cid=${n.cid}&otype=json`}}}class i extends a{async getUrl(e){if(e){return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=&otype=json`}}}class l{constructor(e,t,s){this.quality=e;this.internalName=t;this.displayName=s}async downloadInfo(){const e=new d(this);await e.fetchVideoInfo();return e}static get availableFormats(){return new Promise((e,t)=>{n.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 r=o.accept_quality;const a=o.accept_format.split(",");const i=o.accept_description;const c=[];while(r.length>0){const e=new l(r.pop(),a.pop(),i.pop());c.push(e)}e(c)});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 d{constructor(e,t){this.format=e;this.fragments=t||[];this.progress=null;this.totalSize=null;this.workingXhr=null;this.fragmentSplitFactor=6*5}fetchVideoInfo(){return new Promise((e,t)=>{n.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 r=o.durl;this.fragments=r.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("forEach"in this.workingXhr){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{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}/${i} 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")}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)}]);if(e){GM_setClipboard(t,"text")}else{const e=document.createElement("a");const s=new Blob([t],{type:"text/json"});const n=URL.createObjectURL(s);e.setAttribute("href",n);e.setAttribute("download",`cid${unsafeWindow.cid}.json`);document.body.appendChild(e);e.click();e.remove();URL.revokeObjectURL(n)}}extension(e){return(e||this.fragments[0]).url.indexOf(".flv")!==-1?".flv":".mp4"}makeBlob(e,t=null){return new Blob(Array.isArray(e)?e:[e],{type:this.extension(t)===".flv"?"video/x-flv":"video/mp4"})}cleanUpOldBlobUrl(){const e=document.querySelector("a#video-complete").getAttribute("href");if(e&&!document.querySelector(`.link[href="${e}"]`)){URL.revokeObjectURL(e)}[...document.querySelectorAll(".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[n,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 r=n+".zip";return[o,r]}async download(){const e=[];this.totalSize=this.fragments.map(e=>e.size).reduce((e,t)=>e+t);for(const t of this.fragments){const s=await this.downloadFragment(t);e.push(s)}if(e.length<1){throw new Error("下载失败.")}let t=null;let s=null;if(e.length===1){[t,s]=this.downloadSingle(e)}else{[t,s]=await this.downloadMultiple(e)}this.cleanUpOldBlobUrl();const n=URL.createObjectURL(t);this.progress&&this.progress(0);return{url:n,filename:s}}}async function u(){const e=["/www.bilibili.com/bangumi","/www.bilibili.com/video/av"];if(!e.some(e=>document.URL.includes(e))){return}const{BatchExtractor:s}=await t.importAsync("batchDownload");if(await s.test()!==true){return}const o=new s;document.getElementById("download-video").classList.add("batch");document.getElementById("video-action-batch-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}GM_setClipboard(t,{type:"text/json"});Toast.success("已复制批量数据到剪贴板.","复制批量数据",3e3)});document.getElementById("video-action-batch-download-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}const s=document.createElement("a");const a=new Blob([t],{type:"text/json"});const i=URL.createObjectURL(a);s.setAttribute("href",i);s.setAttribute("download",`export.json`);document.body.appendChild(s);s.click();s.remove();URL.revokeObjectURL(i)})}async function h(){const e=await SpinQuery.select(()=>(unsafeWindow||window).aid);const t=await SpinQuery.select(()=>(unsafeWindow||window).cid);n.aid=e;n.cid=t;if(document.URL.indexOf("bangumi")!==-1){n.entity=new i}else{n.entity=new a}try{o=await l.availableFormats}catch(e){return false}return Boolean(e&&t)}async function p(){r=o[0];const e=async()=>{const e=await h();document.querySelector("#download-video").style.display=e?"flex":"none";if(e===false){return}const t=document.querySelector("ol.video-quality");t.childNodes.forEach(t.removeChild);o.forEach(e=>{const s=document.createElement("li");s.innerHTML=e.displayName;s.addEventListener("click",()=>{r=e;n.entity.nextMenuClass()});t.insertAdjacentElement("afterbegin",s)})};Observer.videoChange(e);const s=()=>r.downloadInfo().catch(e=>{n.entity.addError();$(".video-error").text(e)});async function a(){if(!r){return}n.entity.nextMenuClass();const e=await s();e.progress=(e=>{$(".download-progress-value").text(`${fixed(e*100)}`);$(".download-progress-foreground").css("transform",`scaleX(${e})`)});document.querySelector(".download-progress-cancel>span").onclick=(()=>e.cancelDownload());const t=await e.download().catch(e=>{n.entity.addError();$(".video-error").text(e)});if(!t){return}const o=document.getElementById("video-complete");o.setAttribute("href",t.url);o.setAttribute("download",t.filename);o.click();const a=`下载完成. 再次保存`;Toast.success(a,"下载视频");n.entity.resetMenuClass()}async function i(){if(!r){return}const e=await s();e.copyUrl();Toast.success("已复制链接到剪贴板.","复制链接",3e3);n.entity.resetMenuClass()}document.querySelector("#video-action-download").addEventListener("click",a);document.querySelector("#video-action-copy").addEventListener("click",i);document.querySelector("#video-action-copy-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(true);Toast.success("已复制数据到剪贴板.","复制数据",3e3);n.entity.resetMenuClass()});document.querySelector("#video-action-download-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(false);n.entity.resetMenuClass()});t.applyStyle("downloadVideoStyle");const l=document.querySelector(".download-video-panel");const c=()=>$(".download-video-panel").toggleClass("opened");document.querySelector("#download-video").addEventListener("click",e=>{if(!l.contains(e.target)){c()}});document.querySelector(".video-error").addEventListener("click",()=>{document.querySelector(".video-error").innerHTML="";n.entity.removeError()});await SpinQuery.select(".download-video-panel");n.entity.addMenuClass();u()}return{widget:{content:t.data.downloadVideoHtml.text,condition:h,success:p}}}})(); \ No newline at end of file +(()=>{return(e,t)=>{const{getFriendlyTitle:s}=t.import("title");const n={entity:null,aid:undefined,cid:undefined};let o=[];let r=null;class a{constructor(){this.menuClasses=["quality","action","progress"];this.currentMenuClass="quality"}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}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=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/x/player/playurl?avid=${n.aid}&cid=${n.cid}&otype=json`}}}class i extends a{async getUrl(e){if(e){return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=${e}&otype=json`}else{return`https://api.bilibili.com/pgc/player/web/playurl?avid=${n.aid}&cid=${n.cid}&qn=&otype=json`}}}class l{constructor(e,t,s){this.quality=e;this.internalName=t;this.displayName=s}async downloadInfo(){const e=new d(this);await e.fetchVideoInfo();return e}static get availableFormats(){return new Promise((e,t)=>{n.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 r=o.accept_quality;const a=o.accept_format.split(",");const i=o.accept_description;const c=[];while(r.length>0){const e=new l(r.pop(),a.pop(),i.pop());c.push(e)}e(c)});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 d{constructor(e,t){this.format=e;this.fragments=t||[];this.progress=null;this.totalSize=null;this.workingXhr=null;this.fragmentSplitFactor=6*5}fetchVideoInfo(){return new Promise((e,t)=>{n.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 r=o.durl;this.fragments=r.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("forEach"in this.workingXhr){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{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}/${i} 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")}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=document.createElement("a");const s=new Blob([t],{type:"text/json"});const n=URL.createObjectURL(s);e.setAttribute("href",n);e.setAttribute("download",`cid${unsafeWindow.cid}.json`);document.body.appendChild(e);e.click();e.remove();URL.revokeObjectURL(n)}}extension(e){return(e||this.fragments[0]).url.indexOf(".flv")!==-1?".flv":".mp4"}makeBlob(e,t=null){return new Blob(Array.isArray(e)?e:[e],{type:this.extension(t)===".flv"?"video/x-flv":"video/mp4"})}cleanUpOldBlobUrl(){const e=document.querySelector("a#video-complete").getAttribute("href");if(e&&!document.querySelector(`.link[href="${e}"]`)){URL.revokeObjectURL(e)}[...document.querySelectorAll(".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[n,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 r=n+".zip";return[o,r]}async download(){const e=[];this.totalSize=this.fragments.map(e=>e.size).reduce((e,t)=>e+t);for(const t of this.fragments){const s=await this.downloadFragment(t);e.push(s)}if(e.length<1){throw new Error("下载失败.")}let t=null;let s=null;if(e.length===1){[t,s]=this.downloadSingle(e)}else{[t,s]=await this.downloadMultiple(e)}this.cleanUpOldBlobUrl();const n=URL.createObjectURL(t);this.progress&&this.progress(0);return{url:n,filename:s}}}async function u(){const e=["/www.bilibili.com/bangumi","/www.bilibili.com/video/av"];if(!e.some(e=>document.URL.includes(e))){return}const{BatchExtractor:s}=await t.importAsync("batchDownload");if(await s.test()!==true){return}const o=new s;document.getElementById("download-video").classList.add("batch");document.getElementById("video-action-batch-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}GM_setClipboard(t,{type:"text/json"});Toast.success("已复制批量数据到剪贴板.","复制批量数据",3e3)});document.getElementById("video-action-batch-download-data").addEventListener("click",async()=>{if(!r){return}n.entity.resetMenuClass();const e=Toast.info("获取链接中...","批量下载");const t=await o.collectData(r,e);if(!t){return}const s=document.createElement("a");const a=new Blob([t],{type:"text/json"});const i=URL.createObjectURL(a);s.setAttribute("href",i);s.setAttribute("download",`export.json`);document.body.appendChild(s);s.click();s.remove();URL.revokeObjectURL(i)})}async function h(){const e=await SpinQuery.select(()=>(unsafeWindow||window).aid);const t=await SpinQuery.select(()=>(unsafeWindow||window).cid);n.aid=e;n.cid=t;if(document.URL.indexOf("bangumi")!==-1){n.entity=new i}else{n.entity=new a}try{o=await l.availableFormats}catch(e){return false}return Boolean(e&&t)}async function p(){r=o[0];const e=async()=>{const e=await h();document.querySelector("#download-video").style.display=e?"flex":"none";if(e===false){return}const t=document.querySelector("ol.video-quality");t.childNodes.forEach(t.removeChild);o.forEach(e=>{const s=document.createElement("li");s.innerHTML=e.displayName;s.addEventListener("click",()=>{r=e;n.entity.nextMenuClass()});t.insertAdjacentElement("afterbegin",s)})};Observer.videoChange(e);const s=()=>r.downloadInfo().catch(e=>{n.entity.addError();$(".video-error").text(e)});async function a(){if(!r){return}n.entity.nextMenuClass();const e=await s();e.progress=(e=>{$(".download-progress-value").text(`${fixed(e*100)}`);$(".download-progress-foreground").css("transform",`scaleX(${e})`)});document.querySelector(".download-progress-cancel>span").onclick=(()=>e.cancelDownload());const t=await e.download().catch(e=>{n.entity.addError();$(".video-error").text(e)});if(!t){return}const o=document.getElementById("video-complete");o.setAttribute("href",t.url);o.setAttribute("download",t.filename);o.click();const a=`下载完成. 再次保存`;Toast.success(a,"下载视频");n.entity.resetMenuClass()}async function i(){if(!r){return}const e=await s();e.copyUrl();Toast.success("已复制链接到剪贴板.","复制链接",3e3);n.entity.resetMenuClass()}document.querySelector("#video-action-download").addEventListener("click",a);document.querySelector("#video-action-copy").addEventListener("click",i);document.querySelector("#video-action-copy-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(true);Toast.success("已复制数据到剪贴板.","复制数据",3e3);n.entity.resetMenuClass()});document.querySelector("#video-action-download-data").addEventListener("click",async()=>{if(!r){return}const e=await s();e.exportData(false);n.entity.resetMenuClass()});t.applyStyle("downloadVideoStyle");const l=document.querySelector(".download-video-panel");const c=()=>$(".download-video-panel").toggleClass("opened");document.querySelector("#download-video").addEventListener("click",e=>{if(!l.contains(e.target)){c()}});document.querySelector(".video-error").addEventListener("click",()=>{document.querySelector(".video-error").innerHTML="";n.entity.removeError()});await SpinQuery.select(".download-video-panel");n.entity.addMenuClass();u()}return{widget:{content:t.data.downloadVideoHtml.text,condition:h,success:p}}}})(); \ No newline at end of file diff --git a/video/download-video/batch-download.js b/video/download-video/batch-download.js index bd45211b7..99f16ff5a 100644 --- a/video/download-video/batch-download.js +++ b/video/download-video/batch-download.js @@ -1,148 +1,124 @@ -class VideoEpisodeBatch -{ - static async test() - { - if (!document.URL.includes("/www.bilibili.com/video/av")) - { - return false; - } - return await SpinQuery.select("#multi_page") !== null; +class VideoEpisodeBatch { + static async test () { + if (!document.URL.includes('/www.bilibili.com/video/av')) { + return false } - async collectData(quality) - { - const api = `https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}`; - const json = await Ajax.getJson(api); - if (json.code !== 0) - { - Toast.error(`获取视频选集列表失败, message=${json.message}`, "批量下载"); - return ""; - } - const pages = json.data.pages; - if (pages === undefined) - { - Toast.error(`获取视频选集列表失败, 没有找到选集信息.`, "批量下载"); - return ""; - } - const result = []; - for (const page of pages) - { - const url = `https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${page.cid}&qn=${quality}&otype=json`; - const json = await Ajax.getJsonWithCredentials(url); - const data = json.data || json.result || json; - if (data.quality !== quality) - { - Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.", "批量下载"); - return ""; - } - const fragments = data.durl.map(it => - { - return { - length: it.length, - size: it.size, - url: it.url, - }; - }); - result.push({ - fragments, - title: `${page.page} - ${page.part}`, - totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it), - cid: page.cid, - }); - } - return JSON.stringify(result); + return await SpinQuery.select('#multi_page') !== null + } + async collectData (quality) { + const api = `https://api.bilibili.com/x/web-interface/view?aid=${unsafeWindow.aid}` + const json = await Ajax.getJson(api) + if (json.code !== 0) { + Toast.error(`获取视频选集列表失败, message=${json.message}`, '批量下载') + return '' } + const pages = json.data.pages + if (pages === undefined) { + Toast.error(`获取视频选集列表失败, 没有找到选集信息.`, '批量下载') + return '' + } + const result = [] + for (const page of pages) { + const url = `https://api.bilibili.com/x/player/playurl?avid=${unsafeWindow.aid}&cid=${page.cid}&qn=${quality}&otype=json` + const json = await Ajax.getJsonWithCredentials(url) + const data = json.data || json.result || json + if (data.quality !== quality) { + Toast.error('获取下载链接失败, 请确认当前账号有下载权限后重试.', '批量下载') + return '' + } + const fragments = data.durl.map(it => { + return { + length: it.length, + size: it.size, + url: it.url + } + }) + result.push({ + fragments, + title: `${page.page} - ${page.part}`, + totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it), + cid: page.cid, + referer: document.URL.replace(window.location.search, '') + }) + } + return JSON.stringify(result) + } } -class BangumiBatch -{ - static async test() - { - return document.URL.includes("/www.bilibili.com/bangumi"); +class BangumiBatch { + static async test () { + return document.URL.includes('/www.bilibili.com/bangumi') + } + async collectData (quality) { + const metaUrl = document.querySelector("meta[property='og:url']") + if (metaUrl === null) { + Toast.error('获取番剧数据失败: 无法找到 Season ID', '批量下载') + return '' } - async collectData(quality) - { - const metaUrl = document.querySelector("meta[property='og:url']"); - if (metaUrl === null) - { - Toast.error("获取番剧数据失败: 无法找到 Season ID", "批量下载"); - return ""; - } - const seasonId = metaUrl.getAttribute("content").match(/play\/ss(\d+)/)[1]; - if (seasonId === undefined) - { - Toast.error("获取番剧数据失败: 无法解析 Season ID", "批量下载"); - return ""; - } - const json = await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${seasonId}`); - if (json.code !== 0) - { - Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${json.message}`, "批量下载"); - return ""; - } - const pages = json.result.main_section.episodes.map(it => - { - return { aid: it.aid, cid: it.cid, number: it.title, title: it.long_title }; - }); - const result = []; - for (const page of pages) - { - const url = `https://api.bilibili.com/pgc/player/web/playurl?avid=${page.aid}&cid=${page.cid}&qn=${quality}&otype=json`; - const json = await Ajax.getJsonWithCredentials(url); - const data = json.data || json.result || json; - if (data.quality !== quality) - { - Toast.error("获取下载链接失败, 请确认当前账号有下载权限后重试.", "批量下载"); - return ""; - } - const fragments = data.durl.map(it => - { - return { - length: it.length, - size: it.size, - url: it.url, - }; - }); - result.push({ - fragments, - title: `${page.number} - ${page.title}`, - totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it), - cid: page.cid, - }); - } - return JSON.stringify(result); + const seasonId = metaUrl.getAttribute('content').match(/play\/ss(\d+)/)[1] + if (seasonId === undefined) { + Toast.error('获取番剧数据失败: 无法解析 Season ID', '批量下载') + return '' } + const json = await Ajax.getJson(`https://api.bilibili.com/pgc/web/season/section?season_id=${seasonId}`) + if (json.code !== 0) { + Toast.error(`获取番剧数据失败: 无法获取番剧集数列表, message=${json.message}`, '批量下载') + return '' + } + const pages = json.result.main_section.episodes.map(it => { + return { aid: it.aid, cid: it.cid, number: it.title, title: it.long_title } + }) + const result = [] + for (const page of pages) { + const url = `https://api.bilibili.com/pgc/player/web/playurl?avid=${page.aid}&cid=${page.cid}&qn=${quality}&otype=json` + const json = await Ajax.getJsonWithCredentials(url) + const data = json.data || json.result || json + if (data.quality !== quality) { + Toast.error('获取下载链接失败, 请确认当前账号有下载权限后重试.', '批量下载') + return '' + } + const fragments = data.durl.map(it => { + return { + length: it.length, + size: it.size, + url: it.url + } + }) + result.push({ + fragments, + title: `${page.number} - ${page.title}`, + totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it), + cid: page.cid + }) + } + return JSON.stringify(result) + } } -const extractors = [BangumiBatch, VideoEpisodeBatch]; -let ExtractorClass = null; -export class BatchExtractor -{ - static async test() - { - for (const e of extractors) - { - if (await e.test() === true) - { - ExtractorClass = e; - return true; - } - } - ExtractorClass = null; - return false; +const extractors = [BangumiBatch, VideoEpisodeBatch] +let ExtractorClass = null +export class BatchExtractor { + static async test () { + for (const e of extractors) { + if (await e.test() === true) { + ExtractorClass = e + return true + } } - async collectData(format, toast) - { - if (ExtractorClass === null) - { - logError("[批量下载] 未找到合适的解析模块."); - return null; - } - const extractor = new ExtractorClass; - const result = await extractor.collectData(format.quality); - toast.dismiss(); - return result; + ExtractorClass = null + return false + } + async collectData (format, toast) { + if (ExtractorClass === null) { + logError('[批量下载] 未找到合适的解析模块.') + return null } + const extractor = new ExtractorClass() + const result = await extractor.collectData(format.quality) + toast.dismiss() + return result + } } export default { - export: { - BatchExtractor, - }, -}; \ No newline at end of file + export: { + BatchExtractor + } +} diff --git a/video/download-video/download-video.js b/video/download-video/download-video.js index 4cddf2cd4..9d25094a8 100644 --- a/video/download-video/download-video.js +++ b/video/download-video/download-video.js @@ -1,577 +1,471 @@ -import { getFriendlyTitle } from "../title"; +import { getFriendlyTitle } from '../title' const pageData = { - entity: null, - aid: undefined, - cid: undefined, -}; -let formats = []; -let selectedFormat = null; -class Video -{ - constructor() - { - this.menuClasses = ["quality", "action", "progress"]; - this.currentMenuClass = "quality"; - } - 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 index = this.menuClasses.indexOf(this.currentMenuClass) + 1; - const next = this.menuClasses[index >= this.menuClasses.length ? 0 : index]; - this.currentMenuClass = next; - this.addMenuClass(); - return next; - } - addError() - { - this.menuPanel.classList.add("error"); - } - removeError() - { - this.menuPanel.classList.remove("error"); - this.resetMenuClass(); - } - async getUrl(quality) - { - if (quality) - { - return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json`; - } - else - { - return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&otype=json`; - } - } + entity: null, + aid: undefined, + cid: undefined } -class Bangumi extends Video -{ - async getUrl(quality) - { - if (quality) - { - return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json`; - } - else - { - return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=&otype=json`; - } +let formats = [] +let selectedFormat = null +class Video { + constructor () { + this.menuClasses = ['quality', 'action', 'progress'] + this.currentMenuClass = 'quality' + } + 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 index = this.menuClasses.indexOf(this.currentMenuClass) + 1 + const next = this.menuClasses[index >= this.menuClasses.length ? 0 : index] + this.currentMenuClass = next + this.addMenuClass() + return next + } + addError () { + this.menuPanel.classList.add('error') + } + removeError () { + this.menuPanel.classList.remove('error') + this.resetMenuClass() + } + async getUrl (quality) { + if (quality) { + return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json` + } else { + return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&otype=json` } + } } -class VideoFormat -{ - constructor(quality, internalName, displayName) - { - this.quality = quality; - this.internalName = internalName; - this.displayName = displayName; - } - async downloadInfo() - { - const videoInfo = new VideoDownloader(this); - await videoInfo.fetchVideoInfo(); - return videoInfo; - } - static get availableFormats() - { - return new Promise((resolve, reject) => - { - pageData.entity.getUrl().then(url => - { - const xhr = new XMLHttpRequest(); - xhr.addEventListener("load", () => - { - const json = JSON.parse(xhr.responseText); - if (json.code !== 0) - { - reject("获取清晰度信息失败."); - return; - } - const data = json.data || json.result || json; - const qualities = data.accept_quality; - const internalNames = data.accept_format.split(","); - const displayNames = data.accept_description; - const formats = []; - while (qualities.length > 0) - { - const format = new VideoFormat( - qualities.pop(), - internalNames.pop(), - displayNames.pop() - ); - formats.push(format); - } - resolve(formats); - }); - xhr.addEventListener("error", () => reject(`获取清晰度信息失败.`)); - xhr.withCredentials = true; - xhr.open("GET", url); - xhr.send(); - }); - }); +class Bangumi extends Video { + async getUrl (quality) { + if (quality) { + return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json` + } else { + return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=&otype=json` } + } } -class VideoDownloaderFragment -{ - constructor(length, size, url, backupUrls) - { - this.length = length; - this.size = size; - this.url = url; - this.backupUrls = backupUrls; - } +class VideoFormat { + constructor (quality, internalName, displayName) { + this.quality = quality + this.internalName = internalName + this.displayName = displayName + } + async downloadInfo () { + const videoInfo = new VideoDownloader(this) + await videoInfo.fetchVideoInfo() + return videoInfo + } + static get availableFormats () { + return new Promise((resolve, reject) => { + pageData.entity.getUrl().then(url => { + const xhr = new XMLHttpRequest() + xhr.addEventListener('load', () => { + const json = JSON.parse(xhr.responseText) + if (json.code !== 0) { + reject('获取清晰度信息失败.') + return + } + const data = json.data || json.result || json + const qualities = data.accept_quality + const internalNames = data.accept_format.split(',') + const displayNames = data.accept_description + const formats = [] + while (qualities.length > 0) { + const format = new VideoFormat( + qualities.pop(), + internalNames.pop(), + displayNames.pop() + ) + formats.push(format) + } + resolve(formats) + }) + xhr.addEventListener('error', () => reject(`获取清晰度信息失败.`)) + xhr.withCredentials = true + xhr.open('GET', url) + xhr.send() + }) + }) + } } -class VideoDownloader -{ - constructor(format, fragments) - { - this.format = format; - this.fragments = fragments || []; - this.progress = null; - // this.loaded = 0; - this.totalSize = null; - this.workingXhr = null; - this.fragmentSplitFactor = 6 * 5; +class VideoDownloaderFragment { + constructor (length, size, url, backupUrls) { + this.length = length + this.size = size + this.url = url + this.backupUrls = backupUrls + } +} +class VideoDownloader { + constructor (format, fragments) { + this.format = format + this.fragments = fragments || [] + this.progress = null + // this.loaded = 0 + this.totalSize = null + this.workingXhr = null + this.fragmentSplitFactor = 6 * 5 + } + fetchVideoInfo () { + return new Promise((resolve, reject) => { + pageData.entity.getUrl(this.format.quality).then(url => { + const xhr = new XMLHttpRequest() + xhr.addEventListener('load', () => { + const json = JSON.parse(xhr.responseText.replace(/http:/g, 'https:')) + const data = json.data || json.result || json + if (data.quality !== this.format.quality) { + reject('获取下载链接失败, 请确认当前账号有下载权限后重试.') + } + const urls = data.durl + this.fragments = urls.map(it => new VideoDownloaderFragment( + it.length, it.size, + it.url, + it.backup_url + )) + resolve(this.fragments) + }) + xhr.withCredentials = true + xhr.open('GET', url) + xhr.send() + }) + }) + } + updateProgress () { + const progress = this.progressMap + ? [...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.totalSize : 0 + if (progress > 1 || progress < 0) { + console.error(`[下载视频] 进度异常: ${progress}`, this.progressMap.values()) } - fetchVideoInfo() - { - return new Promise((resolve, reject) => - { - pageData.entity.getUrl(this.format.quality).then(url => - { - const xhr = new XMLHttpRequest(); - xhr.addEventListener("load", () => - { - const json = JSON.parse(xhr.responseText.replace(/http:/g, "https:")); - const data = json.data || json.result || json; - if (data.quality !== this.format.quality) - { - reject("获取下载链接失败, 请确认当前账号有下载权限后重试."); - } - const urls = data.durl; - this.fragments = urls.map(it => new VideoDownloaderFragment( - it.length, it.size, - it.url, - it.backup_url - )); - resolve(this.fragments); - }); - xhr.withCredentials = true; - xhr.open("GET", url); - xhr.send(); - }); - }); + this.progress && this.progress(progress) + } + cancelDownload () { + if ('forEach' in this.workingXhr) { + this.workingXhr.forEach(it => it.abort()) + } else { + logError('Cancel Download Failed: forEach in this.workingXhr not found.') } - updateProgress() - { - const progress = this.progressMap ? - [...this.progressMap.values()].reduce((a, b) => a + b, 0) / this.totalSize : 0; - if (progress > 1 || progress < 0) - { - console.error(`[下载视频] 进度异常: ${progress}`, this.progressMap.values()); - } - this.progress && this.progress(progress); + } + downloadFragment (fragment) { + const promises = [] + this.workingXhr = [] + this.progressMap = new Map() + this.updateProgress() + const partialLength = Math.round(fragment.size / this.fragmentSplitFactor) + let startByte = 0 + const getPartNumber = xhr => [...this.progressMap.keys()].indexOf(xhr) + 1 + while (startByte < fragment.size) { + const endByte = Math.min(fragment.size - 1, Math.round(startByte + partialLength)) + const range = `bytes=${startByte}-${endByte}` + const rangeLength = endByte - startByte + 1 + promises.push(new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + xhr.open('GET', fragment.url) + xhr.responseType = 'arraybuffer' + xhr.withCredentials = false + xhr.addEventListener('progress', (e) => { + console.log(`[下载视频] 视频片段${getPartNumber(xhr)}下载进度: ${e.loaded}/${rangeLength} bytes loaded, ${range}`) + this.progressMap.set(xhr, e.loaded) + this.updateProgress() + }) + xhr.addEventListener('load', () => { + if (('' + xhr.status)[0] === '2') { + resolve(xhr.response) + } else { + reject(`请求失败.`) + } + }) + xhr.addEventListener('abort', () => reject('下载已取消.')) + xhr.addEventListener('error', () => { + console.error(`[下载视频] 视频片段${getPartNumber(xhr)}下载失败: ${range}`) + this.progressMap.set(xhr, 0) + this.updateProgress() + xhr.open('GET', fragment.url) + xhr.setRequestHeader('Range', range) + xhr.send() + }) + xhr.setRequestHeader('Range', range) + this.progressMap.set(xhr, 0) + xhr.send() + this.workingXhr.push(xhr) + })) + startByte = Math.round(startByte + partialLength) + 1 } - cancelDownload() - { - if ("forEach" in this.workingXhr) - { - this.workingXhr.forEach(it => it.abort()); - } - else - { - logError("Cancel Download Failed: forEach in this.workingXhr not found."); - } + return Promise.all(promises) + } + copyUrl () { + const urls = this.fragments.map(it => it.url).reduce((acc, it) => acc + '\r\n' + it) + GM_setClipboard(urls, 'text') + } + exportData (copy = false) { + const data = JSON.stringify([{ + fragments: this.fragments, + title: getFriendlyTitle(true), + totalSize: this.fragments.map(it => it.size).reduce((acc, it) => acc + it), + referer: document.URL.replace(window.location.search, '') + }]) + if (copy) { + GM_setClipboard(data, 'text') + } else { + const a = document.createElement('a') + const blob = new Blob([data], { type: 'text/json' }) + const url = URL.createObjectURL(blob) + a.setAttribute('href', url) + a.setAttribute('download', `cid${unsafeWindow.cid}.json`) + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) } - downloadFragment(fragment) - { - const promises = []; - this.workingXhr = []; - this.progressMap = new Map(); - this.updateProgress(); - const partialLength = Math.round(fragment.size / this.fragmentSplitFactor); - let startByte = 0; - const getPartNumber = xhr => [...this.progressMap.keys()].indexOf(xhr) + 1; - while (startByte < fragment.size) - { - const endByte = Math.min(fragment.size - 1, Math.round(startByte + partialLength)); - const range = `bytes=${startByte}-${endByte}`; - const rangeLength = endByte - startByte + 1; - promises.push(new Promise((resolve, reject) => - { - const xhr = new XMLHttpRequest(); - xhr.open("GET", fragment.url); - xhr.responseType = "arraybuffer"; - xhr.withCredentials = false; - xhr.addEventListener("progress", (e) => - { - console.log(`[下载视频] 视频片段${getPartNumber(xhr)}下载进度: ${e.loaded}/${rangeLength} bytes loaded, ${range}`); - this.progressMap.set(xhr, e.loaded); - this.updateProgress(); - }); - xhr.addEventListener("load", () => - { - if (("" + xhr.status)[0] === "2") - { - resolve(xhr.response); - } - else - { - reject(`请求失败.`); - } - }); - xhr.addEventListener("abort", () => reject("下载已取消.")); - xhr.addEventListener("error", () => - { - console.error(`[下载视频] 视频片段${getPartNumber(xhr)}下载失败: ${range}`); - this.progressMap.set(xhr, 0); - this.updateProgress(); - xhr.open("GET", fragment.url); - xhr.setRequestHeader("Range", range); - xhr.send(); - }); - xhr.setRequestHeader("Range", range); - this.progressMap.set(xhr, 0); - xhr.send(); - this.workingXhr.push(xhr); - })); - startByte = Math.round(startByte + partialLength) + 1; - } - return Promise.all(promises); + } + extension (fragment) { + return (fragment || this.fragments[0]).url + .indexOf('.flv') !== -1 + ? '.flv' + : '.mp4' + } + makeBlob (data, fragment = null) { + return new Blob(Array.isArray(data) ? data : [data], { + type: this.extension(fragment) === '.flv' ? 'video/x-flv' : 'video/mp4' + }) + } + cleanUpOldBlobUrl () { + const oldBlobUrl = document.querySelector('a#video-complete').getAttribute('href') + if (oldBlobUrl && !document.querySelector(`.link[href="${oldBlobUrl}"]`)) { + URL.revokeObjectURL(oldBlobUrl) } - copyUrl() - { - const urls = this.fragments.map(it => it.url).reduce((acc, it) => acc + "\r\n" + it); - GM_setClipboard(urls, "text"); + [...document.querySelectorAll('.toast-card-header')].filter(it => it.innerText.includes('下载视频')).forEach(it => it.querySelector('.toast-card-dismiss').click()) + } + downloadSingle (downloadedData) { + const [data] = downloadedData + const blob = this.makeBlob(data) + const filename = getFriendlyTitle() + this.extension() + return [blob, filename] + } + async downloadMultiple (downloadedData) { + const zip = new JSZip() + const title = getFriendlyTitle() + if (downloadedData.length > 1) { + downloadedData.forEach((data, index) => { + const fragment = this.fragments[index] + zip.file(`${title} - ${index + 1}${this.extension(fragment)}`, this.makeBlob(data, fragment)) + }) + } else { + const [data] = downloadedData + zip.file(`${title}${this.extension()}`, this.makeBlob(data)) } - exportData(copy = false) - { - const data = JSON.stringify([{ - fragments: this.fragments, - title: getFriendlyTitle(true), - totalSize: this.fragments.map(it => it.size).reduce((acc, it) => acc + it), - }]); - if (copy) - { - GM_setClipboard(data, "text"); - } - else - { - const a = document.createElement("a"); - const blob = new Blob([data], { type: "text/json" }); - const url = URL.createObjectURL(blob); - a.setAttribute("href", url); - a.setAttribute("download", `cid${unsafeWindow.cid}.json`); - document.body.appendChild(a); - a.click(); - a.remove(); - URL.revokeObjectURL(url); - } + const blob = await zip.generateAsync({ type: 'blob' }) + const filename = title + '.zip' + return [blob, filename] + } + async download () { + const downloadedData = [] + this.totalSize = this.fragments.map(it => it.size).reduce((acc, it) => acc + it) + for (const fragment of this.fragments) { + const data = await this.downloadFragment(fragment) + downloadedData.push(data) } - extension(fragment) - { - return (fragment || this.fragments[0]).url - .indexOf(".flv") !== -1 - ? ".flv" - : ".mp4"; + if (downloadedData.length < 1) { + throw new Error('下载失败.') } - makeBlob(data, fragment = null) - { - return new Blob(Array.isArray(data) ? data : [data], { - type: this.extension(fragment) === ".flv" ? "video/x-flv" : "video/mp4" - }); - } - cleanUpOldBlobUrl() - { - const oldBlobUrl = document.querySelector("a#video-complete").getAttribute("href"); - if (oldBlobUrl && !document.querySelector(`.link[href="${oldBlobUrl}"]`)) - { - URL.revokeObjectURL(oldBlobUrl); - } - [...document.querySelectorAll(".toast-card-header")].filter(it => it.innerText.includes("下载视频")).forEach(it => it.querySelector(".toast-card-dismiss").click()); - } - downloadSingle(downloadedData) - { - const [data] = downloadedData; - const blob = this.makeBlob(data); - const filename = getFriendlyTitle() + this.extension(); - return [blob, filename]; - } - async downloadMultiple(downloadedData) - { - const zip = new JSZip(); - const title = getFriendlyTitle(); - if (downloadedData.length > 1) - { - downloadedData.forEach((data, index) => - { - const fragment = this.fragments[index]; - zip.file(`${title} - ${index + 1}${this.extension(fragment)}`, this.makeBlob(data, fragment)); - }); - } - else - { - const [data] = downloadedData; - zip.file(`${title}${this.extension()}`, this.makeBlob(data)); - } - const blob = await zip.generateAsync({ type: "blob" }); - const filename = title + ".zip"; - return [blob, filename]; - } - async download() - { - const downloadedData = []; - this.totalSize = this.fragments.map(it => it.size).reduce((acc, it) => acc + it); - for (const fragment of this.fragments) - { - const data = await this.downloadFragment(fragment); - downloadedData.push(data); - } - if (downloadedData.length < 1) - { - throw new Error("下载失败."); - } - let blob = null; - let filename = null; - if (downloadedData.length === 1) - { - [blob, filename] = this.downloadSingle(downloadedData); - } - else - { - [blob, filename] = await this.downloadMultiple(downloadedData); - } - - this.cleanUpOldBlobUrl(); - const blobUrl = URL.createObjectURL(blob); - this.progress && this.progress(0); - return { - url: blobUrl, - filename: filename - }; + let blob = null + let filename = null + if (downloadedData.length === 1) { + [blob, filename] = this.downloadSingle(downloadedData) + } else { + [blob, filename] = await this.downloadMultiple(downloadedData) } + + this.cleanUpOldBlobUrl() + const blobUrl = URL.createObjectURL(blob) + this.progress && this.progress(0) + return { + url: blobUrl, + filename: filename + } + } } -async function checkBatch() -{ - const urls = [ - "/www.bilibili.com/bangumi", - "/www.bilibili.com/video/av", - ]; - if (!urls.some(url => document.URL.includes(url))) - { - return; +async function checkBatch () { + const urls = [ + '/www.bilibili.com/bangumi', + '/www.bilibili.com/video/av' + ] + if (!urls.some(url => document.URL.includes(url))) { + return + } + const { BatchExtractor } = await import('batchDownload') + if (await BatchExtractor.test() !== true) { + return + } + const extractor = new BatchExtractor() + document.getElementById('download-video').classList.add('batch') + document.getElementById('video-action-batch-data').addEventListener('click', async () => { + if (!selectedFormat) { + return } - const { BatchExtractor } = await import("batchDownload"); - if (await BatchExtractor.test() !== true) - { - return; + pageData.entity.resetMenuClass() + const toast = Toast.info('获取链接中...', '批量下载') + const data = await extractor.collectData(selectedFormat, toast) + if (!data) { + return + } + GM_setClipboard(data, { type: 'text/json' }) + Toast.success('已复制批量数据到剪贴板.', '复制批量数据', 3000) + }) + document.getElementById('video-action-batch-download-data').addEventListener('click', async () => { + if (!selectedFormat) { + return + } + pageData.entity.resetMenuClass() + const toast = Toast.info('获取链接中...', '批量下载') + const data = await extractor.collectData(selectedFormat, toast) + if (!data) { + return } - const extractor = new BatchExtractor(); - document.getElementById("download-video").classList.add("batch"); - document.getElementById("video-action-batch-data").addEventListener("click", async () => - { - if (!selectedFormat) - { - return; - } - pageData.entity.resetMenuClass(); - const toast = Toast.info("获取链接中...", "批量下载"); - const data = await extractor.collectData(selectedFormat, toast); - if (!data) - { - return; - } - GM_setClipboard(data, { type: "text/json" }); - Toast.success("已复制批量数据到剪贴板.", "复制批量数据", 3000); - }); - document.getElementById("video-action-batch-download-data").addEventListener("click", async () => - { - if (!selectedFormat) - { - return; - } - pageData.entity.resetMenuClass(); - const toast = Toast.info("获取链接中...", "批量下载"); - const data = await extractor.collectData(selectedFormat, toast); - if (!data) - { - return; - } - const a = document.createElement("a"); - const blob = new Blob([data], { type: "text/json" }); - const url = URL.createObjectURL(blob); - a.setAttribute("href", url); - a.setAttribute("download", `export.json`); - document.body.appendChild(a); - a.click(); - a.remove(); - URL.revokeObjectURL(url); - }); + const a = document.createElement('a') + const blob = new Blob([data], { type: 'text/json' }) + const url = URL.createObjectURL(blob) + a.setAttribute('href', url) + a.setAttribute('download', `export.json`) + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) + }) } -async function loadPageData() -{ - const aid = await SpinQuery.select(() => (unsafeWindow || window).aid); - const cid = await SpinQuery.select(() => (unsafeWindow || window).cid); - pageData.aid = aid; - pageData.cid = cid; - if (document.URL.indexOf("bangumi") !== -1) - { - pageData.entity = new Bangumi(); - } - else - { - pageData.entity = new Video(); - } - try - { - formats = await VideoFormat.availableFormats; - } - catch (error) - { - return false; - } - return Boolean(aid && cid); +async function loadPageData () { + const aid = await SpinQuery.select(() => (unsafeWindow || window).aid) + const cid = await SpinQuery.select(() => (unsafeWindow || window).cid) + pageData.aid = aid + pageData.cid = cid + if (document.URL.indexOf('bangumi') !== -1) { + pageData.entity = new Bangumi() + } else { + pageData.entity = new Video() + } + try { + formats = await VideoFormat.availableFormats + } catch (error) { + return false + } + return Boolean(aid && cid) } -async function loadWidget() -{ - selectedFormat = formats[0]; - const loadQualities = async () => - { - const canDownload = await loadPageData(); - document.querySelector("#download-video").style.display = canDownload ? "flex" : "none"; - if (canDownload === false) - { - return; - } - // formats = await VideoFormat.availableFormats; - - const list = document.querySelector("ol.video-quality"); - list.childNodes.forEach(list.removeChild); - formats.forEach(format => - { - const item = document.createElement("li"); - item.innerHTML = format.displayName; - item.addEventListener("click", () => - { - selectedFormat = format; - pageData.entity.nextMenuClass(); - }); - list.insertAdjacentElement("afterbegin", item); - }); - }; - Observer.videoChange(loadQualities); - const getVideoInfo = () => selectedFormat.downloadInfo().catch(error => - { - pageData.entity.addError(); - $(".video-error").text(error); - }); - async function download() - { - if (!selectedFormat) - { - return; - } - pageData.entity.nextMenuClass(); - const info = await getVideoInfo(); - info.progress = percent => - { - $(".download-progress-value").text(`${fixed(percent * 100)}`); - $(".download-progress-foreground").css("transform", `scaleX(${percent})`); - }; - document.querySelector(".download-progress-cancel>span").onclick = () => info.cancelDownload(); - const result = await info.download() - .catch(error => - { - pageData.entity.addError(); - $(".video-error").text(error); - }); - if (!result) // canceled or other errors - { - return; - } - const completeLink = document.getElementById("video-complete"); - completeLink.setAttribute("href", result.url); - completeLink.setAttribute("download", result.filename); - completeLink.click(); - - const message = `下载完成. 再次保存`; - Toast.success(message, "下载视频"); - pageData.entity.resetMenuClass(); +async function loadWidget () { + selectedFormat = formats[0] + const loadQualities = async () => { + const canDownload = await loadPageData() + document.querySelector('#download-video').style.display = canDownload ? 'flex' : 'none' + if (canDownload === false) { + return } - async function copyLink() - { - if (!selectedFormat) - { - return; - } - const info = await getVideoInfo(); - info.copyUrl(); - Toast.success("已复制链接到剪贴板.", "复制链接", 3000); - pageData.entity.resetMenuClass(); + // formats = await VideoFormat.availableFormats; + + const list = document.querySelector('ol.video-quality') + list.childNodes.forEach(list.removeChild) + formats.forEach(format => { + const item = document.createElement('li') + item.innerHTML = format.displayName + item.addEventListener('click', () => { + selectedFormat = format + pageData.entity.nextMenuClass() + }) + list.insertAdjacentElement('afterbegin', item) + }) + } + Observer.videoChange(loadQualities) + const getVideoInfo = () => selectedFormat.downloadInfo().catch(error => { + pageData.entity.addError() + $('.video-error').text(error) + }) + async function download () { + if (!selectedFormat) { + return } - document.querySelector("#video-action-download").addEventListener("click", download); - document.querySelector("#video-action-copy").addEventListener("click", copyLink); - document.querySelector("#video-action-copy-data").addEventListener("click", async () => - { - if (!selectedFormat) - { - return; - } - const info = await getVideoInfo(); - info.exportData(true); - Toast.success("已复制数据到剪贴板.", "复制数据", 3000); - pageData.entity.resetMenuClass(); - }); - document.querySelector("#video-action-download-data").addEventListener("click", async () => - { - if (!selectedFormat) - { - return; - } - const info = await getVideoInfo(); - info.exportData(false); - pageData.entity.resetMenuClass(); - }); - resources.applyStyle("downloadVideoStyle"); - const downloadPanel = document.querySelector(".download-video-panel"); - const togglePopup = () => $(".download-video-panel").toggleClass("opened"); - document.querySelector("#download-video").addEventListener("click", e => - { - if (!downloadPanel.contains(e.target)) - { - togglePopup(); - } - }); - document.querySelector(".video-error").addEventListener("click", () => - { - document.querySelector(".video-error").innerHTML = ""; - pageData.entity.removeError(); - }); - await SpinQuery.select(".download-video-panel"); - pageData.entity.addMenuClass(); - checkBatch(); + pageData.entity.nextMenuClass() + const info = await getVideoInfo() + info.progress = percent => { + $('.download-progress-value').text(`${fixed(percent * 100)}`) + $('.download-progress-foreground').css('transform', `scaleX(${percent})`) + } + document.querySelector('.download-progress-cancel>span').onclick = () => info.cancelDownload() + const result = await info.download() + .catch(error => { + pageData.entity.addError() + $('.video-error').text(error) + }) + if (!result) { // canceled or other errors + return + } + const completeLink = document.getElementById('video-complete') + completeLink.setAttribute('href', result.url) + completeLink.setAttribute('download', result.filename) + completeLink.click() + + const message = `下载完成. 再次保存` + Toast.success(message, '下载视频') + pageData.entity.resetMenuClass() + } + async function copyLink () { + if (!selectedFormat) { + return + } + const info = await getVideoInfo() + info.copyUrl() + Toast.success('已复制链接到剪贴板.', '复制链接', 3000) + pageData.entity.resetMenuClass() + } + document.querySelector('#video-action-download').addEventListener('click', download) + document.querySelector('#video-action-copy').addEventListener('click', copyLink) + document.querySelector('#video-action-copy-data').addEventListener('click', async () => { + if (!selectedFormat) { + return + } + const info = await getVideoInfo() + info.exportData(true) + Toast.success('已复制数据到剪贴板.', '复制数据', 3000) + pageData.entity.resetMenuClass() + }) + document.querySelector('#video-action-download-data').addEventListener('click', async () => { + if (!selectedFormat) { + return + } + const info = await getVideoInfo() + info.exportData(false) + pageData.entity.resetMenuClass() + }) + resources.applyStyle('downloadVideoStyle') + const downloadPanel = document.querySelector('.download-video-panel') + const togglePopup = () => $('.download-video-panel').toggleClass('opened') + document.querySelector('#download-video').addEventListener('click', e => { + if (!downloadPanel.contains(e.target)) { + togglePopup() + } + }) + document.querySelector('.video-error').addEventListener('click', () => { + document.querySelector('.video-error').innerHTML = '' + pageData.entity.removeError() + }) + await SpinQuery.select('.download-video-panel') + pageData.entity.addMenuClass() + checkBatch() } export default { - widget: - { - content: resources.data.downloadVideoHtml.text, - condition: loadPageData, - success: loadWidget, - }, -}; \ No newline at end of file + widget: + { + content: resources.data.downloadVideoHtml.text, + condition: loadPageData, + success: loadWidget + } +}