feat: add YesPlayMusic-style home page with daily recs, FM, and user playlists

Add backend endpoints for daily recommended songs, personal FM, user
playlists, and playlist detail. Extend NeteaseProvider and MusicProvider
interface with getDailyRecommendSongs and getUserPlaylists. Rewrite Home
page with FM card, daily recommendations grid, and user playlists section.
Fix Playlist view to fetch detail and songs from separate endpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-03-30 02:07:11 +08:00
1 parent 6b725f43f9
commit 67584d2622
5 files changed
+330 -16

No files matched your search

+35
View File
@@ -283,4 +283,39 @@ export class NeteaseProvider implements MusicProvider {
platform: "netease",
}));
}
async getDailyRecommendSongs(): Promise<Song[]> {
const res = await this.api.get("/recommend/songs", {
params: { ...this.cookieParams },
});
return (res.data?.data?.dailySongs ?? []).map((s: any) => ({
id: String(s.id),
name: s.name,
artist: (s.ar ?? []).map((a: any) => a.name).join(" / "),
album: s.al?.name ?? "",
duration: Math.round((s.dt ?? 0) / 1000),
coverUrl: s.al?.picUrl ?? "",
platform: "netease",
}));
}
async getUserPlaylists(): Promise<Playlist[]> {
// First get user ID from login status
const statusRes = await this.api.get("/login/status", {
params: { ...this.cookieParams },
});
const uid = statusRes.data?.data?.profile?.userId;
if (!uid) return [];
const res = await this.api.get("/user/playlist", {
params: { uid, ...this.cookieParams },
});
return (res.data?.playlist ?? []).map((p: any) => ({
id: String(p.id),
name: p.name,
coverUrl: p.coverImgUrl ?? "",
songCount: p.trackCount ?? 0,
platform: "netease",
}));
}
}
+2
View File
@@ -73,4 +73,6 @@ export interface MusicProvider {
getCookie(): string;
getAuthStatus(): Promise<AuthStatus>;
getPersonalFm?(): Promise<Song[]>;
getDailyRecommendSongs?(): Promise<Song[]>;
getUserPlaylists?(): Promise<Playlist[]>;
}
+80
View File
@@ -86,5 +86,85 @@ export function createMusicRouter(
}
});
router.get("/recommend/songs", async (req, res) => {
try {
const provider = getProvider(req.query.platform as string);
if (!provider.getDailyRecommendSongs) {
res.status(501).json({ error: "Not supported by this provider" });
return;
}
const songs = await provider.getDailyRecommendSongs();
res.json({ songs });
} catch (err) {
logger.error({ err }, "Get daily recommend songs failed");
res.status(500).json({ error: (err as Error).message });
}
});
router.get("/personal/fm", async (req, res) => {
try {
const provider = getProvider(req.query.platform as string);
if (!provider.getPersonalFm) {
res.status(501).json({ error: "Not supported by this provider" });
return;
}
const songs = await provider.getPersonalFm();
res.json({ songs });
} catch (err) {
logger.error({ err }, "Get personal FM failed");
res.status(500).json({ error: (err as Error).message });
}
});
router.get("/user/playlists", async (req, res) => {
try {
const provider = getProvider(req.query.platform as string);
if (!provider.getUserPlaylists) {
res.status(501).json({ error: "Not supported by this provider" });
return;
}
const playlists = await provider.getUserPlaylists();
res.json({ playlists });
} catch (err) {
logger.error({ err }, "Get user playlists failed");
res.status(500).json({ error: (err as Error).message });
}
});
router.get("/playlist/:id/detail", async (req, res) => {
try {
const provider = getProvider(req.query.platform as string);
// Use the playlist songs endpoint to get basic info,
// but we also need detail info (name, cover, description).
// For netease, we access the underlying API directly.
const nProvider = provider as any;
if (nProvider.api) {
const cookieParams = nProvider.cookie
? { cookie: nProvider.cookie }
: {};
const detailRes = await nProvider.api.get("/playlist/detail", {
params: { id: req.params.id, ...cookieParams },
});
const p = detailRes.data?.playlist;
if (p) {
res.json({
playlist: {
id: String(p.id),
name: p.name,
description: p.description ?? "",
coverUrl: p.coverImgUrl ?? "",
songCount: p.trackCount ?? 0,
},
});
return;
}
}
res.status(404).json({ error: "Playlist not found" });
} catch (err) {
logger.error({ err }, "Get playlist detail failed");
res.status(500).json({ error: (err as Error).message });
}
});
return router;
}