Files
BYW-VoiceHub/server/api/admin/songs/duration.post.ts
T

292 lines
9.2 KiB
TypeScript

import { readBody } from 'h3'
import { db } from '~/drizzle/db'
import { songs } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
import { requireSongAdmin } from '~~/server/utils/requireSongAdmin'
import { createApiError } from '~~/server/utils/apiError'
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
import {
createTxSongDetailBody,
normalizeTxMusicId,
txHeaders
} from '~~/server/utils/native_tx'
import { fetchSongDuration } from '~~/server/utils/songDurationFetcher'
export default defineEventHandler(async (event) => {
requireSongAdmin(event)
const body = await readBody(event)
// 新增歌曲场景:未传 songId 时按平台+音乐ID获取时长,仅返回不写入
if (body?.songId == null) {
const platform = body?.platform
const musicId = body?.musicId
if (!platform || !musicId) {
throw createApiError(
400,
SERVER_ERROR_CODES.SONG_DURATION_PLATFORM_REQUIRED,
'缺少平台或音乐 ID 信息,无法获取时长'
)
}
const durationSeconds = await fetchSongDuration(String(platform), String(musicId))
if (durationSeconds == null) {
return { success: false, songId: null, durationSeconds: null, message: '未获取到有效时长' }
}
return { success: true, songId: null, durationSeconds, message: '时长获取成功' }
}
const songId = body.songId == null ? null : Number(body.songId)
if (!Number.isFinite(songId) || songId < 1) {
throw createApiError(400, SERVER_ERROR_CODES.SONG_INVALID_ID, '歌曲 ID 无效')
}
const song = await db.select().from(songs).where(eq(songs.id, songId)).limit(1)
if (song.length === 0) {
throw createApiError(404, SERVER_ERROR_CODES.SONG_NOT_FOUND, '歌曲不存在')
}
const s = song[0]
// 浏览器读取或管理员手动输入时,只由服务端负责校验并保存结果。
if (Object.prototype.hasOwnProperty.call(body || {}, 'durationSeconds')) {
if (body.durationSeconds === null) {
await db.update(songs).set({ durationSeconds: null }).where(eq(songs.id, songId))
return { success: true, songId, durationSeconds: null, message: '时长已清空' }
}
const durationSeconds = Number(body.durationSeconds)
if (!Number.isInteger(durationSeconds) || durationSeconds < 0 || durationSeconds > 7200) {
throw createApiError(
400,
SERVER_ERROR_CODES.COMMON_INVALID_PARAMS,
'时长需在 0 秒至 2 小时之间'
)
}
if (s.durationSeconds !== durationSeconds) {
await db.update(songs).set({ durationSeconds }).where(eq(songs.id, songId))
}
return {
success: true,
songId,
durationSeconds,
message: s.durationSeconds === durationSeconds ? '时长未变化' : '时长更新成功'
}
}
const platform = s.musicPlatform
const musicId = s.musicId
if (!platform || !musicId) {
throw createApiError(
400,
SERVER_ERROR_CODES.SONG_DURATION_PLATFORM_REQUIRED,
'歌曲缺少平台或音乐 ID 信息,无法获取时长'
)
}
let durationSeconds: number | null = null
try {
if (platform === 'netease' || platform === 'netease-podcast') {
const response: any = await $fetch('/api/api-enhanced/netease/song/url/v1', {
params: { id: musicId, level: 'standard' },
timeout: 10000
})
if (response?.code === 200 && Array.isArray(response.data) && response.data.length > 0) {
const timeMs = response.data[0]?.time
if (typeof timeMs === 'number' && timeMs > 0) {
durationSeconds = Math.floor(timeMs / 1000)
}
}
if (!durationSeconds) {
return {
success: false,
songId,
durationSeconds: null,
message: '网易云 API 未返回有效时长'
}
}
} else if (platform === 'tencent') {
const normalized = normalizeTxMusicId(musicId)
const result: any = await $fetch(
'https://u.y.qq.com/cgi-bin/musicu.fcg',
{
method: 'POST',
headers: txHeaders,
body: createTxSongDetailBody(normalized),
responseType: 'json',
timeout: 10000
}
)
if (result?.code === 0 && result?.req?.code === 0) {
const interval = result.req?.data?.track_info?.interval
if (typeof interval === 'number' && interval > 0) {
durationSeconds = Math.floor(interval)
}
}
if (!durationSeconds) {
return {
success: false,
songId,
durationSeconds: null,
message: 'QQ 音乐详情接口未返回有效时长'
}
}
} else if (platform === 'migu') {
const songInfo: any = await $fetch(
`https://app.c.nf.migu.cn/resource/song/by-contentids/v2.0?contentId=${encodeURIComponent(musicId)}`,
{ timeout: 10000 }
)
if (songInfo?.code?.toString() === '000000' && songInfo?.data?.length) {
const duration = songInfo.data[0]?.duration
if (typeof duration === 'number' && duration > 0) {
durationSeconds = Math.floor(duration)
}
}
if (!durationSeconds) {
return {
success: false,
songId,
durationSeconds: null,
message: '咪咕歌曲信息接口未返回有效时长'
}
}
} else if (platform === 'bilibili') {
// musicId 格式为 bvid:cid 或 bvid:cid:page
const parts = musicId.split(':')
const bvid = parts[0]
const page = parts.length > 2 ? Number(parts[2]) : 1
if (parts.length > 2 && (!Number.isInteger(page) || page < 1)) {
return {
success: false,
songId,
durationSeconds: null,
message: `哔哩哔哩视频 ID 格式无效:${musicId}`
}
}
const viewResp: any = await $fetch(
'https://api.bilibili.com/x/web-interface/view',
{
params: { bvid },
headers: {
Cookie: 'buvid3=0',
Referer: 'https://www.bilibili.com/'
},
timeout: 10000
}
)
if (viewResp?.code === 0 && viewResp?.data?.pages?.length) {
const pageIdx = page > 1 ? Math.max(0, page - 1) : 0
const duration = viewResp.data.pages[pageIdx]?.duration
if (typeof duration === 'number' && duration > 0) {
durationSeconds = Math.floor(duration)
}
}
if (!durationSeconds) {
return {
success: false,
songId,
durationSeconds: null,
message: '哔哩哔哩视频详情接口未返回有效时长'
}
}
} else {
return {
success: false,
songId,
durationSeconds: null,
message: `暂不支持的平台:${platform}`
}
}
// 网易云固定返回 30 秒时通常是试听片段,不作为歌曲完整时长。
if ((platform === 'netease' || platform === 'netease-podcast') && durationSeconds === 30) {
return {
success: false,
songId,
durationSeconds: null,
message: '网易云返回试听片段时长,未写入'
}
}
// 校验时长范围(合理的歌曲时长:30秒~1小时)
if (durationSeconds < 30 || durationSeconds > 3600) {
console.warn(`[时长刷新] 歌曲 #${songId} 时长异常:${durationSeconds}s,跳过写入`)
return {
success: false,
songId,
durationSeconds: null,
message: `获取到的时长 ${durationSeconds}s 超出合理范围(30秒~1小时),未写入`
}
}
// 避免无变化写入
if (s.durationSeconds === durationSeconds) {
return {
success: true,
songId,
durationSeconds,
message: '时长未变化'
}
}
// 写入数据库
await db.update(songs).set({ durationSeconds }).where(eq(songs.id, songId))
return {
success: true,
songId,
durationSeconds,
message: '时长获取成功'
}
} catch (err: any) {
const errorMsg = err?.message || '未知错误'
const platformName = platform === 'netease' || platform === 'netease-podcast' ? '网易云' : platform === 'tencent' ? 'QQ音乐' : platform === 'migu' ? '咪咕' : platform === 'bilibili' ? 'Bilibili' : platform
const statusCode = err.statusCode || err.status || (err.response?.status as number | undefined)
// AbortError:请求超时或批量刷新中止
if (err.name === 'AbortError' || err.name === 'TimeoutError') {
return {
success: false,
songId,
durationSeconds: null,
message: `${platformName} 响应超时`
}
}
// 404:平台限流或歌曲不存在
if (statusCode === 404) {
return {
success: false,
songId,
durationSeconds: null,
message: `${platformName} 未找到该歌曲`
}
}
if (statusCode === 403 || statusCode === 429) {
return {
success: false,
songId,
durationSeconds: null,
message: `${platformName} 接口被限制,请稍后重试`
}
}
console.error(`[时长刷新] 歌曲 #${songId} 获取时长失败 (${platformName}):`, errorMsg)
return {
success: false,
songId,
durationSeconds: null,
message: `${platformName} 接口异常,请稍后重试`
}
}
})