mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Refactor VideoDownloader
This commit is contained in:
parent
4d23654940
commit
cd96cdfce2
2
@types/global/index.d.ts
vendored
2
@types/global/index.d.ts
vendored
@ -35,6 +35,7 @@ declare global {
|
||||
}
|
||||
type RunAtOptions = "document-start" | "document-end" | "document-idle" | "document-body" | "context-menu";
|
||||
type DanmakuOption = '无' | 'XML' | 'ASS'
|
||||
type FfmpegOption = '无' | '文件列表' | '文件列表+脚本'
|
||||
type Pattern = string
|
||||
interface RpcOption {
|
||||
secretKey: string
|
||||
@ -369,6 +370,7 @@ declare global {
|
||||
customNavbarSeasonLogo: boolean,
|
||||
selectableColumnText: boolean,
|
||||
downloadVideoFormat: 'flv' | 'dash',
|
||||
downloadVideoFfmpegSupport: FfmpegOption,
|
||||
enableDashDownload: boolean,
|
||||
watchlaterExpireWarnings: boolean,
|
||||
watchlaterExpireWarningDays: number,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -44,10 +44,10 @@
|
||||
"double-click-fullscreen.min.js": "09C035FC7E281ABB042A4200F69757AB093079DBCC9939BE8C53C23EE2EDE41C",
|
||||
"download-audio.min.js": "1F4536DE4FF302BFD026CA17E028AF8EB037FB6D0CB355692549DECAEAE0E8C9",
|
||||
"download-danmaku.min.js": "B254644525ABF064DB770FE986D2AB88063406783EC0538004EFBE0E2B2ACD62",
|
||||
"download-video-package.min.js": "13FE4E54C20669A6726DAFDA4D9433D8AE495530494E5C8A989FB6EDC28D7F85",
|
||||
"download-video-package.min.js": "9460B7FC79E4F7A2CCC35F5CC9B01A5D7476BAC9833BAD15917C6806C488E17E",
|
||||
"download-video.min.css": "B570412DD5A8AA9C10020600084CE5EB520E92CDDA03B78450E6480B6A60DB3C",
|
||||
"download-video.min.html": "4311ABAF2886EB02B1003EA864DD605B113CDB05FF04C3A6882AD931F320D216",
|
||||
"download-video.min.js": "91131BF338135AC256B8F743A33DE586A87EBC461D436A0E59107607C14424A5",
|
||||
"download-video.min.html": "08F44F7F2A6C7680D89F229CA9A99FFBCDBE2CC79C912AD6432750FC631E667E",
|
||||
"download-video.min.js": "6CAC0A0EFB6B3C196027D8338B50334A1BE1D02B953536BC16D7AEE9C507DB4B",
|
||||
"dpi-img.vue.min.js": "3D9D93079365DCABA8DC8970F051AD1A7F25C3EFFF850CD1000C4312E9B910DD",
|
||||
"expand-danmaku.min.js": "B21658C40085AEA8DC49652AE62EB8610BB2EBEAF7A9C9AF69EF3B11E08EEC8D",
|
||||
"expand-description.min.css": "58C7710A50521B80F7D872BDC4C652610D84C4FABC6874BA66DA37B4F8759224",
|
||||
@ -167,7 +167,7 @@
|
||||
"v-dropdown.vue.min.js": "D26B4A74515F53A34A3CB0590FC9683476D99FBBBB9647FDFCBF33E265C3F662",
|
||||
"video-card-info.min.js": "13B755C0EE0CA018AE65251E168BAF3395FEA2EB4FAD6949AADD752F61B8819B",
|
||||
"video-card.vue.min.js": "340436CDE3B102F467F671411D4373E0DBF59C5A72867D18BDF45BA6C66156C2",
|
||||
"video-dash.min.js": "AB61CB28867B75A472CA6450046F6F10862BD1D8D8D206A3AAB143CDCE27B8A0",
|
||||
"video-dash.min.js": "57EEF7B547E9BD2A84338F89869D4F5DAF8A08C9F372C915E8F68E4A2B51CF43",
|
||||
"video-downloader-fragment.min.js": "13B755C0EE0CA018AE65251E168BAF3395FEA2EB4FAD6949AADD752F61B8819B",
|
||||
"video-info.min.js": "7234A74CA28A772A31E41BBD4925FC2B22C6701419ADFD511402C6A9D6D9E5FA",
|
||||
"video-list.vue.min.js": "BC1DB652C4CD8E98AC620E74ED306AACE0CD99359EE472BFD2A846ADCA37AD0B",
|
||||
|
||||
BIN
min/bundle.zip
BIN
min/bundle.zip
Binary file not shown.
2
min/download-video-package.min.js
vendored
2
min/download-video-package.min.js
vendored
@ -1 +1 @@
|
||||
(()=>{return(e,t)=>{class n{constructor(e={}){this.config=e;this.entries=[]}download(e,t){const n=document.createElement("a");const i=URL.createObjectURL(t);n.setAttribute("href",i);n.setAttribute("download",e);document.body.appendChild(n);n.click();n.remove();URL.revokeObjectURL(i)}add(e,t){if(t===null||t===undefined){return}this.entries.push({name:e,data:t})}async emit(e){if(this.entries.length===0){return}if(!e||this.entries.length===1){e=this.entries[0].name}if(this.entries.length===1){const t=this.entries[0].data;this.download(e,typeof t==="string"?new Blob([t]):t);return}const t=new JSZip;this.entries.forEach(({name:e,data:n})=>{t.file(e,n)});const n=await t.generateAsync({type:"blob"});this.download(e,n)}static async single(e,t){const i=new n;i.add(e,t);await i.emit()}}return{export:{DownloadVideoPackage:n}}}})();
|
||||
(()=>{return(e,t)=>{class n{constructor(e={}){this.config=e;this.entries=[]}download(e,t){const i=document.createElement("a");const a=URL.createObjectURL(t);if(n.lastPackageUrl){URL.revokeObjectURL(n.lastPackageUrl)}n.lastPackageUrl=a;i.setAttribute("href",a);i.setAttribute("download",e);document.body.appendChild(i);i.click();i.remove()}add(e,t){if(t===null||t===undefined){return}this.entries.push({name:e,data:t})}async preEmit(){const e=this.entries.filter(e=>[".flv",".mp4",".m4s"].some(t=>e.name.endsWith(t)));if(this.config.ffmpeg!==undefined&&e.length>=2){if(this.config.ffmpeg==="文件列表"||this.config.ffmpeg==="文件列表+脚本"){this.entries.push({name:"ffmpeg-files.txt",data:e.map(e=>`file '${e.name}'`).join("\n")})}if(this.config.ffmpeg==="文件列表+脚本"){const e=`ffmpeg -f concat -i ffmpeg-files.txt -c copy ""`;this.entries.push({name:"ffmpeg.bat",data:e},{name:"ffmpeg.sh",data:e.replace(/"/g,"'")})}}}async blob(){await this.preEmit();if(this.entries.length===0){return null}if(this.entries.length===1){const e=this.entries[0].data;return typeof e==="string"?new Blob([e]):e}const e=new JSZip;this.entries.forEach(({name:t,data:n})=>{e.file(t,n)});return await e.generateAsync({type:"blob"})}async emit(e){if(!e||this.entries.length===1){e=this.entries[0].name}const t=await this.blob();if(!t){return}return this.download(e,t)}static async single(e,t,i={}){const a=new n(i);a.add(e,t);return await a.emit()}}n.lastPackageUrl="";return{export:{DownloadVideoPackage:n}}}})();
|
||||
@ -1 +1 @@
|
||||
<div class=download-video><a v-bind:href=blobUrl id=video-complete style="display: none"></a><div class=header><h1>下载视频</h1><i class="mdi mdi-close"v-on:click=close()></i></div><div v-if=batch class=tabs><div class="tab download-single"v-bind:class="{active: downloadSingle}"v-on:click="downloadSingle = true">单个视频</div><div class="tab download-batch"v-bind:class="{active: !downloadSingle}"v-on:click="downloadSingle = false">批量导出</div></div><div v-show=downloadSingle class=info><img v-bind:src=coverUrl class=cover><div class=title><span class=size><div v-if=sizeWarning class=size-warning><i class="mdi mdi-alert-circle"></i><div class=size-warning-tip>警告: 过大的视频大小会在直接下载时占用大量内存, 并可能导致浏览器标签页崩溃. 请考虑降低清晰度或使用导出选项.</div></div>预计大小: {{displaySize}}</span></div></div><div class=options><div class=option-item v-if=enableDash>格式<v-dropdown style="text-transform: uppercase;"v-on:change=dashChange() v-bind:items=dashModel.items v-bind:value.sync=dashModel.value></v-dropdown></div><div class=option-item>清晰度<v-dropdown v-on:change=formatChange() v-bind:items=qualityModel.items v-bind:value.sync=qualityModel.value></v-dropdown></div><div class=option-item>弹幕<v-dropdown v-on:change=danmakuOptionChange() v-bind:items=danmakuModel.items v-bind:value.sync=danmakuModel.value></v-dropdown></div></div><div class=separator></div><div v-show=downloadSingle class=direct-download><div class=direct-download-header><h2>直接下载</h2><span class=download-speed>{{speed}}</span></div><button v-if=!downloading class="primary start-download"v-on:click=startDownload()>开始</button><button v-else class="primary cancel-download"v-on:click=cancelDownload()>取消</button><div class=progress><div class=background><div class=foreground v-bind:style="{width: progressPercent + '%'}"></div></div><span class=percent>{{progressPercent}}%</span></div></div><div v-show=!downloadSingle class=batch-download><div class=episode-header><h2>选集</h2><button class=list-tool title=全选 v-on:click=selectAllEpisodes()><i class="mdi mdi-checkbox-multiple-marked-circle"></i></button><button class=list-tool title=全不选 v-on:click=unselectAllEpisodes()><i class="mdi mdi-checkbox-multiple-blank-circle-outline"></i></button><button class=list-tool title=反选 v-on:click=inverseAllEpisodes()><i class="mdi mdi-circle-slice-4"></i></button><span class=selected-count>{{selectedEpisodeCount}}/{{episodeList.length}}</span></div><div class=episode-list><v-checkbox v-for="ep of episodeList"v-bind:key=ep.index v-bind:title=ep.title v-bind:checked.sync=ep.checked></v-checkbox></div></div><div class=separator></div><div class=exports><h2>导出</h2><div class=actions v-bind:class="{busy: busy}"><button class=show-link v-bind:disabled="downloadSingle ? null : 'disabled'"v-on:click="exportData('showLink')">显示链接</button><button class=aria2-file v-on:click="exportData('aria2')">aria2</button><div class="button aria2-rpc"v-on:click.self=toggleRpcSettings()>{{showRpcSettings ? '取消' : 'aria2 RPC'}}<i :class="{'mdi-close': showRpcSettings, 'mdi-chevron-right': !showRpcSettings}"class=mdi v-on:click.self=toggleRpcSettings()></i><div class=rpc-settings v-bind:class="{show: showRpcSettings}"><h1>aria2 RPC</h1><rpc-profiles @profile-change=updateProfile></rpc-profiles><h2>配置</h2><div class=rpc-settings-item>主机<input type=text v-model=rpcSettings.host placeholder=127.0.0.1></div><div class=rpc-settings-item>端口<input type=text v-model=rpcSettings.port placeholder=6800></div><div class=rpc-settings-item>密钥<input type=text v-model=rpcSettings.secretKey></div><div class=rpc-settings-item>限速<input type=text v-model=rpcSettings.maxDownloadLimit placeholder=无></div><div class=rpc-settings-item>默认路径<input type=text v-model=rpcSettings.baseDir></div><div class=rpc-settings-item>路径<input type=text v-model=rpcSettings.dir></div><div class=final-dir>最终路径: {{rpcSettings.baseDir + rpcSettings.dir}}</div><div class=rpc-settings-item>方法<v-dropdown style="text-transform: uppercase;"v-bind:items="['get', 'post']"v-bind:value.sync=rpcSettings.method></v-dropdown></div><div class=operations><div class="primary button"v-on:click="saveRpcSettings();toggleRpcSettings();exportData('aria2RPC')">开始下载</div><div class=button v-on:click=saveRpcSettings()>{{saveRpcSettingsText}}</div></div></div></div></div></div></div>
|
||||
<div class=download-video><a v-bind:href=blobUrl id=video-complete style="display: none"></a><div class=header><h1>下载视频</h1><i class="mdi mdi-close"v-on:click=close()></i></div><div v-if=batch class=tabs><div class="tab download-single"v-bind:class="{active: downloadSingle}"v-on:click="downloadSingle = true">单个视频</div><div class="tab download-batch"v-bind:class="{active: !downloadSingle}"v-on:click="downloadSingle = false">批量导出</div></div><div v-show=downloadSingle class=info><img v-bind:src=coverUrl class=cover><div class=title><span class=size><div v-if=sizeWarning class=size-warning><i class="mdi mdi-alert-circle"></i><div class=size-warning-tip>警告: 过大的视频大小会在直接下载时占用大量内存, 并可能导致浏览器标签页崩溃. 请考虑降低清晰度或使用导出选项.</div></div>预计大小: {{displaySize}}</span></div></div><div class=options><div class=option-item v-if=enableDash>格式<v-dropdown style="text-transform: uppercase;"v-on:change=dashChange() v-bind:items=dashModel.items v-bind:value.sync=dashModel.value></v-dropdown></div><div class=option-item>清晰度<v-dropdown v-on:change=formatChange() v-bind:items=qualityModel.items v-bind:value.sync=qualityModel.value></v-dropdown></div><div class=option-item>弹幕<v-dropdown v-on:change=danmakuOptionChange() v-bind:items=danmakuModel.items v-bind:value.sync=danmakuModel.value></v-dropdown></div><div class=option-item v-if=enableDash>ffmpeg支持<v-dropdown v-on:change=ffmpegChange() v-bind:items=ffmpegModel.items v-bind:value.sync=ffmpegModel.value></v-dropdown></div></div><div class=separator></div><div v-show=downloadSingle class=direct-download><div class=direct-download-header><h2>直接下载</h2><span class=download-speed>{{speed}}</span></div><button v-if=!downloading class="primary start-download"v-on:click=startDownload()>开始</button><button v-else class="primary cancel-download"v-on:click=cancelDownload()>取消</button><div class=progress><div class=background><div class=foreground v-bind:style="{width: progressPercent + '%'}"></div></div><span class=percent>{{progressPercent}}%</span></div></div><div v-show=!downloadSingle class=batch-download><div class=episode-header><h2>选集</h2><button class=list-tool title=全选 v-on:click=selectAllEpisodes()><i class="mdi mdi-checkbox-multiple-marked-circle"></i></button><button class=list-tool title=全不选 v-on:click=unselectAllEpisodes()><i class="mdi mdi-checkbox-multiple-blank-circle-outline"></i></button><button class=list-tool title=反选 v-on:click=inverseAllEpisodes()><i class="mdi mdi-circle-slice-4"></i></button><span class=selected-count>{{selectedEpisodeCount}}/{{episodeList.length}}</span></div><div class=episode-list><v-checkbox v-for="ep of episodeList"v-bind:key=ep.index v-bind:title=ep.title v-bind:checked.sync=ep.checked></v-checkbox></div></div><div class=separator></div><div class=exports><h2>导出</h2><div class=actions v-bind:class="{busy: busy}"><button class=show-link v-bind:disabled="downloadSingle ? null : 'disabled'"v-on:click="exportData('showLink')">显示链接</button><button class=aria2-file v-on:click="exportData('aria2')">aria2</button><div class="button aria2-rpc"v-on:click.self=toggleRpcSettings()>{{showRpcSettings ? '取消' : 'aria2 RPC'}}<i :class="{'mdi-close': showRpcSettings, 'mdi-chevron-right': !showRpcSettings}"class=mdi v-on:click.self=toggleRpcSettings()></i><div class=rpc-settings v-bind:class="{show: showRpcSettings}"><h1>aria2 RPC</h1><rpc-profiles @profile-change=updateProfile></rpc-profiles><h2>配置</h2><div class=rpc-settings-item>主机<input type=text v-model=rpcSettings.host placeholder=127.0.0.1></div><div class=rpc-settings-item>端口<input type=text v-model=rpcSettings.port placeholder=6800></div><div class=rpc-settings-item>密钥<input type=text v-model=rpcSettings.secretKey></div><div class=rpc-settings-item>限速<input type=text v-model=rpcSettings.maxDownloadLimit placeholder=无></div><div class=rpc-settings-item>默认路径<input type=text v-model=rpcSettings.baseDir></div><div class=rpc-settings-item>路径<input type=text v-model=rpcSettings.dir></div><div class=final-dir>最终路径: {{rpcSettings.baseDir + rpcSettings.dir}}</div><div class=rpc-settings-item>方法<v-dropdown style="text-transform: uppercase;"v-bind:items="['get', 'post']"v-bind:value.sync=rpcSettings.method></v-dropdown></div><div class=operations><div class="primary button"v-on:click="saveRpcSettings();toggleRpcSettings();exportData('aria2RPC')">开始下载</div><div class=button v-on:click=saveRpcSettings()>{{saveRpcSettingsText}}</div></div></div></div></div></div></div>
|
||||
2
min/download-video.min.js
vendored
2
min/download-video.min.js
vendored
File diff suppressed because one or more lines are too long
2
min/video-dash.min.js
vendored
2
min/video-dash.min.js
vendored
@ -1 +1 @@
|
||||
(()=>{return(t,a)=>{const d=t=>{return{url:t.downloadUrl,backupUrls:t.backupUrls,length:t.duration,size:Math.trunc(t.bandWidth*t.duration/8)}};const r=async(t,a)=>{const d=await Ajax.getJsonWithCredentials(t);const r=d.data||d.result||d;if(d.code!==0||!r.dash){throw new Error("DASH api failed")}const e=r.accept_quality;if(!e.includes(a)){throw new Error("没有找到请求的清晰度")}if(r.quality!==a){throw new Error("无法获取请求的清晰度, 请确认当前账号有相应的权限")}const o=r.accept_description;const n=o[e.indexOf(a)];const c=r.dash.duration;const i=r.dash.video.filter(t=>t.id===a).map(t=>{const d={quality:a,qualityText:n,width:t.width,height:t.height,codecs:t.codecs,codecId:t.codecid,bandWidth:t.bandwidth,frameRate:t.frameRate,backupUrls:t.backupUrl,downloadUrl:t.baseUrl,duration:c};return d});const s=r.dash.audio.map(t=>{return{bandWidth:t.bandwidth,codecs:t.codecs,codecId:t.codecid,backupUrls:t.backupUrl,downloadUrl:t.baseUrl,duration:c}});return{videoDashes:i,audioDashes:s}};return{export:{getDashInfo:r,dashToFragment:d}}}})();
|
||||
(()=>{return(t,a)=>{const r=t=>{return{url:t.downloadUrl,backupUrls:t.backupUrls,length:t.duration,size:Math.trunc(t.bandWidth*t.duration/8)}};const e=async(t,a)=>{const r=await Ajax.getJsonWithCredentials(t);const e=r.data||r.result||r;if(r.code!==0||!e.dash){throw new Error("DASH api failed")}const d=e.accept_quality;if(!d.includes(a)){throw new Error("没有找到请求的清晰度")}if(e.quality!==a){throw new Error("无法获取请求的清晰度, 请确认当前账号有相应的权限")}const c=e.accept_description;const o=c[d.indexOf(a)];const s=e.dash.duration;const n=e.dash.video.filter(t=>t.id===a).map(t=>{const r={quality:a,qualityText:o,width:t.width,height:t.height,codecs:t.codecs,codecId:t.codecid,bandWidth:t.bandwidth,frameRate:t.frameRate,backupUrls:(t.backupUrl||t.backup_url||"").replace("http:","https:"),downloadUrl:(t.baseUrl||t.base_url||"").replace("http:","https:"),duration:s};return r});const i=e.dash.audio.map(t=>{return{bandWidth:t.bandwidth,codecs:t.codecs,codecId:t.codecid,backupUrls:(t.backupUrl||t.backup_url||"").replace("http:","https:"),downloadUrl:(t.baseUrl||t.base_url||"").replace("http:","https:"),duration:s}});return{videoDashes:n,audioDashes:i}};return{export:{getDashInfo:e,dashToFragment:r}}}})();
|
||||
@ -286,13 +286,17 @@ Resource.manifest = {
|
||||
useCache: '启用缓存'
|
||||
}
|
||||
},
|
||||
videoDownloadPackage: {
|
||||
path: 'download-video-package.min.js',
|
||||
},
|
||||
downloadVideo: {
|
||||
path: 'download-video.min.js',
|
||||
html: true,
|
||||
style: 'instant',
|
||||
dependencies: ['title', 'videoInfo'],
|
||||
dependencies: ['title', 'videoInfo', 'videoDownloadPackage'],
|
||||
displayNames: {
|
||||
'downloadVideo': '下载视频',
|
||||
'videoDownloadPackage': '下载视频打包器',
|
||||
'batchDownload': '批量下载',
|
||||
'aria2Rpc': 'aria2 RPC',
|
||||
}
|
||||
|
||||
@ -163,6 +163,7 @@ export const settings = {
|
||||
customNavbarSeasonLogo: false,
|
||||
selectableColumnText: true,
|
||||
downloadVideoFormat: 'flv',
|
||||
downloadVideoFfmpegSupport: '无',
|
||||
enableDashDownload: false,
|
||||
watchlaterExpireWarnings: true,
|
||||
watchlaterExpireWarningDays: 14,
|
||||
|
||||
@ -1,49 +1,85 @@
|
||||
export interface DownloadVideoPackageConfig {
|
||||
ffmpeg?: boolean
|
||||
ffmpeg?: FfmpegOption
|
||||
}
|
||||
export class DownloadVideoPackage {
|
||||
entries: {name: string; data: Blob | string}[] = []
|
||||
static lastPackageUrl: string = ''
|
||||
entries: { name: string; data: Blob | string }[] = []
|
||||
constructor(private config: DownloadVideoPackageConfig = {}) {
|
||||
}
|
||||
private download(filename: string, blob: Blob) {
|
||||
const a = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
if (DownloadVideoPackage.lastPackageUrl) {
|
||||
URL.revokeObjectURL(DownloadVideoPackage.lastPackageUrl)
|
||||
}
|
||||
DownloadVideoPackage.lastPackageUrl = url
|
||||
a.setAttribute('href', url)
|
||||
a.setAttribute('download', filename)
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
add(name: string, data: string | Blob | null | undefined) {
|
||||
if (data === null || data === undefined) {
|
||||
return
|
||||
}
|
||||
this.entries.push({name, data})
|
||||
this.entries.push({ name, data })
|
||||
}
|
||||
async emit(filename?: string) {
|
||||
if (this.entries.length === 0) {
|
||||
return
|
||||
private async preEmit() {
|
||||
const videoFiles = this.entries
|
||||
.filter(it => ['.flv', '.mp4', '.m4s']
|
||||
.some(ext => it.name.endsWith(ext)))
|
||||
if (this.config.ffmpeg !== undefined && videoFiles.length >= 2) {
|
||||
if (this.config.ffmpeg === '文件列表' || this.config.ffmpeg === '文件列表+脚本') {
|
||||
this.entries.push({
|
||||
name: 'ffmpeg-files.txt',
|
||||
data: videoFiles.map(it => `file '${it.name}'`).join('\n'),
|
||||
})
|
||||
}
|
||||
if (this.config.ffmpeg === '文件列表+脚本') {
|
||||
const command = `ffmpeg -f concat -i ffmpeg-files.txt -c copy ""`
|
||||
this.entries.push(
|
||||
{
|
||||
name: 'ffmpeg.bat',
|
||||
data: command,
|
||||
},
|
||||
{
|
||||
name: 'ffmpeg.sh',
|
||||
data: command.replace(/"/g, "'"),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!filename || this.entries.length === 1) {
|
||||
filename = this.entries[0].name
|
||||
}
|
||||
async blob(): Promise<Blob | null> {
|
||||
await this.preEmit()
|
||||
if (this.entries.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (this.entries.length === 1) {
|
||||
const data = this.entries[0].data
|
||||
this.download(filename, typeof data === 'string' ? new Blob([data]) : data)
|
||||
return
|
||||
return typeof data === 'string' ? new Blob([data]) : data
|
||||
}
|
||||
const zip = new JSZip()
|
||||
this.entries.forEach(({name, data}) => {
|
||||
this.entries.forEach(({ name, data }) => {
|
||||
zip.file(name, data)
|
||||
})
|
||||
const blob = await zip.generateAsync({type: 'blob'})
|
||||
this.download(filename, blob)
|
||||
return await zip.generateAsync({ type: 'blob' })
|
||||
}
|
||||
static async single(filename: string, data: string | Blob) {
|
||||
const pack = new DownloadVideoPackage()
|
||||
async emit(filename?: string) {
|
||||
if (!filename || this.entries.length === 1) {
|
||||
filename = this.entries[0].name
|
||||
}
|
||||
const blob = await this.blob()
|
||||
if (!blob) {
|
||||
return
|
||||
}
|
||||
return this.download(filename, blob)
|
||||
}
|
||||
static async single(filename: string, data: string | Blob, config: DownloadVideoPackageConfig = {}) {
|
||||
const pack = new DownloadVideoPackage(config)
|
||||
pack.add(filename, data)
|
||||
await pack.emit()
|
||||
return await pack.emit()
|
||||
}
|
||||
}
|
||||
export default {
|
||||
|
||||
@ -41,6 +41,11 @@
|
||||
弹幕
|
||||
<v-dropdown v-on:change="danmakuOptionChange()" v-bind:items="danmakuModel.items" v-bind:value.sync="danmakuModel.value"></v-dropdown>
|
||||
</div>
|
||||
<div class="option-item" v-if="enableDash">
|
||||
ffmpeg支持
|
||||
<v-dropdown v-on:change="ffmpegChange()" v-bind:items="ffmpegModel.items"
|
||||
v-bind:value.sync="ffmpegModel.value"></v-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div v-show="downloadSingle" class="direct-download">
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { getFriendlyTitle, formatTitle } from '../title'
|
||||
import { VideoInfo, DanmakuInfo } from '../video-info'
|
||||
import { VideoDownloaderFragment } from './video-downloader-fragment';
|
||||
import { VideoDownloaderFragment } from './video-downloader-fragment'
|
||||
import { DownloadVideoPackage } from './download-video-package'
|
||||
|
||||
interface PageData {
|
||||
entity: Video
|
||||
@ -39,6 +40,24 @@ class Bangumi extends Video {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 课程, 不知道为什么b站给它起名cheese
|
||||
class Cheese extends Video {
|
||||
constructor(public ep: number | string) { super() }
|
||||
async getDashUrl(quality?: number) {
|
||||
if (quality) {
|
||||
return `https://api.bilibili.com/pugv/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json&ep_id=${this.ep}&fnver=0&fnval=16`
|
||||
} else {
|
||||
return `https://api.bilibili.com/pugv/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&otype=json&ep_id=${this.ep}&fnver=0&fnval=16`
|
||||
}
|
||||
}
|
||||
async getUrl(quality?: number) {
|
||||
if (quality) {
|
||||
return `https://api.bilibili.com/pugv/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json&ep_id=${this.ep}`
|
||||
} else {
|
||||
return `https://api.bilibili.com/pugv/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&otype=json&ep_id=${this.ep}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pageData: PageData = {
|
||||
entity: new Video(),
|
||||
@ -108,7 +127,8 @@ class VideoDownloader {
|
||||
workingXhr: XMLHttpRequest[] | null = null
|
||||
progress: (progress: number) => void
|
||||
progressMap: Map<XMLHttpRequest, number> = new Map()
|
||||
danmakuOption: DanmakuOption
|
||||
danmakuOption: DanmakuOption = '无'
|
||||
ffmpegOption: FfmpegOption = '无'
|
||||
videoSpeed: VideoSpeed
|
||||
|
||||
constructor(format: VideoFormat, fragments?: VideoDownloaderFragment[]) {
|
||||
@ -167,17 +187,17 @@ class VideoDownloader {
|
||||
}
|
||||
}
|
||||
downloadFragment(fragment: VideoDownloaderFragment) {
|
||||
const promises: Promise<any>[] = []
|
||||
const promises: Promise<ArrayBuffer>[] = []
|
||||
this.workingXhr = []
|
||||
this.progressMap = new Map()
|
||||
this.updateProgress()
|
||||
// const partialLength = Math.round(fragment.size / this.fragmentSplitFactor)
|
||||
// 按一定大小分段或许对大视频更好
|
||||
let partialLength: number
|
||||
if (fragment.size <= 96 * 1024 * 1024) { // 小于等于96MB时, 均分为12段 (this.fragmentSplitFactor)
|
||||
if (fragment.size <= 48 * 1024 * 1024) { // 小于等于48MB时, 均分为12段 (this.fragmentSplitFactor)
|
||||
partialLength = fragment.size / this.fragmentSplitFactor
|
||||
} else { // 大于96MB时, 每16MB为一段
|
||||
partialLength = 16 * 1024 * 1024 // 16MB
|
||||
} else { // 大于48MB时, 每4MB为一段
|
||||
partialLength = 4 * 1024 * 1024 // 4MB
|
||||
}
|
||||
let startByte = 0
|
||||
const getPartNumber = (xhr: XMLHttpRequest) => [...this.progressMap.keys()].indexOf(xhr) + 1
|
||||
@ -185,7 +205,7 @@ class VideoDownloader {
|
||||
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<any>((resolve, reject) => {
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', fragment.url)
|
||||
xhr.responseType = 'arraybuffer'
|
||||
@ -262,8 +282,7 @@ class VideoDownloader {
|
||||
} else {
|
||||
const blob = new Blob([data], { type: 'text/json' })
|
||||
const danmaku = await this.downloadDanmaku()
|
||||
const { DownloadVideoPackage } = await import('./download-video-package')
|
||||
const pack = new DownloadVideoPackage()
|
||||
const pack = new DownloadVideoPackage({ ffmpeg: this.ffmpegOption })
|
||||
pack.add(`${getFriendlyTitle()}.json`, blob)
|
||||
pack.add(getFriendlyTitle() + '.' + this.danmakuOption.toLowerCase(), danmaku)
|
||||
await pack.emit(`${getFriendlyTitle()}.zip`)
|
||||
@ -272,13 +291,12 @@ class VideoDownloader {
|
||||
async exportAria2(rpc = false) {
|
||||
if (rpc) { // https://aria2.github.io/manual/en/html/aria2c.html#json-rpc-using-http-get
|
||||
const danmaku = await this.downloadDanmaku()
|
||||
if (danmaku !== null) {
|
||||
const { DownloadVideoPackage } = await import('./download-video-package')
|
||||
DownloadVideoPackage.single(
|
||||
`${getFriendlyTitle()}.${this.danmakuOption === 'ASS' ? 'ass' : 'xml'}`,
|
||||
danmaku
|
||||
)
|
||||
}
|
||||
const pack = new DownloadVideoPackage({ ffmpeg: this.ffmpegOption })
|
||||
pack.add(
|
||||
`${getFriendlyTitle()}.${this.danmakuOption === 'ASS' ? 'ass' : 'xml'}`,
|
||||
danmaku
|
||||
)
|
||||
await pack.emit()
|
||||
const option = settings.aria2RpcOption
|
||||
const params = this.fragments.map((fragment, index) => {
|
||||
let indexNumber = ''
|
||||
@ -326,8 +344,7 @@ ${it.url}
|
||||
`.trim()
|
||||
const blob = new Blob([input], { type: 'text/plain' })
|
||||
const danmaku = await this.downloadDanmaku()
|
||||
const { DownloadVideoPackage } = await import('./download-video-package')
|
||||
const pack = new DownloadVideoPackage()
|
||||
const pack = new DownloadVideoPackage({ ffmpeg: this.ffmpegOption })
|
||||
pack.add(`${getFriendlyTitle()}.txt`, blob)
|
||||
pack.add(getFriendlyTitle() + '.' + this.danmakuOption.toLowerCase(), danmaku)
|
||||
await pack.emit(`${getFriendlyTitle()}.zip`)
|
||||
@ -347,20 +364,20 @@ ${it.url}
|
||||
return '.flv'
|
||||
}
|
||||
}
|
||||
makeBlob(data: any, fragment?: VideoDownloaderFragment) {
|
||||
return new Blob(Array.isArray(data) ? data : [data], {
|
||||
type: this.extension(fragment) === '.flv' ? 'video/x-flv' : 'video/mp4'
|
||||
})
|
||||
}
|
||||
cleanUpOldBlobUrl() {
|
||||
const oldBlobUrl = dq('a#video-complete')!.getAttribute('href')
|
||||
if (oldBlobUrl && !dq(`.link[href="${oldBlobUrl}"]`)) {
|
||||
URL.revokeObjectURL(oldBlobUrl)
|
||||
}
|
||||
dqa('.toast-card-header')
|
||||
.filter((it: HTMLElement) => it.innerText.includes('下载视频'))
|
||||
.forEach((it: HTMLElement) => (it.querySelector('.toast-card-dismiss') as HTMLElement).click())
|
||||
}
|
||||
// makeBlob(data: any, fragment?: VideoDownloaderFragment) {
|
||||
// return new Blob(Array.isArray(data) ? data : [data], {
|
||||
// type: this.extension(fragment) === '.flv' ? 'video/x-flv' : 'video/mp4'
|
||||
// })
|
||||
// }
|
||||
// cleanUpOldBlobUrl() {
|
||||
// const oldBlobUrl = dq('a#video-complete')!.getAttribute('href')
|
||||
// if (oldBlobUrl && !dq(`.link[href="${oldBlobUrl}"]`)) {
|
||||
// URL.revokeObjectURL(oldBlobUrl)
|
||||
// }
|
||||
// dqa('.toast-card-header')
|
||||
// .filter((it: HTMLElement) => it.innerText.includes('下载视频'))
|
||||
// .forEach((it: HTMLElement) => (it.querySelector('.toast-card-dismiss') as HTMLElement).click())
|
||||
// }
|
||||
async downloadDanmaku() {
|
||||
if (this.danmakuOption !== '无') {
|
||||
const danmakuInfo = new DanmakuInfo(pageData.cid)
|
||||
@ -375,44 +392,51 @@ ${it.url}
|
||||
return null
|
||||
}
|
||||
}
|
||||
async downloadSingle(downloadedData: any[]) {
|
||||
const danmaku = await this.downloadDanmaku()
|
||||
const [data] = downloadedData
|
||||
if (danmaku === null) {
|
||||
const blob = this.makeBlob(data)
|
||||
const filename = getFriendlyTitle() + this.extension()
|
||||
return { blob, filename }
|
||||
} else {
|
||||
const zip = new JSZip()
|
||||
zip.file(getFriendlyTitle() + this.extension(), this.makeBlob(data))
|
||||
zip.file(getFriendlyTitle() + '.' + this.danmakuOption.toLowerCase(), danmaku)
|
||||
const blob = await zip.generateAsync({ type: 'blob' })
|
||||
const filename = getFriendlyTitle() + '.zip'
|
||||
return { blob, filename }
|
||||
}
|
||||
}
|
||||
async downloadMultiple(downloadedData: any[]) {
|
||||
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 danmaku = await this.downloadDanmaku()
|
||||
if (danmaku !== null) {
|
||||
zip.file(getFriendlyTitle() + '.' + this.danmakuOption.toLowerCase(), danmaku)
|
||||
}
|
||||
const blob = await zip.generateAsync({ type: 'blob' })
|
||||
const filename = title + '.zip'
|
||||
return { blob, filename }
|
||||
}
|
||||
// async downloadSingle(downloadedData: any[]) {
|
||||
// const danmaku = await this.downloadDanmaku()
|
||||
// const [data] = downloadedData
|
||||
// const pack = new DownloadVideoPackage({ ffmpeg: this.ffmpegOption })
|
||||
// pack.add(getFriendlyTitle() + this.extension(), this.makeBlob(data))
|
||||
// pack.add(getFriendlyTitle() + '.' + this.danmakuOption.toLowerCase(), danmaku)
|
||||
// return {
|
||||
// blob: await pack.blob(),
|
||||
// filename: getFriendlyTitle() + '.zip'
|
||||
// }
|
||||
// // if (danmaku === null) {
|
||||
// // const blob = this.makeBlob(data)
|
||||
// // const filename = getFriendlyTitle() + this.extension()
|
||||
// // return { blob, filename }
|
||||
// // } else {
|
||||
// // const zip = new JSZip()
|
||||
// // zip.file(getFriendlyTitle() + this.extension(), this.makeBlob(data))
|
||||
// // zip.file(getFriendlyTitle() + '.' + this.danmakuOption.toLowerCase(), danmaku)
|
||||
// // const blob = await zip.generateAsync({ type: 'blob' })
|
||||
// // const filename = getFriendlyTitle() + '.zip'
|
||||
// // return { blob, filename }
|
||||
// // }
|
||||
// }
|
||||
// async downloadMultiple(downloadedData: any[]) {
|
||||
// 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 danmaku = await this.downloadDanmaku()
|
||||
// if (danmaku !== null) {
|
||||
// zip.file(getFriendlyTitle() + '.' + this.danmakuOption.toLowerCase(), danmaku)
|
||||
// }
|
||||
// const blob = await zip.generateAsync({ type: 'blob' })
|
||||
// const filename = title + '.zip'
|
||||
// return { blob, filename }
|
||||
// }
|
||||
async download() {
|
||||
const downloadedData = []
|
||||
const downloadedData: ArrayBuffer[][] = []
|
||||
this.videoSpeed.startMeasure()
|
||||
for (const fragment of this.fragments) {
|
||||
const data = await this.downloadFragment(fragment)
|
||||
@ -422,21 +446,41 @@ ${it.url}
|
||||
throw new Error('下载失败.')
|
||||
}
|
||||
|
||||
let { blob, filename } = await (async () => {
|
||||
if (downloadedData.length === 1) {
|
||||
return await this.downloadSingle(downloadedData)
|
||||
// let { blob, filename } = await (async () => {
|
||||
// if (downloadedData.length === 1) {
|
||||
// return await this.downloadSingle(downloadedData)
|
||||
// } else {
|
||||
// return await this.downloadMultiple(downloadedData)
|
||||
// }
|
||||
// })()
|
||||
// this.cleanUpOldBlobUrl()
|
||||
// const blobUrl = URL.createObjectURL(blob)
|
||||
const pack = new DownloadVideoPackage({ffmpeg: this.ffmpegOption})
|
||||
const title = getFriendlyTitle()
|
||||
downloadedData.forEach((data, index) => {
|
||||
let filename: string
|
||||
const fragment = this.fragments[index]
|
||||
if (downloadedData.length > 1) {
|
||||
filename = `${title} - ${index + 1}${this.extension(fragment)}`
|
||||
} else {
|
||||
return await this.downloadMultiple(downloadedData)
|
||||
filename = `${title}${this.extension(fragment)}`
|
||||
}
|
||||
})()
|
||||
this.cleanUpOldBlobUrl()
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
pack.add(filename, new Blob(Array.isArray(data) ? data : [data], {
|
||||
type: this.extension(fragment) === '.flv' ? 'video/x-flv' : 'video/mp4'
|
||||
}))
|
||||
})
|
||||
const danmaku = await this.downloadDanmaku()
|
||||
pack.add(
|
||||
`${getFriendlyTitle()}.${this.danmakuOption === 'ASS' ? 'ass' : 'xml'}`,
|
||||
danmaku
|
||||
)
|
||||
await pack.emit(title + '.zip')
|
||||
this.progress && this.progress(0)
|
||||
this.videoSpeed.stopMeasure()
|
||||
return {
|
||||
url: blobUrl,
|
||||
filename: filename
|
||||
}
|
||||
// return {
|
||||
// url: blobUrl,
|
||||
// filename: filename
|
||||
// }
|
||||
}
|
||||
}
|
||||
class VideoSpeed {
|
||||
@ -471,8 +515,11 @@ async function loadPageData() {
|
||||
}
|
||||
pageData.aid = aid
|
||||
pageData.cid = cid
|
||||
if (document.URL.indexOf('bangumi') !== -1) {
|
||||
if (document.URL.includes('bangumi')) {
|
||||
pageData.entity = new Bangumi()
|
||||
} else if (document.URL.includes('cheese')) {
|
||||
const match = document.URL.match(/cheese\/play\/ep([\d]+)/)!
|
||||
pageData.entity = new Cheese(match[1])
|
||||
} else {
|
||||
pageData.entity = new Video()
|
||||
}
|
||||
@ -530,6 +577,10 @@ async function loadPanel() {
|
||||
value: settings.downloadVideoDefaultDanmaku as DanmakuOption,
|
||||
items: ['无', 'XML', 'ASS'] as DanmakuOption[]
|
||||
},
|
||||
ffmpegModel: {
|
||||
value: settings.downloadVideoFfmpegSupport,
|
||||
items: ['无', '文件列表', '文件列表+脚本']
|
||||
},
|
||||
progressPercent: 0,
|
||||
size: '获取大小中' as number | string,
|
||||
blobUrl: '',
|
||||
@ -542,6 +593,7 @@ async function loadPanel() {
|
||||
busy: false,
|
||||
saveRpcSettingsText: '保存配置',
|
||||
enableDash: settings.enableDashDownload,
|
||||
lastDirectDownloadLink: '',
|
||||
},
|
||||
computed: {
|
||||
displaySize() {
|
||||
@ -665,7 +717,6 @@ async function loadPanel() {
|
||||
return match.checked
|
||||
}
|
||||
const format = this.getFormat()
|
||||
const { DownloadVideoPackage } = await import('./download-video-package')
|
||||
if (this.danmakuModel.value !== '无') {
|
||||
const danmakuToast = Toast.info('下载弹幕中...', '批量导出')
|
||||
const pack = new DownloadVideoPackage()
|
||||
@ -700,7 +751,8 @@ async function loadPanel() {
|
||||
result = await this.batchExtractor.collectAria2(format, toast)
|
||||
await DownloadVideoPackage.single(
|
||||
getFriendlyTitle(false) + '.txt',
|
||||
new Blob([result], { type: 'text/plain' })
|
||||
new Blob([result], { type: 'text/plain' }),
|
||||
{ ffmpeg: this.ffmpegOption }
|
||||
)
|
||||
return
|
||||
case 'aria2RPC':
|
||||
@ -715,7 +767,8 @@ async function loadPanel() {
|
||||
result = await this.batchExtractor.collectData(format, toast)
|
||||
await DownloadVideoPackage.single(
|
||||
getFriendlyTitle(false) + '.json',
|
||||
new Blob([result], { type: 'text/json' })
|
||||
new Blob([result], { type: 'text/json' }),
|
||||
{ ffmpeg: this.ffmpegOption }
|
||||
)
|
||||
return
|
||||
default:
|
||||
@ -767,16 +820,13 @@ async function loadPanel() {
|
||||
const videoDownloader = await format.downloadInfo(this.dash)
|
||||
videoDownloader.videoSpeed.speedUpdate = speed => this.speed = speed
|
||||
videoDownloader.danmakuOption = this.danmakuModel.value
|
||||
videoDownloader.ffmpegOption = this.ffmpegModel.value
|
||||
videoDownloader.progress = percent => {
|
||||
this.progressPercent = Math.trunc(percent * 100)
|
||||
}
|
||||
workingDownloader = videoDownloader
|
||||
const result = await videoDownloader.download()
|
||||
const completeLink = document.getElementById('video-complete') as HTMLAnchorElement
|
||||
completeLink.setAttribute('href', result.url)
|
||||
completeLink.setAttribute('download', result.filename)
|
||||
completeLink.click()
|
||||
Toast.success(/*html*/`下载完成: ${result.filename} <a class="link" href="${result.url}" download="${result.filename.replace(/"/g, '"')}">再次保存</a>`, '下载视频')
|
||||
await videoDownloader.download()
|
||||
this.lastDirectDownloadLink = DownloadVideoPackage.lastPackageUrl
|
||||
}
|
||||
catch (error) {
|
||||
if (error !== 'canceled') {
|
||||
@ -836,9 +886,13 @@ async function loadPanel() {
|
||||
|
||||
panel.aid = pageData.aid
|
||||
panel.cid = pageData.cid
|
||||
const videoInfo = new VideoInfo(pageData.aid)
|
||||
await videoInfo.fetchInfo()
|
||||
panel.coverUrl = videoInfo.coverUrl.replace('http:', 'https:')
|
||||
try {
|
||||
const videoInfo = new VideoInfo(pageData.aid)
|
||||
await videoInfo.fetchInfo()
|
||||
panel.coverUrl = videoInfo.coverUrl.replace('http:', 'https:')
|
||||
} catch (error) {
|
||||
panel.coverUrl = EmptyImageUrl
|
||||
}
|
||||
|
||||
formats = await VideoFormat.getAvailableFormats();
|
||||
[selectedFormat] = formats
|
||||
|
||||
@ -51,8 +51,8 @@ export const getDashInfo = async (api: string, quality: number) => {
|
||||
codecId: d.codecid,
|
||||
bandWidth: d.bandwidth,
|
||||
frameRate: d.frameRate,
|
||||
backupUrls: d.backupUrl,
|
||||
downloadUrl: d.baseUrl,
|
||||
backupUrls: (d.backupUrl || d.backup_url || '').replace('http:', 'https:'),
|
||||
downloadUrl: (d.baseUrl || d.base_url || '').replace('http:', 'https:'),
|
||||
duration,
|
||||
}
|
||||
return dash
|
||||
@ -62,8 +62,8 @@ export const getDashInfo = async (api: string, quality: number) => {
|
||||
bandWidth: d.bandwidth,
|
||||
codecs: d.codecs,
|
||||
codecId: d.codecid,
|
||||
backupUrls: d.backupUrl,
|
||||
downloadUrl: d.baseUrl,
|
||||
backupUrls: (d.backupUrl || d.backup_url || '').replace('http:', 'https:'),
|
||||
downloadUrl: (d.baseUrl || d.base_url || '').replace('http:', 'https:'),
|
||||
duration,
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user