Add batch support for dash

This commit is contained in:
the1812 2019-11-09 12:23:46 +08:00
parent d2acf71d01
commit 88af768056
15 changed files with 180 additions and 113 deletions

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

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@
"auto-continue.min.js": "96CD47C367D7397CE1467A69764409BAEFF16E8F535BC8E15523CD0B8A86687E", "auto-continue.min.js": "96CD47C367D7397CE1467A69764409BAEFF16E8F535BC8E15523CD0B8A86687E",
"auto-draw.min.js": "AE72CF2623DF2D15AD4AF82D125FFAF7EF5B1E6B36D9E9AC646AC98AA6AA8698", "auto-draw.min.js": "AE72CF2623DF2D15AD4AF82D125FFAF7EF5B1E6B36D9E9AC646AC98AA6AA8698",
"auto-play.min.js": "DC9938AC15DADDC9D88DCA0C9BE64BE142C37D32CB85E42E23DAB2A7378531E5", "auto-play.min.js": "DC9938AC15DADDC9D88DCA0C9BE64BE142C37D32CB85E42E23DAB2A7378531E5",
"batch-download.min.js": "226DA052A6A9542AEF8B1767823F774C600D586394E2B36A6E6765BD76A7597E", "batch-download.min.js": "B1E7B544EB4A04CEAE1FCC8668DDF29642C2B394D4C5F63A4EB3D2BA8519587E",
"biliplus-redirect.min.js": "9882D14DAC5C103212A101A5168883C41B4B3B2737FD5F222DC343D0304FF8AF", "biliplus-redirect.min.js": "9882D14DAC5C103212A101A5168883C41B4B3B2737FD5F222DC343D0304FF8AF",
"blackboard.min.js": "8BB9CCDE72302AE7DE39D66F9231EFB82BA27DDA799BA4AAAC06F6DE802661BF", "blackboard.min.js": "8BB9CCDE72302AE7DE39D66F9231EFB82BA27DDA799BA4AAAC06F6DE802661BF",
"blackboard.vue.min.js": "8F1984AB69B029701DC88F33568EF1881D0C54EEC72D65EA3029CF2F68BD6674", "blackboard.vue.min.js": "8F1984AB69B029701DC88F33568EF1881D0C54EEC72D65EA3029CF2F68BD6674",
@ -47,7 +47,7 @@
"download-video-package.min.js": "B10FF11DE7DFBACEC38F195B8E836511E527215B5E177CB1D223D14F30EB1435", "download-video-package.min.js": "B10FF11DE7DFBACEC38F195B8E836511E527215B5E177CB1D223D14F30EB1435",
"download-video.min.css": "95FF9DA3C1FFF1C1134D70AE3BB609049B7D5D3FBB71CAAAE84C8742C51AD841", "download-video.min.css": "95FF9DA3C1FFF1C1134D70AE3BB609049B7D5D3FBB71CAAAE84C8742C51AD841",
"download-video.min.html": "2727789E8423E8DBC8801FF6AC0D8181AC684A643A83CD446A5D69318DDBF9F6", "download-video.min.html": "2727789E8423E8DBC8801FF6AC0D8181AC684A643A83CD446A5D69318DDBF9F6",
"download-video.min.js": "4013A2F8DB6B2859DF83EFEC32274158ABA1769536D8656537E2FCDF96560AD8", "download-video.min.js": "06CA9FEB6A20B45E142D68D8C8C102993F30237B35D8D6562C9D7D7EAD04F866",
"dpi-img.vue.min.js": "3D9D93079365DCABA8DC8970F051AD1A7F25C3EFFF850CD1000C4312E9B910DD", "dpi-img.vue.min.js": "3D9D93079365DCABA8DC8970F051AD1A7F25C3EFFF850CD1000C4312E9B910DD",
"expand-danmaku.min.js": "B21658C40085AEA8DC49652AE62EB8610BB2EBEAF7A9C9AF69EF3B11E08EEC8D", "expand-danmaku.min.js": "B21658C40085AEA8DC49652AE62EB8610BB2EBEAF7A9C9AF69EF3B11E08EEC8D",
"expand-description.min.css": "58C7710A50521B80F7D872BDC4C652610D84C4FABC6874BA66DA37B4F8759224", "expand-description.min.css": "58C7710A50521B80F7D872BDC4C652610D84C4FABC6874BA66DA37B4F8759224",
@ -56,7 +56,7 @@
"feeds-apis.min.js": "ED1FFDEBC22680C5089F9D30CF667535FB99881C96A969DB47BC70BF095A99E8", "feeds-apis.min.js": "ED1FFDEBC22680C5089F9D30CF667535FB99881C96A969DB47BC70BF095A99E8",
"feeds-filter-card.vue.min.js": "3A7F8F402EA46E43B718506329814953C9E358A8AF94088D4E5745D48296D80B", "feeds-filter-card.vue.min.js": "3A7F8F402EA46E43B718506329814953C9E358A8AF94088D4E5745D48296D80B",
"feeds-filter.min.js": "10557E498B8BADA3C1C03E8C1DCD6FB24DBA2DA27A5A03BD861054F7E2989D46", "feeds-filter.min.js": "10557E498B8BADA3C1C03E8C1DCD6FB24DBA2DA27A5A03BD861054F7E2989D46",
"ffmpeg-support.min.js": "CB33E150098394ABCACE6FC45CC1C6DA1B71012C77265D93F23C555A532016A1", "ffmpeg-support.min.js": "1C9B76371E6E9DDE615FE0D74E11BF205F54C9B825FC9FF0223FB835617EA219",
"fill-with-comments.min.js": "5D52ACB5057267C2EEBA5AA74EEE5D4086215645623560825C25AAA133536F2F", "fill-with-comments.min.js": "5D52ACB5057267C2EEBA5AA74EEE5D4086215645623560825C25AAA133536F2F",
"filter-type-switch.vue.min.js": "E06B00EF48F3CCFACC82B858A266CE0DF48F8332C5900B3AD579330D4C6D3125", "filter-type-switch.vue.min.js": "E06B00EF48F3CCFACC82B858A266CE0DF48F8332C5900B3AD579330D4C6D3125",
"fix-fullscreen.min.js": "C0628A7CABB4421FCBD7663700EF9965C96F6D79979B9F7523A9F9B0B009C8B6", "fix-fullscreen.min.js": "C0628A7CABB4421FCBD7663700EF9965C96F6D79979B9F7523A9F9B0B009C8B6",
@ -172,7 +172,7 @@
"v-dropdown.vue.min.js": "D26B4A74515F53A34A3CB0590FC9683476D99FBBBB9647FDFCBF33E265C3F662", "v-dropdown.vue.min.js": "D26B4A74515F53A34A3CB0590FC9683476D99FBBBB9647FDFCBF33E265C3F662",
"video-card-info.min.js": "13B755C0EE0CA018AE65251E168BAF3395FEA2EB4FAD6949AADD752F61B8819B", "video-card-info.min.js": "13B755C0EE0CA018AE65251E168BAF3395FEA2EB4FAD6949AADD752F61B8819B",
"video-card.vue.min.js": "340436CDE3B102F467F671411D4373E0DBF59C5A72867D18BDF45BA6C66156C2", "video-card.vue.min.js": "340436CDE3B102F467F671411D4373E0DBF59C5A72867D18BDF45BA6C66156C2",
"video-dash.min.js": "160CD4F33B2AA90D66F9EEFE4070EFD16AA794F542DCBCE3E5E504A1F2A26F28", "video-dash.min.js": "E0345C36E793F8C05E5FC83DDAB88751CD6BA28FB0A1AA63BC20FBCE330F037B",
"video-downloader-fragment.min.js": "13B755C0EE0CA018AE65251E168BAF3395FEA2EB4FAD6949AADD752F61B8819B", "video-downloader-fragment.min.js": "13B755C0EE0CA018AE65251E168BAF3395FEA2EB4FAD6949AADD752F61B8819B",
"video-info.min.js": "7234A74CA28A772A31E41BBD4925FC2B22C6701419ADFD511402C6A9D6D9E5FA", "video-info.min.js": "7234A74CA28A772A31E41BBD4925FC2B22C6701419ADFD511402C6A9D6D9E5FA",
"video-list.vue.min.js": "BC1DB652C4CD8E98AC620E74ED306AACE0CD99359EE472BFD2A846ADCA37AD0B", "video-list.vue.min.js": "BC1DB652C4CD8E98AC620E74ED306AACE0CD99359EE472BFD2A846ADCA37AD0B",

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
(()=>{return(e,t)=>{const n=(e,t,n)=>{if(e<2){return null}const s=[];for(let r=1;r<=e;r++){let e=` - ${r}`;if(n.includes(".m4a")){e=""}s.push(escapeFilename(`file '${t}${e}${n[r-1]}'`))}return s.join("\n")};const s=(e,t)=>{const n=e=>e.fragments.length>1;const s=e.filter(n);if(s.length===0){return null}const r=new Map;s.forEach(e=>{r.set(escapeFilename(`ffmpeg-files-${e.title}.txt`),e.fragments.map((n,s)=>{return escapeFilename(`file '${e.title} - ${s+1}${t[s]}'`)}).join("\n"))});return r};const r=(e,t)=>{const n=[];e.forEach(e=>{e.fragments.forEach((s,r)=>{let i="";if(e.fragments.length>1&&!t.includes(".m4a")){i=` - ${r+1}`}n.push(escapeFilename(`file '${e.title}${i}${t[r]}'`))})});return n.join("\n")};return{export:{getFragmentsList:n,getBatchFragmentsList:s,getBatchEpisodesList:r}}}})(); (()=>{return(e,t)=>{const n=[".mp4",".m4a"];const r=(e,t,n)=>{if(e<2){return null}const r=[];for(let s=1;s<=e;s++){let e=` - ${s}`;if(n.includes(".m4a")){e=""}r.push(escapeFilename(`file '${t}${e}${n[s-1]}'`))}return r.join("\n")};const s=(e,t)=>{const r=e=>e.fragments.length>1;const s=e.filter(r);if(s.length===0){return null}const i=new Map;s.forEach(e=>{i.set(escapeFilename(`ffmpeg-files-${e.title}.txt`),e.fragments.map((r,s)=>{let i=` - ${s+1}`;if(t===true){i=""}return escapeFilename(`file '${e.title}${i}${t===true?n[s]:t}'`)}).join("\n"))});return i};const i=(e,t)=>{const r=[];e.forEach(e=>{e.fragments.forEach((s,i)=>{let l="";if(e.fragments.length>1&&t!==true){l=` - ${i+1}`}r.push(escapeFilename(`file '${e.title}${l}${t===true?n[i]:t}'`))})});return r.join("\n")};return{export:{getFragmentsList:r,getBatchFragmentsList:s,getBatchEpisodesList:i}}}})();

