[+] My playlists
This commit is contained in:
+1
-1
@@ -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 })
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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 } })
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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,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
@@ -53,7 +53,7 @@ export interface UserDocument {
|
||||
syncCodeCreated?: Date
|
||||
|
||||
// User data
|
||||
data?: UserData
|
||||
data: UserData
|
||||
}
|
||||
|
||||
export interface ResultDocument {
|
||||
|
||||
Reference in New Issue
Block a user