import { and, asc, eq, sql } from 'drizzle-orm' import { db } from '~/drizzle/db' import { schedules, songs, systemSettings, kuGouPlaylistSyncs } from '~/drizzle/schema' import { kuGouFetch } from '~~/server/services/kugouService' const WEEKDAY_ZH = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] function pad2(n: number) { return n < 10 ? '0' + n : '' + n } function dayKey(d: Date) { return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}` } function parseDate(s: string | Date) { const d = s instanceof Date ? new Date(s) : new Date(s) return new Date(d.getFullYear(), d.getMonth(), d.getDate()) } async function readSettings() { const rows = await db.select().from(systemSettings).limit(1) return rows[0] as any } async function fetchScheduleForDate(dayStart: Date) { const dayEnd = new Date(dayStart.getFullYear(), dayStart.getMonth(), dayStart.getDate() + 1) return await db .select({ scheduleId: schedules.id, sequence: schedules.sequence, songId: schedules.songId, playDate: schedules.playDate, title: songs.title, singer: songs.singer, artist: songs.artist, kgHash: songs.kgHash, musicId: songs.musicId, coverUrl: songs.coverUrl, cover: songs.cover, }) .from(schedules) .innerJoin(songs, eq(songs.id, schedules.songId)) .where(and(sql`${schedules.playDate} >= ${dayStart}`, sql`${schedules.playDate} < ${dayEnd}`, eq(schedules.isDraft, false))) .orderBy(asc(schedules.sequence), asc(schedules.playDate)) } async function ensureLogin(settings: any) { if (!settings?.kugouAdminToken && !settings?.kugouAdminCookie) { throw createError({ statusCode: 412, message: '尚未绑定酷狗管理员账号,请先在本页扫码绑定', }) } try { const res = await kuGouFetch('/login/status', { timestamp: Date.now() }) const ok = Number((res as any)?.status ?? 0) === 1 || !!(res as any)?.data?.profile || !!(res as any)?.data?.account if (!ok) { throw new Error('酷狗账号未登录或已过期,请重新扫码绑定') } } catch (e: any) { throw createError({ statusCode: 412, message: '酷狗登录状态无效:' + (e?.message || String(e)), }) } } async function searchKugouTrackFor(item: { title: string; singer?: string | null; artist?: string | null; kgHash?: string | null }) { const q = [item.title, item.singer || item.artist].filter(Boolean).join(' - ') const res = await kuGouFetch('/cloudsearch', { keywords: q, limit: 10, timestamp: Date.now(), }).catch(() => kuGouFetch('/search', { keywords: q, limit: 10, timestamp: Date.now(), }), ) const list: any[] = (res as any)?.result?.songs || (res as any)?.data?.info || (res as any)?.songs || [] if (!list.length) return null const targetL = item.title.trim().toLowerCase() const targetArtist = (item.singer || item.artist || '').trim().toLowerCase() const best = list.find((it) => { const nameL = (it.name || it.fileName || it.songname || '').toString().trim().toLowerCase() const artistL = ((it.ar && it.ar[0]?.name) || it.singerName || it.singername || '') .toString() .trim() .toLowerCase() return nameL === targetL && (!targetArtist || artistL.includes(targetArtist)) }) || list[0] const name = best.name || best.fileName || best.songname || item.title const hash = best.hash || best.fileHash || '' const albumId = best.album?.id ?? best.album_id ?? best.albumId ?? '' const audioId = best.id ?? best.album_audio_id ?? best.albumAudioId ?? '' return { name, hash, albumId: String(albumId || ''), audioId: String(audioId || ''), raw: best, } } export default defineEventHandler(async (event) => { const user = event.context.user if (!user) throw createError({ statusCode: 401, message: '未登录' }) if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) throw createError({ statusCode: 403, message: '仅管理员可同步排期' }) const body = await readBody(event) const rawDates: (string | Date)[] = Array.isArray(body?.dates) && body.dates.length ? body.dates : [] const playlistUrlTemplate = body.playlistUrlTemplate || 'https://www.kugou.com/yy/special/single/{listid}.html' if (!rawDates.length) throw createError({ statusCode: 400, message: '请选择至少一个排期日期' }) const settings = await readSettings() await ensureLogin(settings) const results: any[] = [] for (const d of rawDates) { const dayStart = parseDate(d as string) const key = dayKey(dayStart) const playlistName = `BYW广播 ${key} (${WEEKDAY_ZH[dayStart.getDay()]})` const scheduled = await fetchScheduleForDate(dayStart) const totalSongs = scheduled.length let playlistId = '' let playlistUrl = '' const missing: any[] = [] const addBatches: string[] = [] let added = 0 let status = 'pending' let error = '' let rawCreate: any = null let rawAdd: any = null try { if (!totalSongs) { status = 'skipped_empty' error = '当日没有已发布排期的歌曲' } else { const createRes = await kuGouFetch('/playlist/create', { name: playlistName, timestamp: Date.now(), }) rawCreate = createRes const createOk = Number((createRes as any)?.code ?? 0) === 200 || Number((createRes as any)?.status ?? 0) === 1 const listid = (createRes as any)?.playlist?.id || (createRes as any)?.data?.id || (createRes as any)?.id || (createRes as any)?.listid || '' if (!createOk || !listid) { status = 'failed_create' error = (createRes as any)?.message || (createRes as any)?.error || '歌单创建失败,请检查 sidecar / 酷狗账号权限' } else { playlistId = String(listid) playlistUrl = playlistUrlTemplate.replace('{listid}', playlistId) const tracks: string[] = [] for (const s of scheduled) { // 先直接用 kgHash / musicId 构造 track;没有就搜索 const hash = s.kgHash || '' const name = s.title const albId = '' const audioId = s.musicId || '' if (hash) { tracks.push([name, hash, albId, audioId].join('|')) } else { const hit = await searchKugouTrackFor(s) if (hit && hit.hash) tracks.push([hit.name, hit.hash, hit.albumId, hit.audioId].join('|')) else missing.push({ songId: s.songId, title: s.title, singer: s.singer || s.artist, reason: '酷狗未匹配到可加歌曲', }) } } added = tracks.length const CHUNK = 100 for (let i = 0; i < tracks.length; i += CHUNK) { const data = tracks.slice(i, i + CHUNK).join(',') const r = await kuGouFetch('/playlist/tracks', { op: 'add', pid: playlistId, trackIds: tracks[i], data, timestamp: Date.now(), }) rawAdd = rawAdd || [] rawAdd.push(r) const ok = Number((r as any)?.code ?? 0) === 200 || Number((r as any)?.status ?? 0) === 1 if (!ok) { status = 'partial_add_failed' error = (r as any)?.message || (r as any)?.error || '歌单添加歌曲失败' break } addBatches.push(`${i + 1}-${Math.min(i + CHUNK, tracks.length)}`) } if (status === 'pending') status = missing.length ? 'partial' : 'success' } } } catch (e: any) { status = 'error' error = e?.message || String(e) } const inserted = ( await db .insert(kuGouPlaylistSyncs) .values({ playDate: dayStart, playlistName, playlistId: playlistId || null, playlistUrl: playlistUrl || null, totalSongs, addedSongs: added, missingSongs: missing as any, status, error: error || null, triggeredByUserId: Number(user.id) || null, rawResponse: { create: rawCreate, add: rawAdd, batches: addBatches, } as any, createdAt: new Date(), updatedAt: new Date(), }) .returning() )[0] results.push({ id: inserted.id, date: key, playlistName, playlistId, playlistUrl, totalSongs, addedSongs: added, missingSongs: missing, status, error, }) } return { ok: true, results, } })