From 0e68e287b7e548193ea8835bae7a90a1b7061bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E6=A2=93=E5=96=B5=5F=E3=81=82=E3=81=9A=E3=81=AB?= =?UTF-8?q?=E3=82=83=E3=82=93?= Date: Thu, 14 May 2026 15:55:21 +0800 Subject: [PATCH] fix(qq): switch search to c.y.qq.com client_search_cp with u.y.qq.com fallback u.y.qq.com/cgi-bin/musicu.fcg started returning is_filter=-9 (empty results) for unauthenticated requests. Primary search now uses the classic c.y.qq.com/soso/fcgi-bin/client_search_cp endpoint, with the old musicu.fcg path kept as a fallback in case the primary endpoint changes format or goes down. Co-Authored-By: Claude Opus 4.7 --- src/music/qq.ts | 94 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/src/music/qq.ts b/src/music/qq.ts index 1e7a8ae..aed3994 100644 --- a/src/music/qq.ts +++ b/src/music/qq.ts @@ -12,9 +12,19 @@ import type { } from "./provider.js"; import { parseLyrics } from "./netease.js"; -// Direct QQ Music API client — bypasses the local API server for search -// because @sansenjian/qq-music-api still uses the broken c.y.qq.com endpoint. -const qqDirectApi = axios.create({ +// Primary search client: c.y.qq.com/soso/fcgi-bin/client_search_cp (classic +// endpoint, currently working without a cookie). +const qqSearchApi = axios.create({ + baseURL: "https://c.y.qq.com", + timeout: 10000, + headers: { referer: "https://y.qq.com" }, +}); + +// Fallback search client: u.y.qq.com/cgi-bin/musicu.fcg used JSON sub-requests +// but started returning is_filter=-9 (empty results) for unauthenticated +// requests ca. 2026-05. Kept as a redundancy in case the primary endpoint +// goes down or changes its response format. +const qqFallbackApi = axios.create({ baseURL: "https://u.y.qq.com", timeout: 10000, headers: { referer: "https://y.qq.com" }, @@ -82,6 +92,82 @@ export class QQMusicProvider implements MusicProvider { } async search(query: string, limit = 20): Promise { + // Primary: c.y.qq.com client_search_cp (currently working). + const primary = await this.searchViaClientSearchCp(query, limit); + if (primary) return primary; + + // Fallback: u.y.qq.com musicu.fcg JSON sub-requests (may return empty + // when Tencent applies is_filter, but acts as a safety net in case the + // primary endpoint format changes or goes down). + return this.searchViaMusicuFcg(query, limit); + } + + /** Primary search via c.y.qq.com/soso/fcgi-bin/client_search_cp */ + private async searchViaClientSearchCp( + query: string, + limit: number + ): Promise { + try { + const songParams = { + w: query, + format: "json", + p: 1, + n: Math.min(limit, 50), + type: 0, + cr: 1, + }; + const albumParams = { + w: query, + format: "json", + p: 1, + n: 5, + t: 8, + cr: 1, + }; + + const [songRes, albumRes] = await Promise.allSettled([ + qqSearchApi.get("/soso/fcgi-bin/client_search_cp", { params: songParams }), + qqSearchApi.get("/soso/fcgi-bin/client_search_cp", { params: albumParams }), + ]); + + const songList: any[] = + songRes.status === "fulfilled" + ? (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; + + 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(" / "), + album: s.albumname ?? s.album?.name ?? "", + duration: s.interval ?? 0, + coverUrl: s.albummid + ? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.albummid}.jpg` + : "", + platform: "qq", + })); + + const albumList: any[] = + albumRes.status === "fulfilled" + ? (albumRes.value.data?.data?.album?.list ?? []) + : []; + const albums = mapQqAlbums(albumList); + + return { songs, playlists: [], albums }; + } catch { + return null; // network error or unexpected response → fall back + } + } + + /** Fallback search via u.y.qq.com/cgi-bin/musicu.fcg */ + private async searchViaMusicuFcg( + query: string, + limit: number + ): Promise { const reqData = JSON.stringify({ req_0: { module: "music.search.SearchCgiService", @@ -94,7 +180,7 @@ export class QQMusicProvider implements MusicProvider { param: { searchid: "1", query, num_per_page: 5, search_type: 8 }, }, }); - const res = await qqDirectApi.get("/cgi-bin/musicu.fcg", { + const res = await qqFallbackApi.get("/cgi-bin/musicu.fcg", { params: { format: "json", data: reqData }, }); const list: any[] =