From 900191eca5c1f087e196da855671aaabd296bd1e Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Wed, 6 May 2026 14:05:48 +0800 Subject: [PATCH] fix(api): add getPlaylistDetail provider method for QQ playlist detail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /playlist/:id/detail route was hardcoded to call the NetEase API path /playlist/detail on whatever provider was selected. For QQ this hit a non-existent path, so clicking into any QQ playlist showed "歌单不存在或加载失败". Replace the platform-specific hack in the route handler with a proper getPlaylistDetail method on MusicProvider. Implement it for NetEase (porting the existing /playlist/detail logic) and QQ (calling /getSongListDetail and reading from response.cdlist[0]). Pre-existing bug exposed by the QQ source tab. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/music/netease.ts | 16 ++++++++++++++++ src/music/provider.ts | 9 +++++++++ src/music/qq.ts | 16 ++++++++++++++++ src/web/api/music.ts | 34 +++++++++------------------------- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/music/netease.ts b/src/music/netease.ts index e01772e..8aaeab6 100644 --- a/src/music/netease.ts +++ b/src/music/netease.ts @@ -3,6 +3,7 @@ import type { MusicProvider, Song, Playlist, + PlaylistDetail, LyricLine, SearchResult, QrCodeResult, @@ -319,6 +320,21 @@ export class NeteaseProvider implements MusicProvider { })); } + async getPlaylistDetail(playlistId: string): Promise { + const res = await this.api.get("/playlist/detail", { + params: { id: playlistId, ...this.cookieParams }, + }); + const p = res.data?.playlist; + if (!p) return null; + return { + id: String(p.id), + name: p.name ?? "", + description: p.description ?? "", + coverUrl: p.coverImgUrl ?? "", + songCount: p.trackCount ?? 0, + }; + } + async getUserPlaylists(): Promise { // First get user ID from login status const statusRes = await this.api.get("/login/status", { diff --git a/src/music/provider.ts b/src/music/provider.ts index 55a2c76..566ffba 100644 --- a/src/music/provider.ts +++ b/src/music/provider.ts @@ -20,6 +20,14 @@ export interface Playlist { platform: "netease" | "qq" | "bilibili" | "youtube"; } +export interface PlaylistDetail { + id: string; + name: string; + description: string; + coverUrl: string; + songCount: number; +} + export interface Album { id: string; name: string; @@ -77,4 +85,5 @@ export interface MusicProvider { getPersonalFm?(): Promise; getDailyRecommendSongs?(): Promise; getUserPlaylists?(): Promise; + getPlaylistDetail?(playlistId: string): Promise; } diff --git a/src/music/qq.ts b/src/music/qq.ts index 73ef2d0..1a96790 100644 --- a/src/music/qq.ts +++ b/src/music/qq.ts @@ -3,6 +3,7 @@ import type { MusicProvider, Song, Playlist, + PlaylistDetail, LyricLine, SearchResult, QrCodeResult, @@ -178,6 +179,21 @@ export class QQMusicProvider implements MusicProvider { })); } + async getPlaylistDetail(playlistId: string): Promise { + const res = await this.api.get("/getSongListDetail", { + params: { disstid: playlistId, ...this.cookieParams }, + }); + const cd = res.data?.response?.cdlist?.[0]; + if (!cd) return null; + return { + id: String(cd.disstid ?? cd.dissid ?? ""), + name: cd.dissname ?? "", + description: cd.desc ?? "", + coverUrl: cd.logo ?? "", + songCount: cd.songnum ?? cd.total_song_num ?? 0, + }; + } + async getRecommendPlaylists(): Promise { const res = await this.api.get("/getSongLists", { params: { categoryId: 10000000, pageSize: 10, ...this.cookieParams }, diff --git a/src/web/api/music.ts b/src/web/api/music.ts index 3690674..b08f9a2 100644 --- a/src/web/api/music.ts +++ b/src/web/api/music.ts @@ -168,32 +168,16 @@ export function createMusicRouter( 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; - } + if (!provider.getPlaylistDetail) { + res.status(501).json({ error: "Not supported by this provider" }); + return; } - res.status(404).json({ error: "Playlist not found" }); + const detail = await provider.getPlaylistDetail(req.params.id); + if (!detail) { + res.status(404).json({ error: "Playlist not found" }); + return; + } + res.json({ playlist: detail }); } catch (err) { logger.error({ err }, "Get playlist detail failed"); res.status(500).json({ error: (err as Error).message });