Add preferred url type config (fix #3234)

This commit is contained in:
the1812 2025-09-07 14:50:53 +08:00
parent 7d0d91140b
commit d5f9c67413
6 changed files with 101 additions and 24 deletions

View File

@ -19,6 +19,7 @@
"akari",
"BALH",
"Bangumi",
"bcache",
"bevo",
"bili",
"bilibili",
@ -68,6 +69,7 @@
"Liveroom",
"materialdesignicons",
"matroska",
"MCDN",
"medialist",
"mimetype",
"minmax",

View File

@ -46,11 +46,16 @@
</div>
</template>
<div class="download-video-config-item">
<div class="download-video-config-title">使用备用下载地址:</div>
<SwitchBox v-model="useBackupUrls" />
<div class="download-video-config-title">下载地址偏好:</div>
<VDropdown v-model="preferredUrlType" :items="urlTypeOptions" />
</div>
<div class="download-video-config-description">
若默认下载地址速度缓慢, 可以尝试更换备用下载地址.
当视频有多个源时, 配置优先使用的源, 各类视频源区别请参考
<a
href="https://github.com/the1812/Bilibili-Evolved/issues/3234#issuecomment-1504764774"
target="_blank"
>此处</a
>
</div>
<component
:is="a.component"
@ -111,6 +116,7 @@ import {
DownloadVideoInputItem,
DownloadVideoOutput,
} from './types'
import { DownloadVideoUrlType, parseVideoUrlType, sortVideoUrlByType } from './url-type'
const [inputs] = registerAndGetData('downloadVideo.inputs', [
videoSingleInput,
@ -134,13 +140,20 @@ const { basicConfig } = getComponentSettings('downloadVideo').options as {
api: string
quality: number
output: string
useBackupUrls: boolean
preferredUrlType: DownloadVideoUrlType
}
}
const filterData = <T extends { match?: TestPattern }>(items: T[]) => {
const matchedItems = items.filter(it => it.match?.some(p => matchUrlPattern(p)) ?? true)
return matchedItems
}
const urlTypeOptions: { displayName: string; name: DownloadVideoUrlType }[] = [
{ displayName: 'Mirror', name: DownloadVideoUrlType.Mirror },
{ displayName: 'UPOS', name: DownloadVideoUrlType.UPOS },
{ displayName: 'BCache', name: DownloadVideoUrlType.BCache },
{ displayName: 'MCDN', name: DownloadVideoUrlType.MCDN },
]
const getFallbackTestVideoInfo = () =>
({
aid: unsafeWindow.aid,
@ -163,7 +176,11 @@ export default Vue.extend({
},
data() {
const lastOutput = basicConfig.output
const lastUseBackupUrls = basicConfig.useBackupUrls
const lastPreferredUrlType = basicConfig.preferredUrlType
console.log({
lastPreferredUrlType,
optionValue: lastPreferredUrlType ?? DownloadVideoUrlType.Mirror,
})
return {
open: false,
busy: false,
@ -183,7 +200,9 @@ export default Vue.extend({
selectedApi: undefined,
outputs,
selectedOutput: outputs.find(it => it.name === lastOutput) || outputs[0],
useBackupUrls: lastUseBackupUrls || false,
urlTypeOptions,
preferredUrlType:
lastPreferredUrlType ?? urlTypeOptions.find(it => it.name === DownloadVideoUrlType.Mirror),
}
},
computed: {
@ -229,11 +248,11 @@ export default Vue.extend({
}
basicConfig.output = output.name
},
useBackupUrls(useBackupUrls: boolean) {
if (useBackupUrls === undefined) {
preferredUrlType(urlType: DownloadVideoUrlType) {
if (urlType === undefined) {
return
}
basicConfig.useBackupUrls = useBackupUrls
basicConfig.preferredUrlType = urlType
},
},
mounted() {
@ -299,6 +318,7 @@ export default Vue.extend({
testItem.quality = this.selectedQuality
const qualityVideoInfo = await api.downloadVideoInfo(testItem)
this.testData.videoInfo = qualityVideoInfo
console.log('[qualityVideoInfo]', qualityVideoInfo)
} catch (error) {
console.error('[updateTestVideoInfo] failed', error)
this.testData.videoInfo = undefined
@ -322,16 +342,20 @@ export default Vue.extend({
Toast.info('未接收到可下载数据, 请检查输入源和格式是否适用于当前视频.', '下载视频', 3000)
return
}
if (this.useBackupUrls) {
videoInfos.forEach(videoInfo => {
videoInfo.fragments.forEach(fragment => {
fragment.url =
fragment.backupUrls && fragment.backupUrls.length !== 0
? fragment.backupUrls.at(0)
: fragment.url
})
videoInfos.forEach(videoInfo => {
videoInfo.fragments.forEach(fragment => {
if (this.preferredUrlType !== undefined) {
const preferredUrl = fragment.allUrls.find(
url => parseVideoUrlType(url) === this.preferredUrlType,
)
if (preferredUrl !== undefined) {
fragment.url = preferredUrl
} else {
fragment.url = sortVideoUrlByType(fragment.allUrls)[0]
}
}
})
}
})
const action = new DownloadVideoAction(videoInfos)
assets.forEach(a => {
const assetsType = a?.getUrls ? action.extraOnlineAssets : action.extraAssets

View File

@ -67,7 +67,7 @@ const getDashExtensions = (type: keyof typeof DefaultDashExtensions): string =>
}
const dashToFragment = (dash: Dash): DownloadVideoFragment => ({
url: dash.downloadUrl,
backupUrls: dash.backupUrls,
allUrls: [dash.downloadUrl, ...(dash.backupUrls ?? [])],
length: dash.duration,
size: Math.trunc((dash.bandWidth * dash.duration) / 8),
extension: getDashExtensions(dash.type),

View File

@ -1,5 +1,5 @@
import { bilibiliApi, getJsonWithCredentials } from '@/core/ajax'
import { formData, matchUrlPattern } from '@/core/utils'
import { matchUrlPattern } from '@/core/utils'
import { allQualities } from '@/components/video/video-quality'
import { bangumiUrls } from '@/core/utils/urls'
import { compareQuality } from '../error'
@ -24,7 +24,7 @@ const parseInfoFromJson = (data: any, extensions: string[]) => {
length: it.length,
size: it.size,
url: it.url,
backupUrls: it.backup_url,
allUrls: [it.url, ...(it.backup_url ?? [])],
extension: getExtension(index),
} as DownloadVideoFragment),
)
@ -45,11 +45,13 @@ const downloadFlv = async (input: DownloadVideoInputItem) => {
const params = {
avid: aid,
cid,
qn: quality?.value ?? '',
qn: quality?.value?.toString() ?? '',
otype: 'json',
}
const isBanugmi = bangumiUrls.some(url => matchUrlPattern(url))
const api = isBanugmi ? bangumiApi(formData(params)) : videoApi(formData(params))
const api = isBanugmi
? bangumiApi(new URLSearchParams(params).toString())
: videoApi(new URLSearchParams(params).toString())
const data = await bilibiliApi(getJsonWithCredentials(api), '获取视频链接失败')
const info = new DownloadVideoInfo({
input,

View File

@ -35,8 +35,8 @@ export interface DownloadVideoFragment {
length: number
size: number
url: string
allUrls: string[]
extension: string
backupUrls?: string[]
}
/** 调用 API 后得到的视频详细信息, 包括下载链接, 清晰度, 分段等 */
export class DownloadVideoInfo {

View File

@ -0,0 +1,49 @@
export enum DownloadVideoUrlType {
Mirror = 'mirror',
UPOS = 'upos',
BCache = 'bcache',
MCDN = 'mcdn',
Other = 'other',
}
export const MirrorCdnHostPattern =
// spell-checker: disable-next-line
/^upos-(sz|hz|bstar)-mirror([0-9,a-z]+)\.(bilivideo\.com|akamaized\.net)$/
/** @see https://github.com/the1812/Bilibili-Evolved/issues/3234#issuecomment-1504764774 */
export const parseVideoUrlType = (url: string): DownloadVideoUrlType => {
try {
const { hostname, searchParams } = new URL(url)
if (MirrorCdnHostPattern.test(hostname)) {
return DownloadVideoUrlType.Mirror
}
const os = searchParams.get('os')?.toLowerCase()
if (os === 'upos') {
return DownloadVideoUrlType.UPOS
}
if (os === 'bcache') {
return DownloadVideoUrlType.BCache
}
if (os === 'mcdn') {
return DownloadVideoUrlType.MCDN
}
return DownloadVideoUrlType.Other
} catch {
return DownloadVideoUrlType.Other
}
}
export const sortVideoUrlByType = (urls: string[]) => {
const sortKeys = {
[DownloadVideoUrlType.Mirror]: 0,
[DownloadVideoUrlType.UPOS]: 1,
[DownloadVideoUrlType.BCache]: 2,
[DownloadVideoUrlType.MCDN]: 3,
[DownloadVideoUrlType.Other]: 4,
}
return [...urls].sort((a, b) => {
const typeA = parseVideoUrlType(a)
const typeB = parseVideoUrlType(b)
return sortKeys[typeA] - sortKeys[typeB]
})
}