diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 28d65be..4f47eff 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -19,8 +19,6 @@ "Read(//tmp/**)" ], "deny": [ - "Bash(git push * main)", - "Bash(git push * master)", "Bash(git push --force *)", "Bash(rm -rf /)" ] diff --git a/src/bot/instance.ts b/src/bot/instance.ts index 5547d78..fabc4ed 100755 --- a/src/bot/instance.ts +++ b/src/bot/instance.ts @@ -638,9 +638,27 @@ export class BotInstance extends EventEmitter { } private async cmdAlbum(cmd: ParsedCommand): Promise { - if (!cmd.args) return "Usage: !album "; + if (!cmd.args) return "Usage: !album "; const provider = this.getProvider(cmd.flags); - const songs = await provider.getAlbumSongs(cmd.args); + + const id = this.extractId(cmd.args); + const isNumericId = /^\d+$/.test(cmd.args.trim()); + + let albumId: string; + + if (isNumericId || id !== cmd.args) { + // Input is a numeric ID or URL containing an ID — use directly + albumId = id; + } else { + // Name-based search + const result = await provider.search(cmd.args); + const albums = result.albums ?? []; + if (albums.length === 0) + return `No albums found for: ${cmd.args}`; + albumId = albums[0].id; + } + + const songs = await provider.getAlbumSongs(albumId); if (songs.length === 0) return "Album is empty or not found"; this.queue.clear(); diff --git a/src/music/netease.ts b/src/music/netease.ts index 4a863db..a9060b4 100644 --- a/src/music/netease.ts +++ b/src/music/netease.ts @@ -112,12 +112,12 @@ export class NeteaseProvider implements MusicProvider { params: { keywords: query, type: 1000, - limit: 5, + limit: 10, ...this.cookieParams, }, }), this.api.get("/cloudsearch", { - params: { keywords: query, type: 10, limit: 5, ...this.cookieParams }, + params: { keywords: query, type: 10, limit: 10, ...this.cookieParams }, }), ]); diff --git a/src/music/qq.ts b/src/music/qq.ts index 1e7a8ae..30dddec 100644 --- a/src/music/qq.ts +++ b/src/music/qq.ts @@ -12,14 +12,25 @@ 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: u.y.qq.com/cgi-bin/musicu.fcg (JSON sub-request +// batch). Was broken ca. 2026-05 due to two upstream API changes: +// 1. searchid param must NOT be present (causes all lists to be empty) +// 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, headers: { referer: "https://y.qq.com" }, }); +// Fallback search client: c.y.qq.com/soso/fcgi-bin/client_search_cp (classic +// endpoint, song + album only, no playlist support). +const qqSearchApi = axios.create({ + baseURL: "https://c.y.qq.com", + timeout: 10000, + 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({ @@ -82,37 +93,132 @@ export class QQMusicProvider implements MusicProvider { } async search(query: string, limit = 20): Promise { - const reqData = JSON.stringify({ - req_0: { - module: "music.search.SearchCgiService", - method: "DoSearchForQQMusicDesktop", - param: { searchid: "1", query, num_per_page: Math.min(limit, 50), search_type: 0 }, - }, - req_album: { - module: "music.search.SearchCgiService", - method: "DoSearchForQQMusicDesktop", - param: { searchid: "1", query, num_per_page: 5, search_type: 8 }, - }, - }); - const res = await qqDirectApi.get("/cgi-bin/musicu.fcg", { - params: { format: "json", data: reqData }, - }); - const list: any[] = - res.data?.req_0?.data?.body?.song?.list ?? []; + // Primary: u.y.qq.com/cgi-bin/musicu.fcg — supports songs + albums + + // 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; - const songs: Song[] = list.map((s: any) => ({ - id: String(s.mid ?? s.id), - name: s.title ?? s.name ?? "", + // Fallback: c.y.qq.com/soso/fcgi-bin/client_search_cp (song + album, + // no playlist support). Kept as redundancy. + return this.searchViaClientSearchCp(query, limit); + } + + /** Primary search via u.y.qq.com/cgi-bin/musicu.fcg. + * + * 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, + limit: number + ): Promise { + try { + const numPerPage = Math.max(10, Math.min(limit, 50)); + const reqData = JSON.stringify({ + req_0: { + module: "music.search.SearchCgiService", + method: "DoSearchForQQMusicDesktop", + param: { query, num_per_page: numPerPage, search_type: 0 }, + }, + req_album: { + module: "music.search.SearchCgiService", + method: "DoSearchForQQMusicDesktop", + param: { query, num_per_page: 10, search_type: 2 }, + }, + req_playlist: { + module: "music.search.SearchCgiService", + method: "DoSearchForQQMusicDesktop", + param: { query, num_per_page: 10, search_type: 3 }, + }, + }); + const res = await qqMusicuApi.get("/cgi-bin/musicu.fcg", { + params: { format: "json", data: reqData }, + }); + + const songList: any[] = + res.data?.req_0?.data?.body?.song?.list ?? []; + if (songList.length === 0) return null; + + const songs: Song[] = songList.map((s: any) => ({ + id: String(s.mid ?? s.id), + name: s.title ?? s.name ?? "", + artist: (s.singer ?? []).map((a: any) => a.name).join(" / "), + 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", + })); + + const albumList: any[] = res.data?.req_album?.data?.body?.album?.list ?? []; + const albums = mapQqAlbums(albumList); + + 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 { + return null; + } + } + + /** Fallback search via c.y.qq.com/soso/fcgi-bin/client_search_cp */ + private async searchViaClientSearchCp( + query: string, + limit: number + ): Promise { + 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 ?? []) + : []; + + 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.album?.name ?? s.album?.title ?? "", + album: s.albumname ?? s.album?.name ?? "", duration: s.interval ?? 0, - coverUrl: s.album?.mid - ? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.album.mid}.jpg` + coverUrl: s.albummid + ? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.albummid}.jpg` : "", 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); return { songs, playlists: [], albums }; diff --git a/web/src/views/Search.vue b/web/src/views/Search.vue index 8822277..9b303e0 100644 --- a/web/src/views/Search.vue +++ b/web/src/views/Search.vue @@ -20,42 +20,89 @@
搜索中...
-