mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
fix(qq): apply musicu.fcg upstream fixes + add playlist search
Per https://github.com/ZHANGTIANYAO1/teamspeak-music-bot/issues/61: - Remove searchid param (its presence now causes empty results) - Enforce num_per_page >= 10 (lower values return empty) - Fix search_type: 2 for albums, 3 for playlists (8 was "user") Now uses the fixed musicu.fcg as primary (supports songs + albums + playlists), with client_search_cp as fallback. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e68e287b7
commit
1d2da33d3c
1 file changed
+102
-82
+102
-82
@@ -12,20 +12,21 @@ import type {
|
|||||||
} from "./provider.js";
|
} from "./provider.js";
|
||||||
import { parseLyrics } from "./netease.js";
|
import { parseLyrics } from "./netease.js";
|
||||||
|
|
||||||
// Primary search client: c.y.qq.com/soso/fcgi-bin/client_search_cp (classic
|
// Primary search client: u.y.qq.com/cgi-bin/musicu.fcg (JSON sub-request
|
||||||
// endpoint, currently working without a cookie).
|
// batch). Was broken ca. 2026-05 due to two upstream API changes:
|
||||||
const qqSearchApi = axios.create({
|
// 1. searchid param must NOT be present (causes all lists to be empty)
|
||||||
baseURL: "https://c.y.qq.com",
|
// 2. num_per_page must be >= 10 (lower values return empty)
|
||||||
|
// Both fixes applied per https://github.com/ZHANGTIANYAO1/teamspeak-music-bot/issues/61
|
||||||
|
const qqMusicuApi = axios.create({
|
||||||
|
baseURL: "https://u.y.qq.com",
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: { referer: "https://y.qq.com" },
|
headers: { referer: "https://y.qq.com" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fallback search client: u.y.qq.com/cgi-bin/musicu.fcg used JSON sub-requests
|
// Fallback search client: c.y.qq.com/soso/fcgi-bin/client_search_cp (classic
|
||||||
// but started returning is_filter=-9 (empty results) for unauthenticated
|
// endpoint, song + album only, no playlist support).
|
||||||
// requests ca. 2026-05. Kept as a redundancy in case the primary endpoint
|
const qqSearchApi = axios.create({
|
||||||
// goes down or changes its response format.
|
baseURL: "https://c.y.qq.com",
|
||||||
const qqFallbackApi = axios.create({
|
|
||||||
baseURL: "https://u.y.qq.com",
|
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: { referer: "https://y.qq.com" },
|
headers: { referer: "https://y.qq.com" },
|
||||||
});
|
});
|
||||||
@@ -92,113 +93,132 @@ export class QQMusicProvider implements MusicProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string, limit = 20): Promise<SearchResult> {
|
async search(query: string, limit = 20): Promise<SearchResult> {
|
||||||
// Primary: c.y.qq.com client_search_cp (currently working).
|
// Primary: u.y.qq.com/cgi-bin/musicu.fcg — supports songs + albums +
|
||||||
const primary = await this.searchViaClientSearchCp(query, limit);
|
// playlists. Fixed per https://github.com/ZHANGTIANYAO1/teamspeak-music-bot/issues/61
|
||||||
|
// (removed searchid, num_per_page >= 10, corrected search_type values).
|
||||||
|
const primary = await this.searchViaMusicuFcg(query, limit);
|
||||||
if (primary) return primary;
|
if (primary) return primary;
|
||||||
|
|
||||||
// Fallback: u.y.qq.com musicu.fcg JSON sub-requests (may return empty
|
// Fallback: c.y.qq.com/soso/fcgi-bin/client_search_cp (song + album,
|
||||||
// when Tencent applies is_filter, but acts as a safety net in case the
|
// no playlist support). Kept as redundancy.
|
||||||
// primary endpoint format changes or goes down).
|
return this.searchViaClientSearchCp(query, limit);
|
||||||
return this.searchViaMusicuFcg(query, limit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Primary search via c.y.qq.com/soso/fcgi-bin/client_search_cp */
|
/** Primary search via u.y.qq.com/cgi-bin/musicu.fcg.
|
||||||
private async searchViaClientSearchCp(
|
*
|
||||||
|
* Two upstream API changes (2026-05) required fixes:
|
||||||
|
* 1. Omit `searchid` — its presence now causes all lists to be empty.
|
||||||
|
* 2. `num_per_page` >= 10 — lower values return empty.
|
||||||
|
* 3. `search_type: 2` for albums, `3` for playlists (8 was "user"). */
|
||||||
|
private async searchViaMusicuFcg(
|
||||||
query: string,
|
query: string,
|
||||||
limit: number
|
limit: number
|
||||||
): Promise<SearchResult | null> {
|
): Promise<SearchResult | null> {
|
||||||
try {
|
try {
|
||||||
const songParams = {
|
const numPerPage = Math.max(10, Math.min(limit, 50));
|
||||||
w: query,
|
const reqData = JSON.stringify({
|
||||||
format: "json",
|
req_0: {
|
||||||
p: 1,
|
module: "music.search.SearchCgiService",
|
||||||
n: Math.min(limit, 50),
|
method: "DoSearchForQQMusicDesktop",
|
||||||
type: 0,
|
param: { query, num_per_page: numPerPage, search_type: 0 },
|
||||||
cr: 1,
|
},
|
||||||
};
|
req_album: {
|
||||||
const albumParams = {
|
module: "music.search.SearchCgiService",
|
||||||
w: query,
|
method: "DoSearchForQQMusicDesktop",
|
||||||
format: "json",
|
param: { query, num_per_page: 10, search_type: 2 },
|
||||||
p: 1,
|
},
|
||||||
n: 5,
|
req_playlist: {
|
||||||
t: 8,
|
module: "music.search.SearchCgiService",
|
||||||
cr: 1,
|
method: "DoSearchForQQMusicDesktop",
|
||||||
};
|
param: { query, num_per_page: 10, search_type: 3 },
|
||||||
|
},
|
||||||
const [songRes, albumRes] = await Promise.allSettled([
|
});
|
||||||
qqSearchApi.get("/soso/fcgi-bin/client_search_cp", { params: songParams }),
|
const res = await qqMusicuApi.get("/cgi-bin/musicu.fcg", {
|
||||||
qqSearchApi.get("/soso/fcgi-bin/client_search_cp", { params: albumParams }),
|
params: { format: "json", data: reqData },
|
||||||
]);
|
});
|
||||||
|
|
||||||
const songList: any[] =
|
const songList: any[] =
|
||||||
songRes.status === "fulfilled"
|
res.data?.req_0?.data?.body?.song?.list ?? [];
|
||||||
? (songRes.value.data?.data?.song?.list ?? [])
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Treat zero songs as "endpoint returned no useful data" so we fall
|
|
||||||
// through to the fallback path.
|
|
||||||
if (songList.length === 0) return null;
|
if (songList.length === 0) return null;
|
||||||
|
|
||||||
const songs: Song[] = songList.map((s: any) => ({
|
const songs: Song[] = songList.map((s: any) => ({
|
||||||
id: String(s.songmid ?? s.songid ?? ""),
|
id: String(s.mid ?? s.id),
|
||||||
name: s.songname ?? s.name ?? "",
|
name: s.title ?? s.name ?? "",
|
||||||
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
|
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
|
||||||
album: s.albumname ?? s.album?.name ?? "",
|
album: s.album?.name ?? s.album?.title ?? "",
|
||||||
duration: s.interval ?? 0,
|
duration: s.interval ?? 0,
|
||||||
coverUrl: s.albummid
|
coverUrl: s.album?.mid
|
||||||
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.albummid}.jpg`
|
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.album.mid}.jpg`
|
||||||
: "",
|
: "",
|
||||||
platform: "qq",
|
platform: "qq",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const albumList: any[] =
|
const albumList: any[] = res.data?.req_album?.data?.body?.album?.list ?? [];
|
||||||
albumRes.status === "fulfilled"
|
|
||||||
? (albumRes.value.data?.data?.album?.list ?? [])
|
|
||||||
: [];
|
|
||||||
const albums = mapQqAlbums(albumList);
|
const albums = mapQqAlbums(albumList);
|
||||||
|
|
||||||
return { songs, playlists: [], albums };
|
const playlistList: any[] = res.data?.req_playlist?.data?.body?.songlist?.list ?? [];
|
||||||
|
const playlists: Playlist[] = playlistList.map((p: any) => ({
|
||||||
|
id: String(p.dissid ?? p.id ?? ""),
|
||||||
|
name: p.dissname ?? p.title ?? "",
|
||||||
|
coverUrl: p.imgurl ?? p.logo ?? "",
|
||||||
|
songCount: p.songnum ?? p.song_count ?? 0,
|
||||||
|
platform: "qq" as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { songs, playlists, albums };
|
||||||
} catch {
|
} catch {
|
||||||
return null; // network error or unexpected response → fall back
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fallback search via u.y.qq.com/cgi-bin/musicu.fcg */
|
/** Fallback search via c.y.qq.com/soso/fcgi-bin/client_search_cp */
|
||||||
private async searchViaMusicuFcg(
|
private async searchViaClientSearchCp(
|
||||||
query: string,
|
query: string,
|
||||||
limit: number
|
limit: number
|
||||||
): Promise<SearchResult> {
|
): Promise<SearchResult> {
|
||||||
const reqData = JSON.stringify({
|
const songParams = {
|
||||||
req_0: {
|
w: query,
|
||||||
module: "music.search.SearchCgiService",
|
format: "json",
|
||||||
method: "DoSearchForQQMusicDesktop",
|
p: 1,
|
||||||
param: { searchid: "1", query, num_per_page: Math.min(limit, 50), search_type: 0 },
|
n: Math.min(limit, 50),
|
||||||
},
|
type: 0,
|
||||||
req_album: {
|
cr: 1,
|
||||||
module: "music.search.SearchCgiService",
|
};
|
||||||
method: "DoSearchForQQMusicDesktop",
|
const albumParams = {
|
||||||
param: { searchid: "1", query, num_per_page: 5, search_type: 8 },
|
w: query,
|
||||||
},
|
format: "json",
|
||||||
});
|
p: 1,
|
||||||
const res = await qqFallbackApi.get("/cgi-bin/musicu.fcg", {
|
n: 5,
|
||||||
params: { format: "json", data: reqData },
|
t: 8,
|
||||||
});
|
cr: 1,
|
||||||
const list: any[] =
|
};
|
||||||
res.data?.req_0?.data?.body?.song?.list ?? [];
|
|
||||||
|
|
||||||
const songs: Song[] = list.map((s: any) => ({
|
const [songRes, albumRes] = await Promise.allSettled([
|
||||||
id: String(s.mid ?? s.id),
|
qqSearchApi.get("/soso/fcgi-bin/client_search_cp", { params: songParams }),
|
||||||
name: s.title ?? s.name ?? "",
|
qqSearchApi.get("/soso/fcgi-bin/client_search_cp", { params: albumParams }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const songList: any[] =
|
||||||
|
songRes.status === "fulfilled"
|
||||||
|
? (songRes.value.data?.data?.song?.list ?? [])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const songs: Song[] = songList.map((s: any) => ({
|
||||||
|
id: String(s.songmid ?? s.songid ?? ""),
|
||||||
|
name: s.songname ?? s.name ?? "",
|
||||||
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
|
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
|
||||||
album: s.album?.name ?? s.album?.title ?? "",
|
album: s.albumname ?? s.album?.name ?? "",
|
||||||
duration: s.interval ?? 0,
|
duration: s.interval ?? 0,
|
||||||
coverUrl: s.album?.mid
|
coverUrl: s.albummid
|
||||||
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.album.mid}.jpg`
|
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.albummid}.jpg`
|
||||||
: "",
|
: "",
|
||||||
platform: "qq",
|
platform: "qq",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const albumList: any[] = res.data?.req_album?.data?.body?.album?.list ?? [];
|
const albumList: any[] =
|
||||||
|
albumRes.status === "fulfilled"
|
||||||
|
? (albumRes.value.data?.data?.album?.list ?? [])
|
||||||
|
: [];
|
||||||
const albums = mapQqAlbums(albumList);
|
const albums = mapQqAlbums(albumList);
|
||||||
|
|
||||||
return { songs, playlists: [], albums };
|
return { songs, playlists: [], albums };
|
||||||
|
|||||||
Reference in new issue
Block a user