[O] Filesystem -> mongodb

This commit is contained in:
2025-11-17 22:58:12 +08:00
parent 9103c213da
commit ba44931ae5
3 changed files with 85 additions and 102 deletions
+84
View File
@@ -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 = <T, R>(collectionName: string, fn: (arg: T) => Promise<R>, keyFn: (arg: T) => any = (x) => x): (arg: T, noCache?: boolean) => Promise<R> =>
async (arg: T, noCache = false): Promise<R> => {
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<any[]> {
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)
})
+1 -1
View File
@@ -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 }) => {
-101
View File
@@ -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 = <T, R>(filePath: (arg: T) => Promise<string>, fn: (arg: T) => Promise<R>): (arg: T, noCache?: boolean) => Promise<R> =>
async (arg: T, noCache = false): Promise<R> => {
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<any[]> {
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))