mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
238 lines
7.9 KiB
TypeScript
238 lines
7.9 KiB
TypeScript
const fragmentSplitFactor = 12
|
|
interface BatchItem {
|
|
aid: number | string
|
|
cid: number | string
|
|
title: string
|
|
}
|
|
interface RawItemFragment { length: number, size: number, url: string }
|
|
interface RawItem {
|
|
fragments: RawItemFragment[]
|
|
title: string
|
|
totalSize: number
|
|
cid: number | string
|
|
referer: string
|
|
}
|
|
abstract class Batch {
|
|
itemList: BatchItem[] = []
|
|
itemFilter: (item: BatchItem) => boolean = () => true
|
|
abstract async getItemList(): Promise<BatchItem[]>
|
|
abstract async collectData(quality: number | string): Promise<string>
|
|
async collectAria2(quality: number | string, rpc: boolean) {
|
|
const json: RawItem[] = JSON.parse(await this.collectData(quality))
|
|
if (rpc) {
|
|
const option = settings.aria2RpcOption
|
|
const { sendRpc } = await import('./aria2-rpc')
|
|
for (const item of json) {
|
|
const params = item.fragments.map((fragment: { url: string }, index: number) => {
|
|
let indexNumber = ''
|
|
if (item.fragments.length > 1) {
|
|
indexNumber = ' - ' + (index + 1)
|
|
}
|
|
const params = []
|
|
if (option.secretKey !== '') {
|
|
params.push(`token:${option.secretKey}`)
|
|
}
|
|
params.push([fragment.url])
|
|
params.push({
|
|
referer: document.URL.replace(window.location.search, ''),
|
|
'user-agent': UserAgent,
|
|
out: `${item.title}${indexNumber}.flv`,
|
|
split: fragmentSplitFactor,
|
|
dir: (option.baseDir + option.dir) || undefined,
|
|
'max-download-limit': option.maxDownloadLimit || undefined,
|
|
})
|
|
const id = encodeURIComponent(`${item.title}${indexNumber}`)
|
|
return {
|
|
params,
|
|
id,
|
|
}
|
|
})
|
|
await sendRpc(params, true)
|
|
}
|
|
} else {
|
|
return `
|
|
# Generated by Bilibili Evolved Video Export
|
|
# https://github.com/the1812/Bilibili-Evolved/
|
|
${json.map(item => {
|
|
return item.fragments.map((f, index) => {
|
|
let indexNumber = ''
|
|
if (item.fragments.length > 1) {
|
|
indexNumber = ` - ${index + 1}`
|
|
}
|
|
return `
|
|
${f.url}
|
|
referer=${item.referer}
|
|
user-agent=${UserAgent}
|
|
out=${item.title}${indexNumber}.flv
|
|
split=${fragmentSplitFactor}
|
|
`.trim()
|
|
})
|
|
}).join('\n')}
|
|
`.trim()
|
|
}
|
|
}
|
|
}
|
|
class VideoEpisodeBatch extends Batch {
|
|
static async test() {
|
|
if (!document.URL.includes('/www.bilibili.com/video/av')) {
|
|
return false
|
|
}
|
|
return await SpinQuery.select('#multi_page') !== null
|
|
}
|
|
async getItemList() {
|
|
if (this.itemList.length > 0) {
|
|
return this.itemList
|
|
}
|
|
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 []
|
|
}
|
|
this.itemList = pages.map((page: any) => {
|
|
return {
|
|
title: `P${page.page} ${page.part}`,
|
|
cid: page.cid,
|
|
aid: unsafeWindow.aid,
|
|
}
|
|
})
|
|
return this.itemList
|
|
}
|
|
async collectData(quality: number | string) {
|
|
const result = []
|
|
for (const item of (await this.getItemList()).filter(this.itemFilter)) {
|
|
const url = `https://api.bilibili.com/x/player/playurl?avid=${item.aid}&cid=${item.cid}&qn=${quality}&otype=json`
|
|
const json = await Ajax.getJsonWithCredentials(url)
|
|
const data = json.data || json.result || json
|
|
if (data.quality !== quality) {
|
|
console.warn(`${item.title} 不支持所选画质, 已回退到较低画质. (quality=${data.quality})`)
|
|
}
|
|
const fragments: RawItemFragment[] = data.durl.map((it: any) => {
|
|
return {
|
|
length: it.length,
|
|
size: it.size,
|
|
url: it.url
|
|
}
|
|
})
|
|
result.push({
|
|
fragments,
|
|
title: item.title.replace(/[\/\\:\*\?"<>\|]/g, ' '),
|
|
totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it),
|
|
cid: item.cid,
|
|
referer: document.URL.replace(window.location.search, '')
|
|
})
|
|
}
|
|
return JSON.stringify(result)
|
|
}
|
|
}
|
|
class BangumiBatch extends Batch {
|
|
static async test() {
|
|
return document.URL.includes('/www.bilibili.com/bangumi')
|
|
}
|
|
async getItemList() {
|
|
if (this.itemList.length > 0) {
|
|
return this.itemList
|
|
}
|
|
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 []
|
|
}
|
|
this.itemList = json.result.main_section.episodes.map((it: any, index: number) => {
|
|
return {
|
|
aid: it.aid,
|
|
cid: it.cid,
|
|
title: it.long_title ? `${it.title} - ${it.long_title}` : `${index + 1} - ${it.title}`,
|
|
}
|
|
})
|
|
return this.itemList
|
|
}
|
|
async collectData(quality: string | number) {
|
|
const result = []
|
|
for (const item of (await this.getItemList()).filter(this.itemFilter)) {
|
|
const url = `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 data = json.data || json.result || json
|
|
if (data.quality !== quality) {
|
|
console.warn(`${item.title} 不支持所选画质, 已回退到较低画质. (quality=${data.quality})`)
|
|
}
|
|
const fragments: RawItemFragment[] = data.durl.map((it: any) => {
|
|
return {
|
|
length: it.length,
|
|
size: it.size,
|
|
url: it.url
|
|
}
|
|
})
|
|
result.push({
|
|
fragments,
|
|
title: item.title.replace(/[\/\\:\*\?"<>\|]/g, ' '),
|
|
totalSize: fragments.map(it => it.size).reduce((acc, it) => acc + it),
|
|
cid: item.cid,
|
|
referer: document.URL.replace(window.location.search, '')
|
|
})
|
|
}
|
|
return JSON.stringify(result)
|
|
}
|
|
}
|
|
const extractors = [BangumiBatch, VideoEpisodeBatch]
|
|
let ExtractorClass: new() => Batch
|
|
export class BatchExtractor {
|
|
itemFilter: (item: BatchItem) => boolean = () => true
|
|
static async test() {
|
|
for (const e of extractors) {
|
|
if (await e.test() === true) {
|
|
ExtractorClass = e
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
getExtractor() {
|
|
if (ExtractorClass === null) {
|
|
logError('[批量下载] 未找到合适的解析模块.')
|
|
throw new Error(`[Batch Download] module not found.`)
|
|
}
|
|
const extractor = new ExtractorClass()
|
|
extractor.itemFilter = this.itemFilter
|
|
return extractor
|
|
}
|
|
async getItemList() {
|
|
const extractor = this.getExtractor()
|
|
return await extractor.getItemList()
|
|
}
|
|
async collectData(format: { quality: number | string }, toast: Toast) {
|
|
const extractor = this.getExtractor()
|
|
const result = await extractor.collectData(format.quality)
|
|
toast.dismiss()
|
|
return result
|
|
}
|
|
async collectAria2(format: { quality: number | string }, toast: Toast, rpc: false): Promise<string>
|
|
async collectAria2(format: { quality: number | string }, toast: Toast, rpc: true): Promise<undefined>
|
|
async collectAria2(format: { quality: number | string }, toast: Toast, rpc = false) {
|
|
const extractor = this.getExtractor()
|
|
const result = await extractor.collectAria2(format.quality, rpc)
|
|
toast.dismiss()
|
|
return result
|
|
}
|
|
}
|
|
export default {
|
|
export: {
|
|
BatchExtractor
|
|
}
|
|
}
|