[+] My playlists

This commit is contained in:
2025-11-20 00:06:08 +08:00
parent 69c9e42a3c
commit 29d598c89b
13 changed files with 75 additions and 43 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ export const API = {
post,
saveUserData: async (data: Partial<UserData>) => await post('/api/user', data),
saveResult: async (data: Omit<ResultDocument, "_id" | "createdAt">) => 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 })
+11 -2
View File
@@ -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<ImportSession> {
export async function startImport(link: string, user: UserDocument): Promise<ImportSession> {
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 } }
)
}
}
+13 -6
View File
@@ -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 }
+3 -4
View File
@@ -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<UserDocument>("users")
@@ -15,7 +15,7 @@ void users.createIndex({ syncCode: 1 }, { name: "users_sync_code_idx" })
*/
export async function createUser(registUA: string): Promise<string> {
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<string> {
* @param session Session token
* @param data Data to update (partial)
*/
export async function updateUserData(session: string, data: Partial<UserData>): Promise<void> {
const user = await login(session)
export async function updateUserData(user: UserDocument, data: Partial<UserData>): Promise<void> {
const newData = { ...(user.data || {}), ...data }
await users.updateOne({ _id: user._id }, { $set: { data: newData } })
}
+5 -4
View File
@@ -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 }
}
+1 -1
View File
@@ -8,7 +8,7 @@
let { data }: PageProps = $props()
console.log(data.myPlaylists)
console.log(data.recPlaylists)
</script>
@@ -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')
+5 -6
View File
@@ -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)
+1 -1
View File
@@ -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)
+22 -6
View File
@@ -1,16 +1,32 @@
<script lang="ts">
import type { PageProps } from "./$types"
import AppBar from "../../../components/appbar/AppBar.svelte";
import Button from "../../../components/Button.svelte";
import type { PageProps } from "./$types"
import AppBar from "../../../components/appbar/AppBar.svelte";
import Button from "../../../components/Button.svelte";
import SongInfo from "../../../components/listitem/SongInfo.svelte";
import { API } from "$lib/client";
let { data }: PageProps = $props()
let { data }: PageProps = $props()
let {meta, songs} = data.playlist
let meta = $derived(data.playlist.meta)
let songs = $derived(data.playlist.songs)
let isFavorite = $derived(data.user.data.myPlaylists?.includes(meta.id) ?? false)
async function toggleFavorite() {
if (isFavorite) {
data.user.data.myPlaylists = data.user.data.myPlaylists?.filter(pl => pl !== meta.id)
} else {
data.user.data.myPlaylists = [...(data.user.data.myPlaylists || []), meta.id]
}
await API.saveUserData(data.user.data)
isFavorite = !isFavorite
}
</script>
<AppBar title="歌单详情" right={[
{icon: "i-material-symbols:bookmark-add-outline-rounded", onclick: () => 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')}
]} />
@@ -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 }
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -53,7 +53,7 @@ export interface UserDocument {
syncCodeCreated?: Date
// User data
data?: UserData
data: UserData
}
export interface ResultDocument {