mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
fix(qq): correct user-playlists field mapping; add collected playlists and daily recommend
Three fixes to the QQ Music provider, all surfaced by enabling the QQ tab on Home/Library: 1. getUserPlaylists field names were wrong. The upstream fcg_get_profile_homepage actually returns title/picurl/subtitle, not dissname/imgurl/song_count, so all playlist cards rendered as empty placeholders. Map title->name, picurl->coverUrl, and parse '(\d+)首' from subtitle for songCount. 2. getUserPlaylists only returned playlists the user CREATED. Now also fetches COLLECTED playlists from c.y.qq.com fcg_get_profile_order_asset (reqtype=3, requires g_tk derived from p_skey cookie) and concatenates them after the created list — same order as the QQ desktop app. 3. Add getDailyRecommendSongs implementation backed by /getNewSongs (新歌速递, ~20 newest songs). The QQ provider previously didn't implement this method, so the daily-recommend QQ tab was empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ec02887d22
commit
e8d2f1ad98
1 file changed
+103
-13
+103
-13
@@ -18,6 +18,22 @@ const qqDirectApi = axios.create({
|
|||||||
headers: { referer: "https://y.qq.com" },
|
headers: { referer: "https://y.qq.com" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Direct client for c.y.qq.com endpoints (collected playlists / favorites).
|
||||||
|
// The bundled qq-music-api wrapper doesn't expose these endpoints.
|
||||||
|
const qqFavApi = axios.create({
|
||||||
|
baseURL: "https://c.y.qq.com",
|
||||||
|
timeout: 10000,
|
||||||
|
headers: { referer: "https://y.qq.com/" },
|
||||||
|
});
|
||||||
|
|
||||||
|
function computeGtk(pSkey: string): number {
|
||||||
|
let hash = 5381;
|
||||||
|
for (let i = 0; i < pSkey.length; i++) {
|
||||||
|
hash = (hash + (hash << 5) + pSkey.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
return hash & 0x7fffffff;
|
||||||
|
}
|
||||||
|
|
||||||
export class QQMusicProvider implements MusicProvider {
|
export class QQMusicProvider implements MusicProvider {
|
||||||
readonly platform = "qq" as const;
|
readonly platform = "qq" as const;
|
||||||
private api: AxiosInstance;
|
private api: AxiosInstance;
|
||||||
@@ -291,25 +307,99 @@ export class QQMusicProvider implements MusicProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserPlaylists(): Promise<Playlist[]> {
|
async getDailyRecommendSongs(): Promise<Song[]> {
|
||||||
if (!this.cookie) return [];
|
// QQ has no per-user daily list; use newsong.NewSongServer (新歌速递)
|
||||||
const uinMatch = /(?:^|; )uin=o?0?(\d+)/.exec(this.cookie);
|
// as the closest analogue. Returns ~20 newly-released songs.
|
||||||
const uin = uinMatch ? uinMatch[1] : "";
|
|
||||||
if (!uin) return [];
|
|
||||||
try {
|
try {
|
||||||
const res = await this.api.get("/user/getUserPlaylists", {
|
const res = await this.api.get("/getNewSongs", {
|
||||||
params: { uin, ...this.cookieParams },
|
params: { ...this.cookieParams },
|
||||||
});
|
});
|
||||||
if (res.data?.response?.code !== 0) return [];
|
const list: any[] = res.data?.response?.new_song?.data?.songlist ?? [];
|
||||||
return (res.data?.response?.data?.playlists ?? []).map((p: any) => ({
|
return list.map((s: any) => ({
|
||||||
id: String(p.dissid ?? p.id ?? ""),
|
id: String(s.mid ?? s.id),
|
||||||
name: p.dissname ?? p.name ?? "",
|
name: s.title ?? s.name ?? "",
|
||||||
coverUrl: p.imgurl ?? p.coverUrl ?? "",
|
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
|
||||||
songCount: p.song_count ?? p.listennum ?? 0,
|
album: s.album?.name ?? s.album?.title ?? "",
|
||||||
|
duration: s.interval ?? 0,
|
||||||
|
coverUrl: s.album?.mid
|
||||||
|
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.album.mid}.jpg`
|
||||||
|
: "",
|
||||||
platform: "qq",
|
platform: "qq",
|
||||||
}));
|
}));
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserPlaylists(): Promise<Playlist[]> {
|
||||||
|
if (!this.cookie) return [];
|
||||||
|
const uinMatch = /(?:^|; )uin=o?0?(\d+)/.exec(this.cookie);
|
||||||
|
const uin = uinMatch ? uinMatch[1] : "";
|
||||||
|
if (!uin) return [];
|
||||||
|
|
||||||
|
// Created and collected playlists come from two separate QQ endpoints.
|
||||||
|
// Run them in parallel and concatenate (created first, then collected),
|
||||||
|
// matching the order shown in the QQ Music desktop app.
|
||||||
|
const [created, collected] = await Promise.all([
|
||||||
|
this.fetchCreatedPlaylists(uin),
|
||||||
|
this.fetchCollectedPlaylists(uin),
|
||||||
|
]);
|
||||||
|
return [...created, ...collected];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchCreatedPlaylists(uin: string): Promise<Playlist[]> {
|
||||||
|
try {
|
||||||
|
const res = await this.api.get("/user/getUserPlaylists", {
|
||||||
|
params: { uin, ...this.cookieParams },
|
||||||
|
});
|
||||||
|
if (res.data?.response?.code !== 0) return [];
|
||||||
|
return (res.data?.response?.data?.playlists ?? []).map((p: any) => {
|
||||||
|
// fcg_get_profile_homepage returns title/picurl/subtitle ("X首 Y次播放").
|
||||||
|
const subtitle: string = p.subtitle ?? "";
|
||||||
|
const songCountFromSubtitle = parseInt(subtitle.match(/(\d+)\s*首/)?.[1] ?? "0", 10);
|
||||||
|
return {
|
||||||
|
id: String(p.dissid ?? p.id ?? ""),
|
||||||
|
name: p.title ?? p.dissname ?? p.name ?? "",
|
||||||
|
coverUrl: p.picurl ?? p.imgurl ?? p.coverUrl ?? "",
|
||||||
|
songCount: p.song_count ?? p.listennum ?? songCountFromSubtitle,
|
||||||
|
platform: "qq",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchCollectedPlaylists(uin: string): Promise<Playlist[]> {
|
||||||
|
// c.y.qq.com fav endpoint: reqtype=3 returns collected playlists (cdlist).
|
||||||
|
// Requires g_tk derived from the p_skey cookie.
|
||||||
|
const pSkeyMatch = /(?:^|; )p_skey=([^;]+)/.exec(this.cookie);
|
||||||
|
if (!pSkeyMatch) return [];
|
||||||
|
const gtk = computeGtk(pSkeyMatch[1]);
|
||||||
|
try {
|
||||||
|
const res = await qqFavApi.get("/fav/fcgi-bin/fcg_get_profile_order_asset.fcg", {
|
||||||
|
params: {
|
||||||
|
ct: 20,
|
||||||
|
cid: 205360956,
|
||||||
|
userid: uin,
|
||||||
|
reqtype: 3,
|
||||||
|
sin: 0,
|
||||||
|
ein: 29,
|
||||||
|
g_tk: gtk,
|
||||||
|
format: "json",
|
||||||
|
},
|
||||||
|
headers: { Cookie: this.cookie },
|
||||||
|
});
|
||||||
|
if (res.data?.code !== 0) return [];
|
||||||
|
return (res.data?.data?.cdlist ?? []).map((p: any) => ({
|
||||||
|
id: String(p.dissid ?? ""),
|
||||||
|
name: p.dissname ?? "",
|
||||||
|
coverUrl: p.logo ?? "",
|
||||||
|
songCount: p.songnum ?? 0,
|
||||||
|
platform: "qq" as const,
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user