View File

@ -1 +1 @@
(()=>{return(t,a)=>{const e=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 e=await Ajax.getJsonWithCredentials(t);const r=e.data||e.result||e;if(e.code!==0||!r.dash){throw new Error("DASH api failed")}const c=r.accept_quality;if(!c.includes(a)){throw new Error("没有找到请求的清晰度")}if(r.quality!==a){throw new Error("无法获取请求的清晰度, 请确认当前账号有相应的权限")}const d=r.accept_description;const o=d[c.indexOf(a)];const s=r.dash.duration;const n=r.dash.video.filter(t=>t.id===a).map(t=>{const e=(()=>{switch(t.codecid){case 12:return"HEVC/H.265";default:case 7:return"AVC/H.264"}})();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||[]).forEach(t=>t.replace("http:","https:")),downloadUrl:(t.baseUrl||t.base_url||"").replace("http:","https:"),duration:s,videoCodec:e};return r});const i=r.dash.audio.map(t=>{return{bandWidth:t.bandwidth,codecs:t.codecs,codecId:t.codecid,backupUrls:(t.backupUrl||t.backup_url||[]).forEach(t=>t.replace("http:","https:")),downloadUrl:(t.baseUrl||t.base_url||"").replace("http:","https:"),duration:s}});return{videoDashes:n,audioDashes:i}};return{export:{getDashInfo:r,dashToFragment:e}}}})(); (()=>{return(t,e)=>{const a=t=>{return{url:t.downloadUrl,backupUrls:t.backupUrls,length:t.duration,size:Math.trunc(t.bandWidth*t.duration/8)}};const d=e=>{console.log(e.videoDashes);const d=(()=>{const a=e=>e.videoCodec===t.downloadVideoDashCodec;if(e.videoDashes.some(a)){return e.videoDashes.filter(a).sort(ascendingSort(t=>t.bandWidth))[0]}else{return e.videoDashes.sort(ascendingSort(t=>t.bandWidth))[0]}})();const o=e.audioDashes.sort(descendingSort(t=>t.bandWidth))[0];return[a(d),a(o)]};const o=async(t,e)=>{const a=await Ajax.getJsonWithCredentials(t);const d=a.data||a.result||a;if(a.code!==0||!d.dash){throw new Error("DASH api failed")}const o=d.accept_quality;if(!o.includes(e)){throw new Error("没有找到请求的清晰度")}if(d.quality!==e){throw new Error("无法获取请求的清晰度, 请确认当前账号有相应的权限")}const r=d.accept_description;const s=r[o.indexOf(e)];const c=d.dash.duration;const n=d.dash.video.filter(t=>t.id===e).map(t=>{const a=(()=>{switch(t.codecid){case 12:return"HEVC/H.265";default:case 7:return"AVC/H.264"}})();const d={quality:e,qualityText:s,width:t.width,height:t.height,codecs:t.codecs,codecId:t.codecid,bandWidth:t.bandwidth,frameRate:t.frameRate,backupUrls:(t.backupUrl||t.backup_url||[]).forEach(t=>t.replace("http:","https:")),downloadUrl:(t.baseUrl||t.base_url||"").replace("http:","https:"),duration:c,videoCodec:a};return d});const i=d.dash.audio.map(t=>{return{bandWidth:t.bandwidth,codecs:t.codecs,codecId:t.codecid,backupUrls:(t.backupUrl||t.backup_url||[]).forEach(t=>t.replace("http:","https:")),downloadUrl:(t.baseUrl||t.base_url||"").replace("http:","https:"),duration:c}});return{videoDashes:n,audioDashes:i}};return{export:{getDashInfo:o,dashToFragments:d}}}})();

