fix(api): add getPlaylistDetail provider method for QQ playlist detail

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) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.7 committed 2026-05-06 14:05:48 +08:00
1 parent e8d2f1ad98
commit 900191eca5
4 files changed
+50 -25

No files matched your search

+16
View File
@@ -3,6 +3,7 @@ import type {
MusicProvider, MusicProvider,
Song, Song,
Playlist, Playlist,
PlaylistDetail,
LyricLine, LyricLine,
SearchResult, SearchResult,
QrCodeResult, QrCodeResult,
@@ -319,6 +320,21 @@ export class NeteaseProvider implements MusicProvider {
})); }));
} }
async getPlaylistDetail(playlistId: string): Promise<PlaylistDetail | null> {
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<Playlist[]> { async getUserPlaylists(): Promise<Playlist[]> {
// First get user ID from login status // First get user ID from login status
const statusRes = await this.api.get("/login/status", { const statusRes = await this.api.get("/login/status", {
+9
View File
@@ -20,6 +20,14 @@ export interface Playlist {
platform: "netease" | "qq" | "bilibili" | "youtube"; platform: "netease" | "qq" | "bilibili" | "youtube";
} }
export interface PlaylistDetail {
id: string;
name: string;
description: string;
coverUrl: string;
songCount: number;
}
export interface Album { export interface Album {
id: string; id: string;
name: string; name: string;
@@ -77,4 +85,5 @@ export interface MusicProvider {
getPersonalFm?(): Promise<Song[]>; getPersonalFm?(): Promise<Song[]>;
getDailyRecommendSongs?(): Promise<Song[]>; getDailyRecommendSongs?(): Promise<Song[]>;
getUserPlaylists?(): Promise<Playlist[]>; getUserPlaylists?(): Promise<Playlist[]>;
getPlaylistDetail?(playlistId: string): Promise<PlaylistDetail | null>;
} }
+16
View File
@@ -3,6 +3,7 @@ import type {
MusicProvider, MusicProvider,
Song, Song,
Playlist, Playlist,
PlaylistDetail,
LyricLine, LyricLine,
SearchResult, SearchResult,
QrCodeResult, QrCodeResult,
@@ -178,6 +179,21 @@ export class QQMusicProvider implements MusicProvider {
})); }));
} }
async getPlaylistDetail(playlistId: string): Promise<PlaylistDetail | null> {
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<Playlist[]> { async getRecommendPlaylists(): Promise<Playlist[]> {
const res = await this.api.get("/getSongLists", { const res = await this.api.get("/getSongLists", {
params: { categoryId: 10000000, pageSize: 10, ...this.cookieParams }, params: { categoryId: 10000000, pageSize: 10, ...this.cookieParams },
+9 -25
View File
@@ -168,32 +168,16 @@ export function createMusicRouter(
router.get("/playlist/:id/detail", async (req, res) => { router.get("/playlist/:id/detail", async (req, res) => {
try { try {
const provider = getProvider(req.query.platform as string); const provider = getProvider(req.query.platform as string);
// Use the playlist songs endpoint to get basic info, if (!provider.getPlaylistDetail) {
// but we also need detail info (name, cover, description). res.status(501).json({ error: "Not supported by this provider" });
// For netease, we access the underlying API directly. return;
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" }); 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) { } catch (err) {
logger.error({ err }, "Get playlist detail failed"); logger.error({ err }, "Get playlist detail failed");
res.status(500).json({ error: (err as Error).message }); res.status(500).json({ error: (err as Error).message });