From ba44931ae512aeabeb2694e784023a70eebb13f2 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:58:12 +0800 Subject: [PATCH] [O] Filesystem -> mongodb --- src/lib/server/songs.ts | 84 ++++++++++++++++++++++++++++++ src/routes/+page.server.ts | 2 +- src/shared/songs.ts | 101 ------------------------------------- 3 files changed, 85 insertions(+), 102 deletions(-) create mode 100644 src/lib/server/songs.ts delete mode 100644 src/shared/songs.ts diff --git a/src/lib/server/songs.ts b/src/lib/server/songs.ts new file mode 100644 index 0000000..27018e9 --- /dev/null +++ b/src/lib/server/songs.ts @@ -0,0 +1,84 @@ +import * as ne from '@neteasecloudmusicapienhanced/api' +import { aiParseLyrics } from '../../shared/lyricsParse' +import type { NeteaseSongBrief } from '../../shared/types' +import { db } from './db' + +/** + * Functional wrapper to cache API results to MongoDB. + * @param collectionName Name of the MongoDB collection. + * @param fn Function to get data if not cached. + * @param keyFn Function to get cache key from argument. Defaults to using the argument directly. + * @returns Function that gets data from cache or calls fn and caches result. + */ +const cached = (collectionName: string, fn: (arg: T) => Promise, keyFn: (arg: T) => any = (x) => x): (arg: T, noCache?: boolean) => Promise => + async (arg: T, noCache = false): Promise => { + const key = keyFn(arg) + if (!noCache) { + const doc = await db.collection(collectionName).findOne({ _id: key }) + if (doc) return doc.data + } + const result = await fn(arg) + await db.collection(collectionName).replaceOne({ _id: key }, { _id: key, data: result }, { upsert: true }) + return result + } + +/** + * Convert a playlist reference (URL or ID) to an ID number. + */ +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') +} + +/** + * Get raw playlist data from cache or netease API. + */ +const getPlaylistRaw = cached('playlists', + async (id: number) => { + const pl = ((await ne.playlist_detail({ id })).body as any) + + // Save each song + for (const track of pl.playlist.tracks) { + await db.collection('songs').replaceOne({ _id: track.id }, { _id: track.id, data: track }, { upsert: true }) + } + return pl + }) + +export const getSongMeta = cached('songs', + async (songId: number) => { + const detail = await ne.song_detail({ ids: songId.toString() }) + return detail.body.songs[0] + }) + +export const parseBrief = (songData: any): NeteaseSongBrief => ({ + id: songData.id, + name: songData.name, + album: songData.al.name, + albumId: songData.al.id, + albumPic: songData.al.picUrl, + artists: songData.ar.map((ar: any) => ({ id: ar.id, name: ar.name })) +}) + +/** + * Get a list of songs from a playlist reference. + */ +export async function getSongsFromPlaylist(ref: string): Promise { + const playlistId = parsePlaylistRef(ref) + const plData = await getPlaylistRaw(playlistId) + return plData.playlist.tracks.map(parseBrief) +} + +interface NeteaseLyricsResponse { lrc: { lyric: string } } + +export const getLyricsRaw = cached('lyrics_raw', + async (songId: number) => (await ne.lyric({ id: songId })).body as any as NeteaseLyricsResponse +) + +export const getLyricsProcessed = cached('lyrics_processed', + async (songId: number) => { + const raw = await getLyricsRaw(songId) + return aiParseLyrics(raw.lrc.lyric) + }) + diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 605b07f..9406628 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,5 +1,5 @@ // import { log } from 'console'; -import { getSongMeta, parseBrief } from '../shared/songs'; +import { getSongMeta, parseBrief } from '../lib/server/songs'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ params }) => { diff --git a/src/shared/songs.ts b/src/shared/songs.ts deleted file mode 100644 index 88cde90..0000000 --- a/src/shared/songs.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as ne from '@neteasecloudmusicapienhanced/api'; -import * as fs from 'fs/promises'; -import * as path from 'path'; -import { aiParseLyrics } from './lyricsParse'; -import type { NeteaseSongBrief } from './types'; - -// Filesystem structure: -// $data/playlists/{playlistId}/detail.json -// $data/songs/{songId}/meta.json -// $data/songs/{songId}/cover.jpg -// $data/songs/{songId}/lyrics.json -// $data/songs/{songId}/lyricsProcessed.json - -/** - * Functional wrapper to cache API results to filesystem. - * @param filePath Function to get cache file path from argument. - * @param fn Function to get data if not cached. - * @returns Function that gets data from cache or calls fn and caches result. - */ -const cached = (filePath: (arg: T) => Promise, fn: (arg: T) => Promise): (arg: T, noCache?: boolean) => Promise => - async (arg: T, noCache = false): Promise => { - const file = await filePath(arg) - if (!noCache && await fs.stat(file).catch(() => false)) { - const data = await fs.readFile(file, 'utf-8') - return JSON.parse(data) as R - } - const result = await fn(arg) - await fs.mkdir(path.dirname(file), { recursive: true }) - await fs.writeFile(file, JSON.stringify(result), 'utf-8') - return result - } - -/** - * Convert a playlist reference (URL or ID) to an ID number. - */ -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') -} - -/** - * Get raw playlist data from cache or netease API. - */ -const getPlaylistRaw = cached( - async ({ id }: { id: number }) => path.join('data', 'playlists', `${id}`, 'detail.json'), - async ({ id }: { id: number }) => { - const pl = ((await ne.playlist_detail({ id })).body as any) - - // Save each song - for (const track of pl.playlist.tracks) { - const p = path.join('data', 'songs', `${track.id}`, 'meta.json') - await fs.mkdir(path.dirname(p), { recursive: true }) - await fs.writeFile(p, JSON.stringify(track), 'utf-8') - } - return pl - }) - -export const getSongMeta = cached( - async (songId: number) => path.join('data', 'songs', `${songId}`, 'meta.json'), - async (songId: number) => { - const detail = await ne.song_detail({ ids: songId.toString() }) - return detail.body.songs[0] - }) - -export const parseBrief = (songData: any): NeteaseSongBrief => ({ - id: songData.id, - name: songData.name, - album: songData.al.name, - albumId: songData.al.id, - albumPic: songData.al.picUrl, - artists: songData.ar.map((ar: any) => ({ id: ar.id, name: ar.name })) -}) - -/** - * Get a list of songs from a playlist reference. - */ -export async function getSongsFromPlaylist(ref: string): Promise { - const playlistId = parsePlaylistRef(ref) - const plData = await getPlaylistRaw({ id: playlistId }) - return plData.playlist.tracks.map(parseBrief) -} - -interface NeteaseLyricsResponse { lrc: { lyric: string } } - -export const getLyricsRaw = cached( - async (songId: number) => path.join('data', 'songs', `${songId}`, 'lyrics.json'), - async (songId: number) => (await ne.lyric({ id: songId })).body as any as NeteaseLyricsResponse -) - -export const getLyricsProcessed = cached( - async (songId: number) => path.join('data', 'songs', `${songId}`, 'lyricsProcessed.json'), - async (songId: number) => { - const raw = await getLyricsRaw(songId) - return aiParseLyrics(raw.lrc.lyric) - } -) - -// console.log((await getSongsFromPlaylist('580208139')).length) -// console.log(await getLyricsProcessed(25723366)) \ No newline at end of file