From 0f2585e5bda02641c62bdd3ab25731bc817c52b0 Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Thu, 17 Sep 2026 06:20:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(album):=20=E6=96=B0=E5=A2=9E=20QQ=20?= =?UTF-8?q?=E9=9F=B3=E4=B9=90(tx)=E4=B8=93=E8=BE=91=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/API.md | 2 +- musicSdk/tx/album.ts | 45 ++++++++++++++++++++++++++++++++++++++ musicSdk/tx/index.ts | 2 ++ musicSdk/tx/singer.ts | 35 ++++------------------------- musicSdk/tx/utils/index.ts | 28 ++++++++++++++++++++++++ src/smoke-test.js | 7 ++++++ 7 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 musicSdk/tx/album.ts diff --git a/README.md b/README.md index f7e52e5..17a7133 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ pnpm test # 运行冒烟测试(自动启动/关闭本地服务器) | `GET /singer/:source/:id` | 歌手信息(kg/tx/wy) | | `GET /singer/:source/:id/songs?page=1&limit=30` | 歌手歌曲列表(kg/tx/wy) | | `GET /singer/:source/:id/albums?page=1&limit=10` | 歌手专辑列表(kg/tx/wy) | -| `GET /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/mg) | +| `GET /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/mg/tx) | | `GET /sources` | 平台列表(音质与能力) | | `GET /health` | 健康检查 | | `GET /docs` `/redoc` `/openapi.json` | 接口文档(Swagger UI / ReDoc / OpenAPI 3.1) | diff --git a/docs/API.md b/docs/API.md index b6b65e0..867771e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -71,7 +71,7 @@ | GET | `/singer/:source/:id` | 歌手信息(name/desc/avatar 与歌曲/专辑数量)。支持 kg/tx/wy | | GET | `/singer/:source/:id/songs` | 歌手歌曲列表;可带 `page`/`limit`(默认 20)。支持 kg/tx/wy | | GET | `/singer/:source/:id/albums` | 歌手专辑列表;可带 `page`/`limit`。支持 kg/tx/wy | -| GET | `/album/:source/:id` | 专辑详情与曲目列表;可带 `page`/`limit`(limit 仅 kg 生效,kw 单页固定 1000 条)。支持 kg/kw/mg | +| GET | `/album/:source/:id` | 专辑详情与曲目列表;可带 `page`/`limit`(limit 仅 kg 生效,kw 单页固定 1000 条)。支持 kg/kw/mg/tx;tx 的专辑名/作者由曲目数据合成(desc 为空) | 歌手/专辑 id 来源:搜索结果条目包含 `albumId`;歌手 id 需从平台方获取(不在搜索结果中)。 diff --git a/musicSdk/tx/album.ts b/musicSdk/tx/album.ts new file mode 100644 index 0000000..fa92fe4 --- /dev/null +++ b/musicSdk/tx/album.ts @@ -0,0 +1,45 @@ +import { filterMusicInfoItem } from './singer.ts' +import { createMusicuFetch } from './utils/index.ts' + +export default { + /** + * 通过专辑 mid 获取专辑详情与曲目列表 + * @param {*} id 专辑 mid(搜索结果中的 albumId) + * @param {*} page + * @param {*} limit + */ + async getAlbumDetail(id, page = 1, limit = 100) { + const body = await createMusicuFetch({ + req: { + module: 'music.musichallAlbum.AlbumSongList', + method: 'GetAlbumSongList', + // 注意:参数名是驼峰 albumMid,下划线形态会被上游以 104400 拒绝 + param: { albumMid: id, begin: (page - 1) * limit, num: limit }, + }, + }) + if (body.req.code != 0) throw new Error('get album song list faild.') + + const data = body.req.data + const songList = data.songList ?? [] + // GetAlbumInfo 详情接口已停用:专辑名取自曲目自带的 album 信息, + // 作者由曲目歌手去重合成,封面用平台固定图片 URL 规则 + const first = songList[0]?.songInfo + const authors = [...new Set(songList.flatMap(item => item.songInfo?.singer?.map(s => s.name) ?? []))] + return { + source: 'tx', + info: { + name: first?.album?.name ?? null, + img: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${id}.jpg`, + desc: null, + author: authors.join('、') || null, + }, + list: this.filterSongList(songList), + page, + limit, + total: data.totalNum, + } + }, + filterSongList(raw) { + return (raw ?? []).map(item => filterMusicInfoItem(item.songInfo)) + }, +} diff --git a/musicSdk/tx/index.ts b/musicSdk/tx/index.ts index 3199633..c6d7ca8 100644 --- a/musicSdk/tx/index.ts +++ b/musicSdk/tx/index.ts @@ -6,6 +6,7 @@ import { apis } from '../api-source.ts' import hotSearch from './hotSearch.ts' import comment from './comment.ts' import singer from './singer.ts' +import album from './album.ts' // import tipSearch from './tipSearch.ts' const tx = { @@ -16,6 +17,7 @@ const tx = { hotSearch, comment, singer, + album, getMusicUrl(songInfo, type) { return apis('tx')!.getMusicUrl(songInfo, type) diff --git a/musicSdk/tx/singer.ts b/musicSdk/tx/singer.ts index 20a0a0e..459ac3f 100644 --- a/musicSdk/tx/singer.ts +++ b/musicSdk/tx/singer.ts @@ -1,8 +1,8 @@ -import type { HttpRequestObject, SongType, Quality, QualityInfo } from '../types.ts' -import { httpFetch } from '../request.ts' +import type { SongType, Quality, QualityInfo } from '../types.ts' import { formatPlayTime, sizeFormate } from '../index.ts' import { formatSingerName } from '../utils.ts' +import { createMusicuFetch } from './utils/index.ts' export const filterMusicInfoItem = item => { const types: SongType[] = [] @@ -60,35 +60,8 @@ export const filterMusicInfoItem = item => { } -/** - * musicu.fcg 通用请求(comm 参数对齐榜单模块的可用配置)。 - * 注意:musichallSinger.GetSingerDetail 等歌手详情接口在该通道已停用(104403), - * 歌手名/统计改由歌单与专辑接口合成。 - */ -const createMusicuFetch = async(data, retryNum = 0) => { - if (retryNum > 2) throw new Error('try max num') - - let result - try { - result = await httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { - method: 'post', - headers: { - 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', - }, - body: { - comm: { uin: 0, format: 'json', ct: 20, cv: 1859 }, - ...data, - }, - }).promise - } catch (err) { - console.log(err) - return createMusicuFetch(data, ++retryNum) - } - if (result.statusCode !== 200 || result.body.code != 0) return createMusicuFetch(data, ++retryNum) - - return result.body -} - +// 歌手详情接口(musichallSinger.GetSingerDetail 等)已停用(104403): +// 歌手名/统计由 GetSingerSongList 与 GetAlbumList 合成,头像走固定图片 URL 规则 export default { /** * 获取歌手信息 diff --git a/musicSdk/tx/utils/index.ts b/musicSdk/tx/utils/index.ts index 963a2da..f2b3753 100644 --- a/musicSdk/tx/utils/index.ts +++ b/musicSdk/tx/utils/index.ts @@ -15,3 +15,31 @@ export const signRequest = async(data) => { body: data, }).promise } + +/** + * musicu.fcg 通用请求(comm 参数对齐榜单模块的可用配置; + * musichallSinger/musichallAlbum 的部分详情接口在该通道已停用,见各模块说明) + */ +export const createMusicuFetch = async(data, retryNum = 0) => { + if (retryNum > 2) throw new Error('try max num') + + let result + try { + result = await httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'post', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + }, + body: { + comm: { uin: 0, format: 'json', ct: 20, cv: 1859 }, + ...data, + }, + }).promise + } catch (err) { + console.log(err) + return createMusicuFetch(data, ++retryNum) + } + if (result.statusCode !== 200 || result.body.code != 0) return createMusicuFetch(data, ++retryNum) + + return result.body +} diff --git a/src/smoke-test.js b/src/smoke-test.js index 10984ce..9e92f70 100644 --- a/src/smoke-test.js +++ b/src/smoke-test.js @@ -271,6 +271,13 @@ await test('专辑详情 /album/kw', async() => { return `${r.data.info.name}(${r.data.list.length} 首)` }) +await test('专辑详情 /album/tx', async() => { + const r = await get('/album/tx/000MkMni19ClKG') + if (r.code != 0) throw new Error(r.msg) + if (!r.data?.info?.name || !r.data?.list?.length) throw new Error('响应不完整') + return `${r.data.info.name}(${r.data.list.length} 首)` +}) + await test('专辑详情 /album/mg', async() => { const r = await get('/album/mg/25578') if (r.code != 0) throw new Error(r.msg)