Files
BYW-VoiceHub/server/api/admin/schedule/song-pool.post.ts
T

103 lines
4.5 KiB
TypeScript

import { defineEventHandler, readBody } from 'h3'
import { db } from '~/drizzle/db'
import { songs, scheduleSongPool } from '~/drizzle/schema'
import { eq, inArray } from 'drizzle-orm'
import { getServerDate } from '~~/server/utils/serverTime'
import { createApiError } from '~~/server/utils/apiError'
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
import { requireSongAdmin } from '~~/server/utils/requireSongAdmin'
import { fetchPoolCount } from '~~/server/utils/scheduleSongPool'
import { fetchSongDuration } from '~~/server/utils/songDurationFetcher'
export default defineEventHandler(async (event) => {
requireSongAdmin(event)
const user = event.context.user as any
const body = await readBody(event)
if (!Array.isArray(body.songIds) || body.songIds.length === 0) {
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'songIds 必须为非空数组')
}
const songIds = body.songIds.map(Number).filter((n) => Number.isInteger(n) && n > 0)
if (songIds.length === 0) {
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'songIds 全部无效')
}
if (songIds.length > 1000) {
throw createApiError(400, SERVER_ERROR_CODES.COMMON_INVALID_PARAMS, 'songIds 数量不能超过 1000')
}
const now = getServerDate()
// 批量查询歌曲,避免 N+1
const songsRows = await db.select().from(songs).where(inArray(songs.id, songIds))
// 时长补全放到响应后的后台任务,避免批量加入时串行等待外部平台
const missingDurationSongs = songsRows.filter(
(s) => s.durationSeconds == null && s.musicPlatform && s.musicId
)
const frontendDurationMap = new Map(
(Array.isArray(body.songDurations) ? body.songDurations : [])
.map((item) => [Number(item?.songId), Number(item?.durationSeconds)])
.filter(([songId, duration]) => Number.isInteger(songId) && Number.isFinite(duration))
)
const durationTolerance = 3
const clientDurationUpdates = missingDurationSongs
.map((song) => {
const duration = frontendDurationMap.get(song.id)
return Number.isInteger(duration) && duration >= 30 && duration <= 3600
? { song, duration }
: null
})
.filter(Boolean)
await Promise.all(
clientDurationUpdates.map(({ song, duration }) =>
db.update(songs).set({ durationSeconds: duration }).where(eq(songs.id, song.id))
)
)
if (missingDurationSongs.length > 0) {
const durationTask = (async () => {
for (const song of missingDurationSongs.filter(
(item) => !clientDurationUpdates.some((update) => update.song.id === item.id)
)) {
const duration = await fetchSongDuration(song.musicPlatform, song.musicId)
if (duration != null) {
const frontendDuration = frontendDurationMap.get(song.id)
if (frontendDuration != null && Math.abs(duration - frontendDuration) > durationTolerance) {
console.warn(`[备选池] 歌曲 #${song.id} 前端时长 ${frontendDuration}s 与后端获取 ${duration}s 不一致,以后端为准`)
}
await db.update(songs).set({ durationSeconds: duration }).where(eq(songs.id, song.id))
}
}
})()
if (typeof event.waitUntil === 'function') {
event.waitUntil(durationTask)
} else {
durationTask.catch((error) => console.error('[备选池] 后台补全时长失败:', error))
}
}
const refreshedMap = new Map(songsRows.map((s) => [s.id, s]))
// 构建待插入值和跳过列表
const insertValues = []
const skipped = []
for (const songId of songIds) {
const song = refreshedMap.get(songId)
if (!song) {
skipped.push({ songId, reason: '歌曲不存在(无法加入备选池)' })
continue
}
insertValues.push({ songId, title: song.title, artist: song.artist, createdAt: now, addedBy: user.id })
}
// 批量插入
const added = []
if (insertValues.length > 0) {
const inserted = await db
.insert(scheduleSongPool)
.values(insertValues.map((v) => ({ songId: v.songId, createdAt: v.createdAt, addedBy: v.addedBy })))
.onConflictDoNothing({ target: [scheduleSongPool.songId] })
.returning()
const insertedIds = new Set(inserted.map((r) => r.songId))
for (const v of insertValues) {
if (insertedIds.has(v.songId)) {
added.push({ songId: v.songId, title: v.title, artist: v.artist })
} else {
skipped.push({ songId: v.songId, reason: '已存在于备选池' })
}
}
}
return { added, skipped, total: await fetchPoolCount() }
})