View File

@ -162,3 +162,4 @@ export function html(strings, ...values) {
return previous + current + (value === undefined ? '' : value) return previous + current + (value === undefined ? '' : value)
}, '') }, '')
} }
export const dashExtensions = ['.mp4', '.m4a']

View File

@ -1,4 +1,10 @@
const fragmentSplitFactor = 12 const fragmentSplitFactor = 12
const dashExtensions = ['.mp4', '.m4a']
export interface BatchExtractorConfig {
itemFilter: (item: BatchItem) => boolean
api?: (aid: number | string, cid: number | string, quality?: number | string) => string
}
interface BatchItem { interface BatchItem {
aid: number | string aid: number | string
cid: number | string cid: number | string
@ -13,13 +19,26 @@ export interface RawItem {
referer: string referer: string
} }
abstract class Batch { abstract class Batch {
constructor(public config: BatchExtractorConfig) { }
itemList: BatchItem[] = [] itemList: BatchItem[] = []
itemFilter: (item: BatchItem) => boolean = () => true
abstract async getItemList(): Promise<BatchItem[]> abstract async getItemList(): Promise<BatchItem[]>
abstract async collectData(quality: number | string): Promise<string> abstract async collectData(quality: number | string): Promise<string>
async getRawItems(quality: number | string): Promise<RawItem[]> { async getRawItems(quality: number | string): Promise<RawItem[]> {
return JSON.parse(await this.collectData(quality)) return JSON.parse(await this.collectData(quality))
} }
extension(url: string, index: number) {
const match = [
'.flv',
'.mp4'
].find(it => url.includes(it))
if (match) {
return match
} else if (url.includes('.m4s')) {
return dashExtensions[index]
} else {
return '.flv'
}
}
async collectAria2(quality: number | string, rpc: boolean) { async collectAria2(quality: number | string, rpc: boolean) {
const json = await this.getRawItems(quality) const json = await this.getRawItems(quality)
if (rpc) { if (rpc) {
@ -39,7 +58,7 @@ abstract class Batch {
params.push({ params.push({
referer: document.URL.replace(window.location.search, ''), referer: document.URL.replace(window.location.search, ''),
'user-agent': UserAgent, 'user-agent': UserAgent,
out: `${item.title}${indexNumber}.flv`, out: `${item.title}${indexNumber}${this.extension(fragment.url, index)}`,
split: fragmentSplitFactor, split: fragmentSplitFactor,
dir: (option.baseDir + option.dir) || undefined, dir: (option.baseDir + option.dir) || undefined,
'max-download-limit': option.maxDownloadLimit || undefined, 'max-download-limit': option.maxDownloadLimit || undefined,
@ -66,7 +85,7 @@ ${json.map(item => {
${f.url} ${f.url}
referer=${item.referer} referer=${item.referer}
user-agent=${UserAgent} user-agent=${UserAgent}
out=${item.title}${indexNumber}.flv out=${item.title}${indexNumber}${this.extension(f.url, index)}
split=${fragmentSplitFactor} split=${fragmentSplitFactor}
`.trim() `.trim()
}) })
@ -108,20 +127,27 @@ class VideoEpisodeBatch extends Batch {
} }
async collectData(quality: number | string) { async collectData(quality: number | string) {
const result = [] const result = []
for (const item of (await this.getItemList()).filter(this.itemFilter)) { for (const item of (await this.getItemList()).filter(this.config.itemFilter)) {
const url = `https://api.bilibili.com/x/player/playurl?avid=${item.aid}&cid=${item.cid}&qn=${quality}&otype=json` const url = this.config.api ? this.config.api(item.aid, item.cid, quality) : `https://api.bilibili.com/x/player/playurl?avid=${item.aid}&cid=${item.cid}&qn=${quality}&otype=json`
const json = await Ajax.getJsonWithCredentials(url) const json = await Ajax.getJsonWithCredentials(url)
const data = json.data || json.result || json const data = json.data || json.result || json
if (data.quality !== quality) { if (data.quality !== quality) {
console.warn(`${item.title} 不支持所选画质, 已回退到较低画质. (quality=${data.quality})`) console.warn(`${item.title} 不支持所选画质, 已回退到较低画质. (quality=${data.quality})`)
} }
const fragments: RawItemFragment[] = data.durl.map((it: any) => { let fragments: RawItemFragment[]
return { if (data.durl) {
length: it.length, fragments = data.durl.map((it: any) => {
size: it.size, return {
url: it.url length: it.length,
} size: it.size,
}) url: it.url
}
})
} else {
const { getDashInfo, dashToFragments } = await import('./video-dash')
const info = await getDashInfo(url, typeof quality === 'string' ? parseInt(quality) : quality)
fragments = dashToFragments(info)
}
result.push({ result.push({
fragments, fragments,
title: item.title.replace(/[\/\\:\*\?"<>\|]/g, ' '), title: item.title.replace(/[\/\\:\*\?"<>\|]/g, ' '),
@ -167,20 +193,27 @@ class BangumiBatch extends Batch {
} }
async collectData(quality: string | number) { async collectData(quality: string | number) {
const result = [] const result = []
for (const item of (await this.getItemList()).filter(this.itemFilter)) { for (const item of (await this.getItemList()).filter(this.config.itemFilter)) {
const url = `https://api.bilibili.com/pgc/player/web/playurl?avid=${item.aid}&cid=${item.cid}&qn=${quality}&otype=json` const url = this.config.api ? this.config.api(item.aid, item.cid, quality) : `https://api.bilibili.com/pgc/player/web/playurl?avid=${item.aid}&cid=${item.cid}&qn=${quality}&otype=json`
const json = await Ajax.getJsonWithCredentials(url) const json = await Ajax.getJsonWithCredentials(url)
const data = json.data || json.result || json const data = json.data || json.result || json
if (data.quality !== quality) { if (data.quality !== quality) {
console.warn(`${item.title} 不支持所选画质, 已回退到较低画质. (quality=${data.quality})`) console.warn(`${item.title} 不支持所选画质, 已回退到较低画质. (quality=${data.quality})`)
} }
const fragments: RawItemFragment[] = data.durl.map((it: any) => { let fragments: RawItemFragment[]
return { if (data.durl) {
length: it.length, fragments = data.durl.map((it: any) => {
size: it.size, return {
url: it.url length: it.length,
} size: it.size,
}) url: it.url
}
})
} else {
const { getDashInfo, dashToFragments } = await import('./video-dash')
const info = await getDashInfo(url, typeof quality === 'string' ? parseInt(quality) : quality)
fragments = dashToFragments(info)
}
result.push({ result.push({
fragments, fragments,
title: item.title.replace(/[\/\\:\*\?"<>\|]/g, ' '), title: item.title.replace(/[\/\\:\*\?"<>\|]/g, ' '),
@ -193,9 +226,14 @@ class BangumiBatch extends Batch {
} }
} }
const extractors = [BangumiBatch, VideoEpisodeBatch] const extractors = [BangumiBatch, VideoEpisodeBatch]
let ExtractorClass: new() => Batch let ExtractorClass: new (config: BatchExtractorConfig) => Batch
export class BatchExtractor { export class BatchExtractor {
itemFilter: (item: BatchItem) => boolean = () => true config: BatchExtractorConfig
constructor(config?: BatchExtractorConfig) {
this.config = Object.assign({
itemFilter: () => true,
}, config)
}
static async test() { static async test() {
for (const e of extractors) { for (const e of extractors) {
if (await e.test() === true) { if (await e.test() === true) {
@ -210,8 +248,7 @@ export class BatchExtractor {
logError('[批量下载] 未找到合适的解析模块.') logError('[批量下载] 未找到合适的解析模块.')
throw new Error(`[Batch Download] module not found.`) throw new Error(`[Batch Download] module not found.`)
} }
const extractor = new ExtractorClass() const extractor = new ExtractorClass(this.config)
extractor.itemFilter = this.itemFilter
return extractor return extractor
} }
async getItemList() { async getItemList() {

View File

@ -11,53 +11,71 @@ interface PageData {
cid: string cid: string
} }
class Video { class Video {
async getDashUrl(quality?: number) { async getApiGenerator(dash = false) {
if (quality) { function api(aid: number | string, cid: number | string, quality?: number) {
return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json&fnver=0&fnval=16` if (dash) {
} else { if (quality) {
return `https://api.bilibili.com/x/player/playurl?avid=${pageData.aid}&cid=${pageData.cid}&otype=json&fnver=0&fnval=16` return `https://api.bilibili.com/x/player/playurl?avid=${aid}&cid=${cid}&qn=${quality}&otype=json&fnver=0&fnval=16`
} else {
return `https://api.bilibili.com/x/player/playurl?avid=${aid}&cid=${cid}&otype=json&fnver=0&fnval=16`
}
} else {
if (quality) {
return `https://api.bilibili.com/x/player/playurl?avid=${aid}&cid=${cid}&qn=${quality}&otype=json`
} else {
return `https://api.bilibili.com/x/player/playurl?avid=${aid}&cid=${cid}&otype=json`
}
}
} }
return api.bind(this) as typeof api
}
async getDashUrl(quality?: number) {
return (await this.getApiGenerator(true))(pageData.aid, pageData.cid, quality)
} }
async getUrl(quality?: number) { async getUrl(quality?: number) {
if (quality) { return (await this.getApiGenerator())(pageData.aid, pageData.cid, 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 Bangumi extends Video { class Bangumi extends Video {
async getDashUrl(quality?: number) { async getApiGenerator(dash = false) {
if (quality) { function api(aid: number | string, cid: number | string, quality?: number) {
return `https://api.bilibili.com/pgc/player/web/playurl?avid=56995872&cid=99547543&qn=${quality}&otype=json&fourk=1&fnval=16` if (dash) {
} else { if (quality) {
return `https://api.bilibili.com/pgc/player/web/playurl?avid=56995872&cid=99547543&otype=json&fourk=1&fnval=16` return `https://api.bilibili.com/pgc/player/web/playurl?avid=${aid}&cid=${cid}&qn=${quality}&otype=json&fourk=1&fnval=16`
} } else {
} return `https://api.bilibili.com/pgc/player/web/playurl?avid=${aid}&cid=${cid}&otype=json&fourk=1&fnval=16`
async getUrl(quality?: number) { }
if (quality) { } else {
return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json` if (quality) {
} else { return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json`
return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=&otype=json` } else {
return `https://api.bilibili.com/pgc/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=&otype=json`
}
}
} }
return api.bind(this) as typeof api
} }
} }
// 课程, 不知道为什么b站给它起名cheese // 课程, 不知道为什么b站给它起名cheese
class Cheese extends Video { class Cheese extends Video {
constructor(public ep: number | string) { super() } constructor(public ep: number | string) { super() }
async getDashUrl(quality?: number) { async getApiGenerator(dash = false) {
if (quality) { function api(aid: number | string, cid: number | string, quality?: number) {
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` if (dash) {
} else { if (quality) {
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` return `https://api.bilibili.com/pugv/player/web/playurl?avid=${aid}&cid=${cid}&qn=${quality}&otype=json&ep_id=${this.ep}&fnver=0&fnval=16`
} } else {
} return `https://api.bilibili.com/pugv/player/web/playurl?avid=${aid}&cid=${cid}&otype=json&ep_id=${this.ep}&fnver=0&fnval=16`
async getUrl(quality?: number) { }
if (quality) { } else {
return `https://api.bilibili.com/pugv/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&qn=${quality}&otype=json&ep_id=${this.ep}` if (quality) {
} else { return `https://api.bilibili.com/pugv/player/web/playurl?avid=${aid}&cid=${cid}&qn=${quality}&otype=json&ep_id=${this.ep}`
return `https://api.bilibili.com/pugv/player/web/playurl?avid=${pageData.aid}&cid=${pageData.cid}&otype=json&ep_id=${this.ep}` } else {
return `https://api.bilibili.com/pugv/player/web/playurl?avid=${aid}&cid=${cid}&otype=json&ep_id=${this.ep}`
}
}
} }
return api.bind(this) as typeof api
} }
} }
@ -168,26 +186,12 @@ class VideoDownloader {
}) })
} }
else { else {
const { dashToFragment, getDashInfo } = await import('./video-dash') const { dashToFragments, getDashInfo } = await import('./video-dash')
const dashes = await getDashInfo( const dashes = await getDashInfo(
await pageData.entity.getDashUrl(this.format.quality), await pageData.entity.getDashUrl(this.format.quality),
this.format.quality this.format.quality
) )
// 画面按照首选编码选择, 若没有相应编码则选择大小较小的编码 this.fragments = dashToFragments(dashes)
console.log(dashes.videoDashes)
const video = (() => {
const matchPreferredCodec = (d: VideoDash) => d.videoCodec === settings.downloadVideoDashCodec
if (dashes.videoDashes.some(matchPreferredCodec)) {
return dashes.videoDashes
.filter(matchPreferredCodec)
.sort(ascendingSort(d => d.bandWidth))[0]
} else {
return dashes.videoDashes.sort(ascendingSort(d => d.bandWidth))[0]
}
})()
// 声音倒序排, 选择最高音质
const audio = dashes.audioDashes.sort(descendingSort(d => d.bandWidth))[0]
this.fragments = [dashToFragment(video), dashToFragment(audio)]
} }
return this.fragments return this.fragments
} }
@ -799,7 +803,8 @@ async function loadPanel() {
} }
const toast = Toast.info('获取链接中...', '批量导出') const toast = Toast.info('获取链接中...', '批量导出')
const batchExtractor = this.batchExtractor as BatchExtractor const batchExtractor = this.batchExtractor as BatchExtractor
batchExtractor.itemFilter = episodeFilter batchExtractor.config.itemFilter = episodeFilter
batchExtractor.config.api = await pageData.entity.getApiGenerator(this.dash)
let result: string let result: string
try { try {
switch (type) { switch (type) {
@ -832,8 +837,7 @@ async function loadPanel() {
const items = await batchExtractor.getRawItems(format) const items = await batchExtractor.getRawItems(format)
const videoDownloader = new VideoDownloader(format, items[0].fragments) const videoDownloader = new VideoDownloader(format, items[0].fragments)
const { getBatchFragmentsList } = await import('./ffmpeg-support') const { getBatchFragmentsList } = await import('./ffmpeg-support')
const extensions = this.dash ? ['.mp4', 'm4a'] : [videoDownloader.extension()] const map = getBatchFragmentsList(items, this.dash || videoDownloader.extension())
const map = getBatchFragmentsList(items, extensions)
if (!map) { if (!map) {
Toast.info('所有选择的分P都没有分段.', '分段列表', 3000) Toast.info('所有选择的分P都没有分段.', '分段列表', 3000)
} else { } else {
@ -850,8 +854,7 @@ async function loadPanel() {
const items = await batchExtractor.getRawItems(format) const items = await batchExtractor.getRawItems(format)
const videoDownloader = new VideoDownloader(format, items[0].fragments) const videoDownloader = new VideoDownloader(format, items[0].fragments)
const { getBatchEpisodesList } = await import('./ffmpeg-support') const { getBatchEpisodesList } = await import('./ffmpeg-support')
const extensions = this.dash ? ['.mp4', 'm4a'] : [videoDownloader.extension()] const content = getBatchEpisodesList(items, this.dash || videoDownloader.extension())
const content = getBatchEpisodesList(items, extensions)
const pack = new DownloadVideoPackage() const pack = new DownloadVideoPackage()
pack.add('ffmpeg-files.txt', content) pack.add('ffmpeg-files.txt', content)
await pack.emit() await pack.emit()

View File

@ -1,5 +1,6 @@
import { RawItem } from './batch-download' import { RawItem } from './batch-download'
const dashExtensions = ['.mp4', '.m4a']
export const getFragmentsList = (count: number, originalTitle: string, extensions: string[]) => { export const getFragmentsList = (count: number, originalTitle: string, extensions: string[]) => {
if (count < 2) { if (count < 2) {
return null return null
@ -14,7 +15,7 @@ export const getFragmentsList = (count: number, originalTitle: string, extension
} }
return names.join('\n') return names.join('\n')
} }
export const getBatchFragmentsList = (items: RawItem[], extensions: string[]) => { export const getBatchFragmentsList = (items: RawItem[], extensionOrDash: string | boolean) => {
const multipleFragments = (item: RawItem) => item.fragments.length > 1 const multipleFragments = (item: RawItem) => item.fragments.length > 1
const fragmentsItems = items.filter(multipleFragments) const fragmentsItems = items.filter(multipleFragments)
if (fragmentsItems.length === 0) { if (fragmentsItems.length === 0) {
@ -23,20 +24,24 @@ export const getBatchFragmentsList = (items: RawItem[], extensions: string[]) =>
const names = new Map<string, string>() const names = new Map<string, string>()
fragmentsItems.forEach(item => { fragmentsItems.forEach(item => {
names.set(escapeFilename(`ffmpeg-files-${item.title}.txt`), item.fragments.map((_, index) => { names.set(escapeFilename(`ffmpeg-files-${item.title}.txt`), item.fragments.map((_, index) => {
return escapeFilename(`file '${item.title} - ${index + 1}${extensions[index]}'`) let indexNumber = ` - ${index + 1}`
if (extensionOrDash === true) {
indexNumber = ''
}
return escapeFilename(`file '${item.title}${indexNumber}${extensionOrDash === true ? dashExtensions[index] : extensionOrDash}'`)
}).join('\n')) }).join('\n'))
}) })
return names return names
} }
export const getBatchEpisodesList = (items: RawItem[], extensions: string[]) => { export const getBatchEpisodesList = (items: RawItem[], extensionOrDash: string | boolean) => {
const names: string[] = [] const names: string[] = []
items.forEach(item => { items.forEach(item => {
item.fragments.forEach((_, index) => { item.fragments.forEach((_, index) => {
let indexNumber = '' let indexNumber = ''
if (item.fragments.length > 1 && !extensions.includes('.m4a')) { if (item.fragments.length > 1 && extensionOrDash !== true) {
indexNumber = ` - ${index + 1}` indexNumber = ` - ${index + 1}`
} }
names.push(escapeFilename(`file '${item.title}${indexNumber}${extensions[index]}'`)) names.push(escapeFilename(`file '${item.title}${indexNumber}${extensionOrDash === true ? dashExtensions[index] : extensionOrDash}'`))
}) })
}) })
return names.join('\n') return names.join('\n')

View File

@ -16,7 +16,7 @@ export interface VideoDash extends AudioDash {
width: number width: number
videoCodec: DashCodec videoCodec: DashCodec
} }
export const dashToFragment = (dash: AudioDash): VideoDownloaderFragment => { const dashToFragment = (dash: AudioDash): VideoDownloaderFragment => {
return { return {
url: dash.downloadUrl, url: dash.downloadUrl,
backupUrls: dash.backupUrls, backupUrls: dash.backupUrls,
@ -24,6 +24,23 @@ export const dashToFragment = (dash: AudioDash): VideoDownloaderFragment => {
size: Math.trunc(dash.bandWidth * dash.duration / 8), size: Math.trunc(dash.bandWidth * dash.duration / 8),
} }
} }
export const dashToFragments = (dashes: { videoDashes: VideoDash[], audioDashes: AudioDash[] }) => {
// 画面按照首选编码选择, 若没有相应编码则选择大小较小的编码
console.log(dashes.videoDashes)
const video = (() => {
const matchPreferredCodec = (d: VideoDash) => d.videoCodec === settings.downloadVideoDashCodec
if (dashes.videoDashes.some(matchPreferredCodec)) {
return dashes.videoDashes
.filter(matchPreferredCodec)
.sort(ascendingSort(d => d.bandWidth))[0]
} else {
return dashes.videoDashes.sort(ascendingSort(d => d.bandWidth))[0]
}
})()
// 声音倒序排, 选择最高音质
const audio = dashes.audioDashes.sort(descendingSort(d => d.bandWidth))[0]
return [dashToFragment(video), dashToFragment(audio)]
}
export const getDashInfo = async (api: string, quality: number) => { export const getDashInfo = async (api: string, quality: number) => {
const json = await Ajax.getJsonWithCredentials(api) const json = await Ajax.getJsonWithCredentials(api)
const data = json.data || json.result || json const data = json.data || json.result || json
@ -86,6 +103,6 @@ export const getDashInfo = async (api: string, quality: number) => {
export default { export default {
export: { export: {
getDashInfo, getDashInfo,
dashToFragment, dashToFragments,
}, },
} }