diff --git a/src/lib/client.ts b/src/lib/client.ts index fec5b66..2f0a2d5 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -14,5 +14,10 @@ export async function post(endpoint: string, data: any) { export const API = { post, saveUserData: async (data: Partial) => await post('/api/user', data), - saveResult: async (data: Omit) => await post('/api/result', data) + saveResult: async (data: Omit) => await post('/api/result', data), + + netease: { + startImport: async (link: string) => await post('/api/import/netease/start', { link }), + checkProgress: async (id: string) => await post('/api/import/netease/progress', { id }) + } } diff --git a/src/lib/server/neteaseImport.ts b/src/lib/server/neteaseImport.ts new file mode 100644 index 0000000..d314487 --- /dev/null +++ b/src/lib/server/neteaseImport.ts @@ -0,0 +1,90 @@ +import { getSongsFromPlaylist, getLyricsRaw } from './songs' +import { db } from './db' +import type { NeteaseSongBrief } from '../../shared/types' + +export interface ImportSession { + id: string + playlistId: number + songs: { + song: NeteaseSongBrief + status: 'importing' | 'success' | 'failed-not-japanese' | 'failed-unknown' + }[] + done: boolean +} + +const sessions = new Map() + +export const getSession = (id: string) => sessions.get(id) + +/** + * Start an import session + * @param link Netease playlist link + * @returns Import session + */ +export async function startImport(link: string): Promise { + const { meta, songs } = await getSongsFromPlaylist(link) + const importId = crypto.randomUUID() + + const session: ImportSession = { + id: importId, + playlistId: meta.id, + songs: songs.map(s => ({ song: s, status: 'importing' })), + done: false + } + + // If there is another session importing the same playlist, return it + if (sessions.has(importId)) { + console.log(`Import session ${importId} already exists`) + return sessions.get(importId)! + } + + sessions.set(importId, session) + + // Start background processing + processImport(session).catch(err => console.error('Import failed', err)) + + return session +} + +/** + * Process an import session + * @param session Session to store data for retriving progress + */ +async function processImport(session: ImportSession) { + const validSongs: NeteaseSongBrief[] = [] + console.log(`Starting import: Playlist ${session.playlistId}`) + + for (let i = 0; i < session.songs.length; i++) { + const item = session.songs[i] + try { + console.log(`Processing song ${item.song.id}`) + const lyrics = await getLyricsRaw(item.song.id) + if (lyrics.lang === 'jpn') { + item.status = 'success' + console.log(`Song ${item.song.id} is valid`) + validSongs.push(item.song) + } else { + item.status = 'failed-not-japanese' + console.log(`Song ${item.song.id} is not Japanese (is ${lyrics.lang})`) + } + } catch (e) { + console.error(`Failed to process song ${item.song.id}`, e) + item.status = 'failed-unknown' + } + } + + session.done = true + + // Save to database + if (validSongs.length > 0) { + await db.collection('playlists_imported').replaceOne( + { _id: session.playlistId as any }, + { + _id: session.playlistId, + songs: validSongs, + importedAt: new Date() + }, + { upsert: true } + ) + } +} diff --git a/src/lib/server/songs.ts b/src/lib/server/songs.ts index 132b034..371c4b5 100644 --- a/src/lib/server/songs.ts +++ b/src/lib/server/songs.ts @@ -88,7 +88,7 @@ export const getLyricsRaw = cached('lyrics_raw', export const getLyricsProcessed = cached('lyrics_processed', async (songId: number) => { const raw = await getLyricsRaw(songId) - if (raw.lang !== 'ja') throw error(400, 'Lyrics are not in Japanese') + if (raw.lang !== 'jpn') throw error(400, 'Lyrics are not in Japanese') console.log(`Processing lyrics for song ${songId}`) return aiParseLyrics(raw.lrc.lyric) }) diff --git a/src/routes/api/import/netease/progress/+server.ts b/src/routes/api/import/netease/progress/+server.ts new file mode 100644 index 0000000..fffc6e5 --- /dev/null +++ b/src/routes/api/import/netease/progress/+server.ts @@ -0,0 +1,13 @@ +import { error, json } from '@sveltejs/kit'; +import { getSession } from '$lib/server/neteaseImport'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ request }) => { + const { id } = await request.json() + if (!id) throw error(400, 'Import ID is required') + + const session = getSession(id) + if (!session) throw error(404, 'Session not found') + + return json(session); +}; diff --git a/src/routes/api/import/netease/start/+server.ts b/src/routes/api/import/netease/start/+server.ts new file mode 100644 index 0000000..84a8533 --- /dev/null +++ b/src/routes/api/import/netease/start/+server.ts @@ -0,0 +1,16 @@ +import { error, json } from '@sveltejs/kit'; +import { startImport } from '$lib/server/neteaseImport'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ request }) => { + const { link } = await request.json(); + if (!link) throw error(400, 'Link is required') + + try { + const session = await startImport(link) + return json(session) + } catch (e) { + console.error(e) + throw error(500, 'Failed to start import') + } +}; diff --git a/src/routes/import/netease/+page.server.ts b/src/routes/import/netease/+page.server.ts deleted file mode 100644 index cc7d774..0000000 --- a/src/routes/import/netease/+page.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { getSongsFromPlaylist, listPlaylists } from "$lib/server/songs.ts"; - -export const load: PageServerLoad = async ({ params }) => ({ - playlist: await getSongsFromPlaylist("14348145982") -}) diff --git a/src/routes/import/netease/+page.svelte b/src/routes/import/netease/+page.svelte index 221ecc0..6ac70a5 100644 --- a/src/routes/import/netease/+page.svelte +++ b/src/routes/import/netease/+page.svelte @@ -1,9 +1,9 @@ @@ -42,26 +88,31 @@ - {#if status === 'importing'} + {#if status !== 'idle'}
-
正在导入
+
+ {#if status === 'importing'}正在导入{:else if status === 'success'}导入完成{:else}导入出错{/if} +
{progress.done} / {progress.total} 首歌曲
- + {/if}
{#if status !== 'idle'} {#each songs as song} -
- - {song.song.name} +
+ +
+ {song.song.name} + {song.song.artists.map(a => a.name).join(', ')} +
{/each} {/if}
- +
\ No newline at end of file