feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Bilibili 播放链接获取接口
|
||||
* 代码参考 https://github.com/rio4raki/Bilibili-audio-extraction 优化
|
||||
* 采用 platform=html5 获取对浏览器 <audio> 标签兼容性最好的直链
|
||||
* 同时转发客户端 IP,解决海外服务器获取到错误 CDN 节点导致访问慢的问题
|
||||
*/
|
||||
import { defineEventHandler, getQuery } from 'h3'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||||
import { getClientIP } from '~~/server/utils/ip-utils'
|
||||
|
||||
interface NoRefererPlayUrlRes {
|
||||
code: number
|
||||
message: string
|
||||
data: {
|
||||
durl: [
|
||||
{
|
||||
url: string
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event)
|
||||
const bvid = query.id as string
|
||||
const cid = query.cid as string
|
||||
|
||||
if (!bvid) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, '缺少 id 参数')
|
||||
}
|
||||
|
||||
// 提取客户端真实 IP,用于转发给 Bilibili 接口,以便分配最快 CDN 节点
|
||||
const clientIp = getClientIP(event)
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Cookie: 'buvid3=0',
|
||||
Referer: 'https://www.bilibili.com/',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
}
|
||||
|
||||
if (clientIp !== 'unknown') {
|
||||
headers['X-Forwarded-For'] = clientIp
|
||||
headers['X-Real-IP'] = clientIp
|
||||
headers['Client-IP'] = clientIp
|
||||
}
|
||||
|
||||
try {
|
||||
let finalCid = cid
|
||||
|
||||
if (!finalCid) {
|
||||
const target_url = 'https://api.bilibili.com/x/web-interface/view'
|
||||
const resp1: any = await $fetch(target_url, {
|
||||
method: 'GET',
|
||||
params: { bvid },
|
||||
headers,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
if (!resp1?.data?.pages?.[0]?.cid) {
|
||||
console.error('[Bilibili] CID 获取失败,响应:', JSON.stringify(resp1).slice(0, 300))
|
||||
// 降级:去掉 Cookie 重试,部分场景 Cookie 反而触发风控
|
||||
try {
|
||||
const fallback: any = await $fetch(target_url, {
|
||||
method: 'GET',
|
||||
params: { bvid },
|
||||
headers: {
|
||||
Referer: 'https://www.bilibili.com/',
|
||||
'User-Agent': headers['User-Agent']
|
||||
},
|
||||
timeout: 10000
|
||||
})
|
||||
if (fallback?.data?.pages?.[0]?.cid) {
|
||||
finalCid = fallback.data.pages[0].cid
|
||||
} else {
|
||||
console.error('[Bilibili] 降级请求仍无 CID,响应:', JSON.stringify(fallback).slice(0, 300))
|
||||
throw new Error('获取 CID 失败')
|
||||
}
|
||||
} catch (fallbackErr: any) {
|
||||
throw new Error(`获取 CID 失败: ${fallbackErr?.message || '未知错误'}`)
|
||||
}
|
||||
} else {
|
||||
finalCid = resp1.data.pages[0].cid
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 platform=html5 参数绕过严格防盗链验证(允许前端使用 referrerpolicy="no-referrer")
|
||||
const target_url2 = 'https://api.bilibili.com/x/player/playurl'
|
||||
|
||||
const resp2 = await $fetch<NoRefererPlayUrlRes>(target_url2, {
|
||||
method: 'GET',
|
||||
params: {
|
||||
fnval: 1,
|
||||
platform: 'html5',
|
||||
high_quality: 1,
|
||||
bvid,
|
||||
cid: finalCid
|
||||
},
|
||||
headers,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
if (resp2.data?.durl?.length > 0) {
|
||||
const url = resp2.data.durl[0].url
|
||||
return { url, pay: false }
|
||||
} else {
|
||||
throw new Error(`获取歌曲链接失败: ${resp2.message || '未知错误'}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Bilibili playurl error:', error)
|
||||
throw createApiError(500, SERVER_ERROR_CODES.BILIBILI_PLAYURL_FAILED, error.message || '获取 Bilibili 音频链接失败')
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Bilibili 搜索接口
|
||||
* 代码参考 https://github.com/ljk743121/Sound-of-experiment/blob/v4/server/utils/plugins/bilibili.ts
|
||||
*/
|
||||
import { defineEventHandler, getQuery, createError } from 'h3'
|
||||
import xss from 'xss'
|
||||
|
||||
interface SongInfo {
|
||||
id: number
|
||||
bvid: string
|
||||
title: string
|
||||
author: string
|
||||
pic: string
|
||||
duration: string
|
||||
}
|
||||
|
||||
interface VideoPage {
|
||||
cid: number
|
||||
page: number
|
||||
part: string
|
||||
duration: number
|
||||
}
|
||||
|
||||
interface VideoInfoRes {
|
||||
code: number
|
||||
message: string
|
||||
data: {
|
||||
pages: VideoPage[]
|
||||
}
|
||||
}
|
||||
|
||||
interface SearchRes {
|
||||
code: number
|
||||
message: string
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
id: number
|
||||
bvid: string
|
||||
title: string
|
||||
author: string
|
||||
pic: string
|
||||
duration: string
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function htmlToPlainText(value: string) {
|
||||
return xss(value, {
|
||||
whiteList: {},
|
||||
stripIgnoreTag: true,
|
||||
stripIgnoreTagBody: ['script', 'style']
|
||||
})
|
||||
}
|
||||
|
||||
function bi_convert_song(song_info: SongInfo, pages?: VideoPage[]) {
|
||||
let imgUrl = song_info.pic
|
||||
const durationStr = song_info.duration
|
||||
.split(':')
|
||||
.map((x) => Number.parseInt(x))
|
||||
.reverse()
|
||||
let duration = durationStr[0] + durationStr[1] * 60
|
||||
if (durationStr.length === 3) {
|
||||
duration += durationStr[2] * 60 * 60
|
||||
}
|
||||
if (imgUrl.startsWith('//')) {
|
||||
imgUrl = `https:${imgUrl}`
|
||||
}
|
||||
const track = {
|
||||
id: song_info.bvid,
|
||||
title: htmlToPlainText(song_info.title),
|
||||
artist: htmlToPlainText(song_info.author),
|
||||
source: 'bilibili',
|
||||
musicPlatform: 'bilibili',
|
||||
cover: imgUrl,
|
||||
duration,
|
||||
album: 'Bilibili Video',
|
||||
pages: pages || []
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event)
|
||||
const keyword = query.keyword as string
|
||||
|
||||
if (!keyword) {
|
||||
return []
|
||||
}
|
||||
|
||||
const target_url = `https://api.bilibili.com/x/web-interface/search/type`
|
||||
|
||||
try {
|
||||
const resp = await $fetch<SearchRes>(target_url, {
|
||||
method: 'GET',
|
||||
params: {
|
||||
__refresh__: true,
|
||||
page: 1,
|
||||
page_size: 15,
|
||||
platform: 'pc',
|
||||
highlight: 1,
|
||||
single_column: 0,
|
||||
keyword,
|
||||
search_type: 'video',
|
||||
dynamic_offset: 0,
|
||||
preload: true,
|
||||
com2co: true
|
||||
},
|
||||
headers: {
|
||||
Cookie: 'buvid3=0',
|
||||
Referer: 'https://www.bilibili.com/',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
}
|
||||
})
|
||||
|
||||
if (!resp.data?.result) {
|
||||
return []
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
resp.data.result.map(async (song) => {
|
||||
try {
|
||||
const videoInfoResp = await $fetch<VideoInfoRes>(
|
||||
`https://api.bilibili.com/x/web-interface/view`,
|
||||
{
|
||||
method: 'GET',
|
||||
params: {
|
||||
bvid: song.bvid
|
||||
},
|
||||
headers: {
|
||||
Cookie: 'buvid3=0',
|
||||
Referer: 'https://www.bilibili.com/',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const pages = videoInfoResp.data?.pages || []
|
||||
return bi_convert_song(song, pages)
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch video info for ${song.bvid}:`, error)
|
||||
return bi_convert_song(song)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return results
|
||||
} catch (error: any) {
|
||||
console.error('Bilibili search error:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error.message || 'Bilibili search failed'
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user