From 9eef604c74ccd22854d18cf4d645a944bc866d55 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Fri, 21 Nov 2025 18:42:53 +0800 Subject: [PATCH] [+] Prepare page --- src/lib/client.ts | 5 ++ src/lib/server/songs.ts | 75 ++++++++++++++++++--- src/routes/api/song/[id]/prepare/+server.ts | 14 ++++ src/routes/song/[id]/+page.server.ts | 6 ++ src/routes/song/[id]/+page.svelte | 57 ++++++++++++++++ src/routes/song/[id]/play/+page.server.ts | 10 +-- 6 files changed, 153 insertions(+), 14 deletions(-) create mode 100644 src/routes/api/song/[id]/prepare/+server.ts create mode 100644 src/routes/song/[id]/+page.server.ts create mode 100644 src/routes/song/[id]/+page.svelte diff --git a/src/lib/client.ts b/src/lib/client.ts index a3e4c54..fd584f6 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -30,5 +30,10 @@ export const API = { user: { createSyncCode: async () => await post('/api/user/sync-code', {}), loginWithSyncCode: async (code: string) => await post('/api/auth/login', { code }) + }, + + song: { + prepare: async (id: number) => await post(`/api/song/${id}/prepare`, {}), + status: async (id: number) => await fetch(`/api/song/${id}/prepare`).then(res => res.json()) } } diff --git a/src/lib/server/songs.ts b/src/lib/server/songs.ts index eb4187e..fb65ef2 100644 --- a/src/lib/server/songs.ts +++ b/src/lib/server/songs.ts @@ -11,6 +11,9 @@ import path from 'path' const CACHE_DIR = path.resolve('static/audio') +const checkNetease = async () => (await ne.login_status({}) as any).body.data.account +const eToString = (e: any) => e.message ?? e.body.message + /** * Functional wrapper to cache API results to MongoDB. * @param collectionName Name of the MongoDB collection. @@ -37,17 +40,14 @@ function parsePlaylistRef(ref: string): number { const urlMatch = ref.match(/playlist\?id=(\d+)/) if (urlMatch) return +urlMatch[1] if (/^\d+$/.test(ref)) return +ref - throw new Error('Invalid playlist reference') + throw error(400, "无法识别歌单 URL... 只支持网易云的 URL 哦") } const getPlaylistRaw = cached('playlists_raw', async (id: number) => { const pl = ((await ne.playlist_detail({ id })).body as any).playlist - - // Save each song for (const track of pl.tracks) await db.collection('songs_raw').replaceOne({ _id: track.id }, { _id: track.id, data: track }, { upsert: true }) - return pl }) @@ -100,23 +100,81 @@ export const getSongUrl = async (id: number | string) => { const publicUrl = `/audio/${id}/standard.mp3` if (await fs.exists(filePath)) return publicUrl + // Check netease api status + if (await checkNetease() === null) throw error(500, '服务器的网易云账号坏掉了 :(') + console.log(`Downloading song ${id}...`) // @ts-ignore const res = await ne.song_url_v1({ id: id.toString(), level: 'standard' }) const url = (res.body as any).data?.[0]?.url - if (!url) throw error(404, 'Song URL not found') + if (!url) throw error(404, '没获取到歌曲 URL(是不是被下架了)') const audioRes = await fetch(url) - if (!audioRes.ok) throw error(500, 'Failed to download song') + if (!audioRes.ok) throw error(500, '歌曲下载失败') const buffer = await audioRes.arrayBuffer() + await fs.mkdir(path.dirname(filePath), { recursive: true }) await fs.writeFile(filePath, Buffer.from(buffer)) console.log(`Song ${id} cached to ${filePath}`) return publicUrl } +// ///////////////////////////////////////////////////////////////////////////// +// API for Song Preparation + +export interface ProgressItem { task: string, progress: number } +export interface SongProcessState { items: ProgressItem[], status: 'running' | 'done' | 'error' } + +const songProcessingStatus = new Map() +export const getSongStatus = (songId: number) => songProcessingStatus.get(songId) || { items: [], status: 'idle' } +export const checkLyricsProcessed = async (songId: number) => !!await db.collection('lyrics_processed').findOne({ _id: songId as any }) +export const prepareSong = async (songId: number) => { + if (songProcessingStatus.has(songId)) return + + const state: SongProcessState = { items: [], status: 'running' } + songProcessingStatus.set(songId, state) + + const addTask = (task: string) => ({ task, progress: 0 }).also(it => state.items.push(it)) + try { + // 1. Get Lyrics + const taskLyrics = addTask('从网易云获取歌词') + const raw = await getLyricsRaw(songId) + taskLyrics.progress = 1 + + if (raw.lang !== 'jpn') { + addTask('错误: 不是日语歌曲').progress = -1 + return state.status = 'error' + } + + // 2. AI Process + const taskAI = addTask('AI 标注歌词读音') + + // Check cache + if (await checkLyricsProcessed(songId)) taskAI.progress = 1 + else { + const lrc = await aiParseLyrics(raw.lrc.lyric, (i, n) => taskAI.progress = i / n) + await db.collection('lyrics_processed').replaceOne({ _id: songId as any }, { _id: songId, data: lrc }, { upsert: true }) + taskAI.progress = 1 + } + + // 3. Audio + const taskAudio = addTask('从网易云获取音乐') + await getSongUrl(songId) + taskAudio.progress = 1 + + state.status = 'done' + + } catch (e) { + addTask(`错误: ${eToString(e)}`).progress = -1 + state.status = 'error' + } +} + + +// ///////////////////////////////////////////////////////////////////////////// +// API for Netease Import export interface ImportSession { id: string @@ -128,7 +186,6 @@ export interface ImportSession { }[] done: boolean } - const sessions = new Map() export const getSession = (id: string) => sessions.get(id) @@ -156,7 +213,6 @@ export async function startImport(link: string, userId?: number): Promise { + const song = await getSongRaw(+params.id) + return { song } +} diff --git a/src/routes/song/[id]/+page.svelte b/src/routes/song/[id]/+page.svelte new file mode 100644 index 0000000..f5406cb --- /dev/null +++ b/src/routes/song/[id]/+page.svelte @@ -0,0 +1,57 @@ + + + + + + +{#if loadStatus === "done"} +
+ +
+{/if} diff --git a/src/routes/song/[id]/play/+page.server.ts b/src/routes/song/[id]/play/+page.server.ts index a95102a..578477c 100644 --- a/src/routes/song/[id]/play/+page.server.ts +++ b/src/routes/song/[id]/play/+page.server.ts @@ -1,15 +1,15 @@ -import type { PageServerLoad } from '../$types' -import { getLyricsProcessed, getSongRaw, getSongUrl, checkLyricsProcessed } from "$lib/server/songs.ts"; -import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types' +import { getLyricsProcessed, getSongRaw, getSongUrl, checkLyricsProcessed } from "$lib/server/songs.ts" +import { redirect } from '@sveltejs/kit' export const load: PageServerLoad = async ({ params }) => { const songId = +params.id const song = await getSongRaw(songId) const hasLrc = await checkLyricsProcessed(songId) - if (!hasLrc) throw redirect(302, `/song/${songId}/prepare`) + if (!hasLrc) throw redirect(302, `/song/${songId}`) - const lrc = await getLyricsProcessed(songId) + const lrc = await getLyricsProcessed(songId)! // const audioUrl = await getSongUrl(songId) return { song, lrc, audioUrl: undefined } } \ No newline at end of file