From 29d598c89b7fd5176dbbae956109be9c6fe1caf2 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:06:08 +0800 Subject: [PATCH] [+] My playlists --- src/lib/client.ts | 2 +- src/lib/server/neteaseImport.ts | 13 +++++++-- src/lib/server/songs.ts | 19 +++++++++---- src/lib/server/user.ts | 7 ++--- src/routes/+page.server.ts | 9 +++--- src/routes/+page.svelte | 2 +- .../api/import/netease/start/+server.ts | 9 ++++-- src/routes/api/user/+server.ts | 11 ++++---- src/routes/playlist/[id]/+page.server.ts | 2 +- src/routes/playlist/[id]/+page.svelte | 28 +++++++++++++++---- .../playlists/[category]/+page.server.ts | 12 ++++---- src/routes/song/[id]/+page.server.ts | 2 +- src/shared/types.ts | 2 +- 13 files changed, 75 insertions(+), 43 deletions(-) diff --git a/src/lib/client.ts b/src/lib/client.ts index 2dd934b..d9319f1 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -21,7 +21,7 @@ export const API = { post, saveUserData: async (data: Partial) => await post('/api/user', 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 index d314487..d4acc7c 100644 --- a/src/lib/server/neteaseImport.ts +++ b/src/lib/server/neteaseImport.ts @@ -1,10 +1,11 @@ import { getSongsFromPlaylist, getLyricsRaw } from './songs' import { db } from './db' -import type { NeteaseSongBrief } from '../../shared/types' +import type { NeteaseSongBrief, UserDocument } from '../../shared/types' export interface ImportSession { id: string playlistId: number + userId: any songs: { song: NeteaseSongBrief status: 'importing' | 'success' | 'failed-not-japanese' | 'failed-unknown' @@ -19,15 +20,17 @@ export const getSession = (id: string) => sessions.get(id) /** * Start an import session * @param link Netease playlist link + * @param userId User ID to associate the imported playlist with * @returns Import session */ -export async function startImport(link: string): Promise { +export async function startImport(link: string, user: UserDocument): Promise { const { meta, songs } = await getSongsFromPlaylist(link) const importId = crypto.randomUUID() const session: ImportSession = { id: importId, playlistId: meta.id, + userId: user._id, songs: songs.map(s => ({ song: s, status: 'importing' })), done: false } @@ -86,5 +89,11 @@ async function processImport(session: ImportSession) { }, { upsert: true } ) + + // Add to user's favorites + await db.collection('users').updateOne( + { _id: session.userId }, + { $addToSet: { "data.myPlaylists": session.playlistId } } + ) } } diff --git a/src/lib/server/songs.ts b/src/lib/server/songs.ts index 371c4b5..a947b86 100644 --- a/src/lib/server/songs.ts +++ b/src/lib/server/songs.ts @@ -1,9 +1,11 @@ import * as ne from '@neteasecloudmusicapienhanced/api' import { aiParseLyrics } from './tools/lyrics' -import type { NeteaseSongBrief } from '../../shared/types' +import type { NeteaseSongBrief, UserDocument } from '../../shared/types' import { db } from './db' import { franc } from 'franc' import { error } from '@sveltejs/kit' +import type { ObjectId } from 'mongodb' +import '../../shared/ext' /** * Functional wrapper to cache API results to MongoDB. @@ -39,17 +41,22 @@ function parsePlaylistRef(ref: string): number { */ export const getPlaylistRaw = cached('playlists', async (id: number) => { - const pl = ((await ne.playlist_detail({ id })).body as any) + const pl = ((await ne.playlist_detail({ id })).body as any).playlist // Save each song - for (const track of pl.playlist.tracks) { + for (const track of pl.tracks) { await db.collection('songs').replaceOne({ _id: track.id }, { _id: track.id, data: track }, { upsert: true }) } return pl }) -export const listPlaylists = () => db.collection('playlists').find() - .map(it => it.data.playlist).toArray() +// TODO: A better recommendation system +export const listRecPlaylists = async () => await db.collection('playlists').find() + .map(it => it.data).toArray() + +export const listMyPlaylists = async (user: UserDocument) => (await user.data.myPlaylists?.let(pl => db.collection('playlists').find({ + _id: { $in: pl as any as ObjectId[] } +}).map(it => it.data).toArray())) ?? [] export const getSongMeta = cached('songs', async (songId: number) => { @@ -72,7 +79,7 @@ export const parseBrief = (songData: any): NeteaseSongBrief => ({ export async function getSongsFromPlaylist(ref: string): Promise<{meta: any, songs: NeteaseSongBrief[]}> { const playlistId = parsePlaylistRef(ref) const plData = await getPlaylistRaw(playlistId) - return {meta: plData.playlist, songs: plData.playlist.tracks.map(parseBrief)} + return {meta: plData, songs: plData.tracks.map(parseBrief)} } interface NeteaseLyricsResponse { lrc: { lyric: string }, lang: string } diff --git a/src/lib/server/user.ts b/src/lib/server/user.ts index 6e36750..02aa1df 100644 --- a/src/lib/server/user.ts +++ b/src/lib/server/user.ts @@ -1,5 +1,5 @@ import { db } from "./db" -import type { UserDocument, UserData } from "../../shared/types.ts"; +import { type UserDocument, type UserData } from "../../shared/types.ts"; import { error } from "@sveltejs/kit"; const users = db.collection("users") @@ -15,7 +15,7 @@ void users.createIndex({ syncCode: 1 }, { name: "users_sync_code_idx" }) */ export async function createUser(registUA: string): Promise { const ses = `${crypto.randomUUID()}-${Date.now().toString(36)}` - await users.insertOne({ registUA, createdAt: new Date(), sessions: [ses] }) + await users.insertOne({ registUA, createdAt: new Date(), sessions: [ses], data: {} }) return ses } @@ -49,8 +49,7 @@ export async function createSyncCode(session: string): Promise { * @param session Session token * @param data Data to update (partial) */ -export async function updateUserData(session: string, data: Partial): Promise { - const user = await login(session) +export async function updateUserData(user: UserDocument, data: Partial): Promise { const newData = { ...(user.data || {}), ...data } await users.updateOne({ _id: user._id }, { $set: { data: newData } }) } \ No newline at end of file diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 70e1a9e..3627ac8 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,10 +1,11 @@ // import { log } from 'console'; -import { getSongMeta, getSongsFromPlaylist, listPlaylists, parseBrief } from '../lib/server/songs'; +import { getSongMeta, listMyPlaylists, listRecPlaylists, parseBrief } from '../lib/server/songs'; import type { PageServerLoad } from './$types'; -export const load: PageServerLoad = async ({ params }) => { +export const load: PageServerLoad = async ({ params, parent }) => { let last = parseBrief(await getSongMeta(25723366)) - let myPlaylists = await listPlaylists() - let recPlaylists = myPlaylists + const { user } = await parent() + let myPlaylists = await listMyPlaylists(user) + let recPlaylists = await listRecPlaylists() return { last, myPlaylists, recPlaylists } } \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 42b805b..476529c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -8,7 +8,7 @@ let { data }: PageProps = $props() - console.log(data.myPlaylists) + console.log(data.recPlaylists) diff --git a/src/routes/api/import/netease/start/+server.ts b/src/routes/api/import/netease/start/+server.ts index 84a8533..9a41e5b 100644 --- a/src/routes/api/import/netease/start/+server.ts +++ b/src/routes/api/import/netease/start/+server.ts @@ -1,14 +1,17 @@ import { error, json } from '@sveltejs/kit'; import { startImport } from '$lib/server/neteaseImport'; +import { login } from '$lib/server/user'; import type { RequestHandler } from './$types'; -export const POST: RequestHandler = async ({ request }) => { +export const POST: RequestHandler = async ({ request, cookies }) => { const { link } = await request.json(); if (!link) throw error(400, 'Link is required') + const user = await login(cookies.get('session')) + if (!user) throw error(401, 'Unauthorized'); + try { - const session = await startImport(link) - return json(session) + return json(await startImport(link, user)) } catch (e) { console.error(e) throw error(500, 'Failed to start import') diff --git a/src/routes/api/user/+server.ts b/src/routes/api/user/+server.ts index 7d9b34d..3a562db 100644 --- a/src/routes/api/user/+server.ts +++ b/src/routes/api/user/+server.ts @@ -1,16 +1,15 @@ -import { json } from '@sveltejs/kit' +import { error, json } from '@sveltejs/kit' import { updateUserData } from '$lib/server/user' import type { RequestHandler } from './$types' +import { login } from '$lib/server/user' export const POST: RequestHandler = async ({ request, cookies }) => { - const session = cookies.get('session') - if (!session) { - return json({ error: 'Unauthorized' }, { status: 401 }) - } + const user = await login(cookies.get('session')) + if (!user) throw error(401, 'Unauthorized') try { const data = await request.json() - await updateUserData(session, data) + await updateUserData(user, data) return json({ success: true }) } catch (e) { console.error('Failed to update user data', e) diff --git a/src/routes/playlist/[id]/+page.server.ts b/src/routes/playlist/[id]/+page.server.ts index e9594fe..c449f09 100644 --- a/src/routes/playlist/[id]/+page.server.ts +++ b/src/routes/playlist/[id]/+page.server.ts @@ -1,5 +1,5 @@ import type { PageServerLoad } from './$types'; -import { getSongsFromPlaylist, listPlaylists } from "$lib/server/songs.ts"; +import { getSongsFromPlaylist, listRecPlaylists } from "$lib/server/songs.ts"; export const load: PageServerLoad = async ({ params }) => ({ playlist: await getSongsFromPlaylist(params.id) diff --git a/src/routes/playlist/[id]/+page.svelte b/src/routes/playlist/[id]/+page.svelte index 645ac92..e4f1fdc 100644 --- a/src/routes/playlist/[id]/+page.svelte +++ b/src/routes/playlist/[id]/+page.svelte @@ -1,16 +1,32 @@ alert('Bookmark clicked')}, + { + icon: isFavorite ? "i-material-symbols:bookmark-rounded" : "i-material-symbols:bookmark-add-outline-rounded", + onclick: toggleFavorite + }, {icon: "i-material-symbols:more-vert", onclick: () => alert('More clicked')} ]} /> diff --git a/src/routes/playlists/[category]/+page.server.ts b/src/routes/playlists/[category]/+page.server.ts index 04b6c0a..3f66ffb 100644 --- a/src/routes/playlists/[category]/+page.server.ts +++ b/src/routes/playlists/[category]/+page.server.ts @@ -1,11 +1,9 @@ import type { PageServerLoad } from './$types'; -import { listPlaylists } from "$lib/server/songs.ts"; +import { listMyPlaylists, listRecPlaylists } from "$lib/server/songs.ts"; -// TODO: slug should be "my" or "recommended", fetch accordingly -export const load: PageServerLoad = async ({ params }) => { +export const load: PageServerLoad = async ({ params, parent }) => { + const { user } = await parent() const isMine = params.category === 'my' - return { - playlists: await listPlaylists(), - isMine - } + const playlists = isMine ? await listMyPlaylists(user) : await listRecPlaylists() + return { playlists, isMine } } \ No newline at end of file diff --git a/src/routes/song/[id]/+page.server.ts b/src/routes/song/[id]/+page.server.ts index 3a15f10..740ada8 100644 --- a/src/routes/song/[id]/+page.server.ts +++ b/src/routes/song/[id]/+page.server.ts @@ -1,5 +1,5 @@ import type { PageServerLoad } from './$types' -import { getLyricsProcessed, getSongMeta, listPlaylists, parseBrief } from "$lib/server/songs.ts"; +import { getLyricsProcessed, getSongMeta, listRecPlaylists, parseBrief } from "$lib/server/songs.ts"; export const load: PageServerLoad = async ({ params }) => { const songId = +params.id diff --git a/src/shared/types.ts b/src/shared/types.ts index ec4cadd..fa627fe 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -53,7 +53,7 @@ export interface UserDocument { syncCodeCreated?: Date // User data - data?: UserData + data: UserData } export interface ResultDocument {