diff --git a/README.md b/README.md index 70768d9..db36719 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,10 @@ pnpm test # 运行冒烟测试(自动启动/关闭本地服务器) | `GET /boards/:source/:boardId` | 榜单详情 | | `GET /hot-search/:source` | 热搜列表 | | `GET /song-list/:source?page=1` | 歌单列表 | +| `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/mg) | | `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 9161014..53fbeba 100644 --- a/docs/API.md +++ b/docs/API.md @@ -29,7 +29,7 @@ | 方法 | 路径 | 说明 | | --- | --- | --- | | GET | `/health` | 健康检查(status / uptime / 平台数) | -| GET | `/sources` | 支持的平台及其音质、能力(search/url/lyric/pic/boards/hotSearch/songList) | +| GET | `/sources` | 支持的平台及其音质、能力(search/url/lyric/pic/boards/hotSearch/songList/singer/album) | | GET | `/` | 302 重定向到 `/docs` | | GET | `/docs` | Swagger UI 文档页 | | GET | `/redoc` | ReDoc 阅读版文档 | @@ -39,7 +39,7 @@ | 方法 | 路径 | 说明 | | --- | --- | --- | -| GET | `/search?name=稻香&singer=周杰伦&source=kw&page=1&limit=20` | 搜索歌曲;`source` 省略时聚合全部平台(排除 xm),失败的平台跳过 | +| GET | `/search?name=稻香&singer=周杰伦&source=kw&page=1&limit=20` | 搜索歌曲;`source` 省略时聚合全部平台,失败的平台跳过 | | GET | `/find?name=稻香&singer=周杰伦&albumName=魔杰座&interval=03:43&excludeSource=kw` | 跨平台精确匹配,按歌手/歌名/专辑/时长综合排序 | ### 歌曲资源 @@ -64,6 +64,17 @@ | GET | `/hot-search/:source` | 热搜词 | | GET | `/song-list/:source` | 歌单列表;可带 `page`/`sortId`/`tagId`。`sortId`/`tagId` 各平台词汇不同(kw `new`/`hot`、tx `5`/`2`、kw 标签为 `173-digest` 形态等),`sortId` 省略时用平台默认 | +### 歌手 & 专辑 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| 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 生效)。支持 kg/mg;kw 专辑上游接口已失效暂不支持 | + +歌手/专辑 id 来源:搜索结果条目包含 `albumId`;歌手 id 需从平台方获取(不在搜索结果中)。 + ## 响应示例 ```jsonc diff --git a/musicSdk/kg/index.ts b/musicSdk/kg/index.ts index fbe4131..b53bac5 100644 --- a/musicSdk/kg/index.ts +++ b/musicSdk/kg/index.ts @@ -6,6 +6,8 @@ import pic from './pic.ts' import lyric from './lyric.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.js' const kg = { @@ -15,6 +17,8 @@ const kg = { musicSearch, hotSearch, comment, + singer, + album, getMusicUrl(songInfo, type) { return apis('kg').getMusicUrl!(songInfo, type) }, diff --git a/musicSdk/kg/singer.ts b/musicSdk/kg/singer.ts index fa4a859..2eccd58 100644 --- a/musicSdk/kg/singer.ts +++ b/musicSdk/kg/singer.ts @@ -76,7 +76,7 @@ export default { info: { name: item.albumname, author: item.singername, - img: item.replaceAll('{size}', '480'), + img: String(item.imgurl || item.img || '').replace('{size}', '480'), desc: item.intro, }, } diff --git a/musicSdk/mg/album.js b/musicSdk/mg/album.js index 884c046..c192972 100644 --- a/musicSdk/mg/album.js +++ b/musicSdk/mg/album.js @@ -29,23 +29,21 @@ export default { play_count: listInfo.play_count, }, } - }, - /** + }, /** * 通过AlbumId获取专辑信息 * @param {*} id - * @param {*} page */ async getAlbumInfo(id) { const info = await createHttpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/resource/album/v2.0?albumId=${id}`) if (!info) return Promise.reject(new Error('Get album info error.')) return { - name: info.title, - image: info.imgItems.length ? info.imgItems[0].img : null, - desc: info.summary, - author: info.singer, - play_count: formatPlayCount(info.opNumItem.playNum), - total: info.totalCount, + name: info.title ?? null, + image: info.imgItems?.length ? info.imgItems[0].img : null, + desc: info.summary ?? null, + author: info.singer ?? null, + play_count: formatPlayCount(info.opNumItem?.playNum ?? 0), + total: parseInt(info.totalCount) || null, } }, } diff --git a/musicSdk/mg/index.ts b/musicSdk/mg/index.ts index 779ba5c..4cdf5b2 100644 --- a/musicSdk/mg/index.ts +++ b/musicSdk/mg/index.ts @@ -6,6 +6,7 @@ import pic from './pic.ts' import lyric from './lyric.ts' import hotSearch from './hotSearch.ts' import comment from './comment.ts' +import album from './album.js' // import tipSearch from './tipSearch.ts' const mg = { @@ -15,6 +16,7 @@ const mg = { leaderboard, hotSearch, comment, + album, getMusicUrl(songInfo, type) { return apis('mg').getMusicUrl!(songInfo, type) }, diff --git a/musicSdk/tx/index.ts b/musicSdk/tx/index.ts index 2d6793f..3199633 100644 --- a/musicSdk/tx/index.ts +++ b/musicSdk/tx/index.ts @@ -5,6 +5,7 @@ import musicSearch from './musicSearch.ts' import { apis } from '../api-source.ts' import hotSearch from './hotSearch.ts' import comment from './comment.ts' +import singer from './singer.ts' // import tipSearch from './tipSearch.ts' const tx = { @@ -14,6 +15,7 @@ const tx = { musicSearch, hotSearch, comment, + singer, getMusicUrl(songInfo, type) { return apis('tx')!.getMusicUrl(songInfo, type) diff --git a/musicSdk/tx/singer.ts b/musicSdk/tx/singer.ts index 39b326c..20a0a0e 100644 --- a/musicSdk/tx/singer.ts +++ b/musicSdk/tx/singer.ts @@ -61,38 +61,30 @@ export const filterMusicInfoItem = item => { /** - * 创建一个适用于TX的Http请求 - * @param {*} url - * @param {*} options - * @param {*} retryNum + * musicu.fcg 通用请求(comm 参数对齐榜单模块的可用配置)。 + * 注意:musichallSinger.GetSingerDetail 等歌手详情接口在该通道已停用(104403), + * 歌手名/统计改由歌单与专辑接口合成。 */ -const createMusicuFetch = async(data, options?, retryNum = 0) => { +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', - body: { - comm: { - cv: 4747474, - ct: 24, - format: 'json', - inCharset: 'utf-8', - outCharset: 'utf-8', - uin: 0, - }, - ...data, - }, + method: 'post', headers: { - 'User-Angent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + '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, options, ++retryNum) + return createMusicuFetch(data, ++retryNum) } - if (result.statusCode !== 200 || result.body.code != 0) return createMusicuFetch(data, options, ++retryNum) + if (result.statusCode !== 200 || result.body.code != 0) return createMusicuFetch(data, ++retryNum) return result.body } @@ -103,59 +95,28 @@ export default { * @param {*} id */ getInfo(id) { - return createMusicuFetch({ - req_1: { - module: 'music.musichallSinger.SingerInfoInter', - method: 'GetSingerDetail', - param: { - singer_mid: [id], - ex_singer: 1, - wiki_singer: 1, - group_singer: 0, - pic: 1, - photos: 0, - }, - }, - req_2: { - module: 'music.musichallAlbum.AlbumListServer', - method: 'GetAlbumList', - param: { - singerMid: id, - order: 0, - begin: 0, - num: 1, - songNumTag: 0, - singerID: 0, - }, - }, - req_3: { - module: 'musichall.song_list_server', - method: 'GetSingerSongList', - param: { - singerMid: id, - order: 1, - begin: 0, - num: 1, - }, - }, - }).then(body => { - if (body.req_1.code != 0 || body.req_2 != 0 || body.req_3 != 0) throw new Error('get singer info faild.') - - const info = body.req_1.data.singer_list[0] - const music = body.req_3.data - const album = body.req_3.data + return Promise.all([ + createMusicuFetch({ req: { module: 'musichall.song_list_server', method: 'GetSingerSongList', param: { singerMid: id, order: 1, begin: 0, num: 1 } } }), + createMusicuFetch({ req: { module: 'music.musichallAlbum.AlbumListServer', method: 'GetAlbumList', param: { singerMid: id, order: 0, begin: 0, num: 1, songNumTag: 0, singerID: 0 } } }), + ]).then(([songBody, albumBody]) => { + if (songBody.req.code != 0 || albumBody.req.code != 0) throw new Error('get singer info faild.') + const songData = songBody.req.data + const albumData = albumBody.req.data + // 详情接口停用:歌手名从歌曲条目反查,头像用平台的固定图片 URL 规则 + const singers = songData.songList?.[0]?.songInfo?.singer ?? [] + const self = singers.find(s => s.mid === id) ?? singers[0] return { source: 'tx', - id: info.basic_info.singer_mid, + id, info: { - name: info.basic_info.name, - desc: info.ex_info.desc, - avatar: info.pic.pic, - gender: info.ex_info.genre === 1 ? 'man' : 'woman', + name: self?.name ?? '', + desc: null, + avatar: `https://y.gtimg.cn/music/photo_new/T001R500x500M000${id}.jpg`, + gender: null, }, count: { - music: music.totalNum, - album: album.total, + music: songData.totalNum, + album: albumData.total, }, } }) @@ -167,7 +128,6 @@ export default { * @param {*} limit */ getAlbumList(id, page = 1, limit = 10) { - if (page === 1) page = 0 return createMusicuFetch({ req: { module: 'music.musichallAlbum.AlbumListServer', @@ -175,7 +135,7 @@ export default { param: { singerMid: id, order: 0, - begin: page * limit, + begin: (page - 1) * limit, num: limit, songNumTag: 0, singerID: 0, @@ -201,30 +161,28 @@ export default { * @param {*} limit */ async getSongList(id, page = 1, limit = 100) { - if (page === 1) page = 0 - return createMusicuFetch({ + const body = await createMusicuFetch({ req: { module: 'musichall.song_list_server', method: 'GetSingerSongList', param: { singerMid: id, order: 1, - begin: page * limit, + begin: (page - 1) * limit, num: limit, }, }, - }).then(body => { - if (body.req.code != 0) throw new Error('get singer song list faild.') - - const list = this.filterSongList(body.req.data.songList) - return { - source: 'tx', - list, - limit, - page, - total: body.req.data.totalNum, - } }) + if (body.req.code != 0) throw new Error('get singer song list faild.') + + const list = this.filterSongList(body.req.data.songList) + return { + source: 'tx', + list, + limit, + page, + total: body.req.data.totalNum, + } }, filterAlbumList(raw) { return raw.map(item => { @@ -242,7 +200,7 @@ export default { }) }, filterSongList(raw) { - raw.map(item => { + return raw.map(item => { return filterMusicInfoItem(item.songInfo) }) }, diff --git a/musicSdk/wy/index.ts b/musicSdk/wy/index.ts index 3222883..f208edf 100644 --- a/musicSdk/wy/index.ts +++ b/musicSdk/wy/index.ts @@ -6,6 +6,7 @@ import musicSearch from './musicSearch.ts' import songList from './songList.ts' import hotSearch from './hotSearch.ts' import comment from './comment.ts' +import singer from './singer.ts' // import tipSearch from './tipSearch.js' const wy = { @@ -15,6 +16,7 @@ const wy = { songList, hotSearch, comment, + singer, getMusicUrl(songInfo, type) { return apis('wy').getMusicUrl!(songInfo, type) }, diff --git a/musicSdk/wy/singer.ts b/musicSdk/wy/singer.ts index 17eab2d..c8f415c 100644 --- a/musicSdk/wy/singer.ts +++ b/musicSdk/wy/singer.ts @@ -8,20 +8,23 @@ export default { * @param {*} id */ getInfo(id) { - return (eapiRequest('/api/artist/head/info/get', { id }) as any).then(({ body }) => { + return eapiRequest('/api/artist/head/info/get', { id }).promise.then(({ body }) => { if (!body || body.code != 200) throw new Error('get singer info faild.') + // 响应结构为 { code, data: { artist: { ... } } },无 body.user + const artist = body.data?.artist + if (!artist) throw new Error('get singer info faild.') return { source: 'wy', - id: body.artist.id, + id: artist.id, info: { - name: body.artist.name, - desc: body.artist.briefDesc, - avatar: body.user.avatarUrl, - gender: body.user.gender === 1 ? 'man' : 'woman', + name: artist.name, + desc: artist.briefDesc, + avatar: artist.avatar ?? artist.cover ?? null, + gender: null, }, count: { - music: body.artist.musicSize, - album: body.artist.albumSize, + music: artist.musicSize, + album: artist.albumSize, }, } }) @@ -33,12 +36,11 @@ export default { * @param {*} limit */ getSongList(id, page = 1, limit = 100) { - if (page === 1) page = 0 - return (eapiRequest('/api/v2/artist/songs', { + return eapiRequest('/api/v2/artist/songs', { id, limit, - offset: limit * page, - }) as any).then(({ body }) => { + offset: (page - 1) * limit, + }).promise.then(({ body }) => { if (!body.songs || body.code != 200) throw new Error('get singer song list faild.') const list = this.filterSongList(body.songs) @@ -58,11 +60,10 @@ export default { * @param {*} limit */ getAlbumList(id, page = 1, limit = 10) { - if (page === 1) page = 0 - return (eapiRequest(`/api/artist/albums/${id}`, { + return eapiRequest(`/api/artist/albums/${id}`, { limit, - offset: limit * page, - }) as any).then(({ body }) => { + offset: (page - 1) * limit, + }).promise.then(({ body }) => { if (!body.hotAlbums || body.code != 200) throw new Error('get singer album list faild.') const list = this.filterAlbumList(body.hotAlbums) @@ -108,24 +109,28 @@ export default { _types['128k'] = { size, } + break case 320000: size = item.hMusic ? sizeFormate(item.hMusic.size) : null types.push({ type: '320k', size }) _types['320k'] = { size, } + break case 999000: size = item.sqMusic ? sizeFormate(item.sqMusic.size) : null types.push({ type: 'flac', size }) _types.flac = { size, } + break case 1999000: size = item.hrMusic ? sizeFormate(item.hrMusic.size) : null types.push({ type: 'flac24bit', size }) _types.flac24bit = { size, } + break } }) @@ -136,7 +141,7 @@ export default { albumId: item.album.id, songmid: item.id, source: 'wy', - interval: formatPlayTime(item.duration), + interval: formatPlayTime((item.dt ?? item.duration) / 1000), img: null, lrc: null, otherSource: null, diff --git a/src/openapi/components.js b/src/openapi/components.js index 1a19cb2..a1dc398 100644 --- a/src/openapi/components.js +++ b/src/openapi/components.js @@ -102,6 +102,96 @@ export const COMPONENT_SCHEMAS = { properties: { list: { type: 'array', items: ref('BoardItem') } }, additionalProperties: true, }, + SingerInfo: { + type: 'object', + properties: { + name: { type: 'string', description: '歌手名' }, + desc: { type: ['string', 'null'], description: '歌手简介' }, + avatar: { type: ['string', 'null'], description: '头像地址' }, + gender: { type: ['string', 'null'], description: '性别(man/woman)' }, + }, + additionalProperties: true, + }, + SingerInfoResult: { + type: 'object', + properties: { + source: { type: 'string' }, + id: { type: 'string', description: '平台歌手 id' }, + info: ref('SingerInfo'), + count: { + type: 'object', + properties: { + music: { type: ['integer', 'null'], description: '歌曲数' }, + album: { type: ['integer', 'null'], description: '专辑数' }, + }, + additionalProperties: true, + }, + }, + additionalProperties: true, + }, + SingerAlbumItem: { + type: 'object', + properties: { + id: { type: 'string', description: '专辑 id,可用于 /album/:source/:id' }, + mid: { type: 'string', description: 'tx 平台专辑 mid' }, + count: { type: ['integer', 'null'], description: '专辑曲目数' }, + info: { + type: 'object', + properties: { + name: { type: 'string' }, + author: { type: ['string', 'null'] }, + img: { type: ['string', 'null'] }, + desc: { type: ['string', 'null'] }, + }, + additionalProperties: true, + }, + }, + additionalProperties: true, + }, + SingerAlbumListResult: { + type: 'object', + properties: { + source: { type: 'string' }, + list: { type: 'array', items: ref('SingerAlbumItem') }, + page: { type: 'integer' }, + limit: { type: 'integer' }, + total: { type: ['integer', 'string', 'null'] }, + }, + additionalProperties: true, + }, + SingerSongListResult: { + type: 'object', + properties: { + source: { type: 'string' }, + list: { type: 'array', items: ref('SongInfo') }, + page: { type: 'integer' }, + limit: { type: 'integer' }, + total: { type: ['integer', 'string', 'null'] }, + }, + additionalProperties: true, + }, + AlbumInfo: { + type: 'object', + properties: { + name: { type: ['string', 'null'], description: '专辑名' }, + img: { type: ['string', 'null'], description: '专辑封面' }, + desc: { type: ['string', 'null'], description: '专辑简介' }, + author: { type: ['string', 'null'], description: '专辑作者' }, + }, + additionalProperties: true, + }, + AlbumDetailResult: { + type: 'object', + properties: { + source: { type: 'string' }, + info: ref('AlbumInfo'), + list: { type: 'array', items: ref('SongInfo'), description: '专辑曲目' }, + page: { type: 'integer' }, + limit: { type: ['integer', 'string', 'null'] }, + total: { type: ['integer', 'string', 'null'] }, + }, + additionalProperties: true, + }, SongItem: { type: 'object', properties: { @@ -179,6 +269,8 @@ export const COMPONENT_SCHEMAS = { boards: { type: 'boolean' }, hotSearch: { type: 'boolean' }, songList: { type: 'boolean' }, + singer: { type: 'boolean', description: '支持歌手接口(/singer/:source/:id)' }, + album: { type: 'boolean', description: '支持专辑接口(/album/:source/:id)' }, }, additionalProperties: true, }, diff --git a/src/routes.js b/src/routes.js index bb6f413..d78c454 100644 --- a/src/routes.js +++ b/src/routes.js @@ -285,6 +285,72 @@ export const musicRoutes = async(fastify) => { return songListHandler(request.params.source, page, sortId, tagId) }) + // ---------- 歌手 & 专辑 ---------- + + fastify.get('/singer/:source/:id', { + schema: { + summary: '获取歌手信息', + description: '支持 kg/tx/wy(见 /sources 的 singer 能力位)。', + tags: ['歌手专辑'], + params: params({ source: pSource('singer'), id: { type: 'string', description: '平台歌手 id' } }), + response: responses('SingerInfoResult'), + }, + }, async(request) => { + const { source, id } = request.params + return getSource(musicSdk, source, 'singer').singer + .getInfo(id).catch(err => { throw upstream(err.message) }) + }) + + fastify.get('/singer/:source/:id/songs', { + schema: { + summary: '获取歌手歌曲列表', + description: '支持 kg/tx/wy。', + tags: ['歌手专辑'], + params: params({ source: pSource('singer'), id: { type: 'string', description: '平台歌手 id' } }), + querystring: qs({ page: pPage, limit: pLimit }), + response: responses('SingerSongListResult'), + }, + }, async(request) => { + const { source, id } = request.params + const { page, limit } = request.query + return getSource(musicSdk, source, 'singer').singer + .getSongList(id, page, limit).catch(err => { throw upstream(err.message) }) + }) + + fastify.get('/singer/:source/:id/albums', { + schema: { + summary: '获取歌手专辑列表', + description: '支持 kg/tx/wy。', + tags: ['歌手专辑'], + params: params({ source: pSource('singer'), id: { type: 'string', description: '平台歌手 id' } }), + querystring: qs({ page: pPage, limit: pLimit }), + response: responses('SingerAlbumListResult'), + }, + }, async(request) => { + const { source, id } = request.params + const { page, limit } = request.query + return getSource(musicSdk, source, 'singer').singer + .getAlbumList(id, page, limit).catch(err => { throw upstream(err.message) }) + }) + + fastify.get('/album/:source/:id', { + schema: { + summary: '获取专辑详情与曲目列表', + description: '支持 kg/mg(见 /sources 的 album 能力位)。kw 的专辑上游接口已失效暂不支持。', + tags: ['歌手专辑'], + params: params({ source: pSource('album'), id: { type: 'string', description: '平台专辑 id(搜索结果中的 albumId)' } }), + querystring: qs({ page: pPage, limit: pLimit }), + response: responses('AlbumDetailResult'), + }, + }, async(request) => { + const { source, id } = request.params + const { page, limit } = request.query + const album = getSource(musicSdk, source, 'album').album + // kg/mg 为 getAlbumDetail(id, page, limit),其他平台适配器如只提供 getAlbumListDetail(id, page) 也可接入 + const fn = album.getAlbumDetail || album.getAlbumListDetail + return fn.call(album, id, page, limit).catch(err => { throw upstream(err.message) }) + }) + // ---------- 系统 ---------- fastify.get('/health', { @@ -327,6 +393,8 @@ export const musicRoutes = async(fastify) => { boards: Boolean(m.leaderboard), hotSearch: Boolean(m.hotSearch), songList: Boolean(m.songList), + singer: Boolean(m.singer), + album: Boolean(m.album), }, } }), diff --git a/src/server.js b/src/server.js index df67761..146dd63 100644 --- a/src/server.js +++ b/src/server.js @@ -10,6 +10,10 @@ // GET /boards/:source/:boardId 榜单详情 // GET /hot-search/:source 热搜词 // GET /song-list/:source 歌单列表 +// GET /singer/:source/:id 歌手信息(kg/tx/wy) +// GET /singer/:source/:id/songs 歌手歌曲(kg/tx/wy) +// GET /singer/:source/:id/albums 歌手专辑(kg/tx/wy) +// GET /album/:source/:id 专辑详情+曲目(kg/kw/mg) // GET /sources /health GET /docs /redoc /openapi.json // // 状态码语义:200 成功;422 参数校验失败;404 路由或平台不存在;405 方法不允许;502 上游平台失败;500 内部错误 @@ -139,6 +143,7 @@ fastify.register(swagger, { { name: '歌曲资源', description: '播放地址、歌词、封面' }, { name: '榜单', description: '排行榜' }, { name: '歌单', description: '歌单与热搜' }, + { name: '歌手专辑', description: '歌手信息、歌曲与专辑详情' }, { name: '系统', description: '健康检查、平台信息、文档' }, ], }, diff --git a/src/smoke-test.js b/src/smoke-test.js index 8786dc6..3453a1f 100644 --- a/src/smoke-test.js +++ b/src/smoke-test.js @@ -2,7 +2,7 @@ // 运行:node src/smoke-test.js // // 覆盖:搜索(各平台 + 聚合)、URL(kw/wy/tx)、歌词、封面、榜单详情、热搜、歌单、 -// 跨平台匹配 /find、路径参数风格路由、标准 HTTP 语义(404/405/422/302)、 +// 跨平台匹配 /find、歌手/专辑、路径参数风格路由、标准 HTTP 语义(404/405/422/302)、 // 文档页、旧 query 路由移除回归 import http from 'node:http' @@ -220,6 +220,57 @@ await test('歌单 默认排序 /song-list/kw', async() => { return `${r.data.list.length} 个歌单` }) +// ---- 歌手 & 专辑 ---- + +await test('歌手信息 /singer/kg', async() => { + const r = await get('/singer/kg/3062') + if (r.code != 0) throw new Error(r.msg) + if (!r.data?.info?.name) throw new Error('无歌手名') + return r.data.info.name +}) + +await test('歌手信息 /singer/wy', async() => { + const r = await get('/singer/wy/3684') + if (r.code != 0) throw new Error(r.msg) + if (!r.data?.info?.name) throw new Error('无歌手名') + return r.data.info.name +}) + +await test('歌手信息 /singer/tx', async() => { + const r = await get('/singer/tx/0025NhlN2yWrP4') + if (r.code != 0) throw new Error(r.msg) + if (!r.data?.info?.name) throw new Error('无歌手名') + return r.data.info.name +}) + +await test('歌手歌曲 /singer/tx/:id/songs', async() => { + const r = await get('/singer/tx/0025NhlN2yWrP4/songs?page=1&limit=3') + if (r.code != 0) throw new Error(r.msg) + if (!r.data?.list?.length) throw new Error('空列表') + return `${r.data.list.length} 首 / 共 ${r.data.total}` +}) + +await test('歌手专辑 /singer/kg/:id/albums', async() => { + const r = await get('/singer/kg/3062/albums?page=1&limit=3') + if (r.code != 0) throw new Error(r.msg) + if (!r.data?.list?.length) throw new Error('空列表') + return `${r.data.list.length} 张` +}) + +await test('专辑详情 /album/kg', async() => { + const r = await get('/album/kg/171089481') + if (r.code != 0) throw new Error(r.msg) + if (!r.data?.info?.name || !r.data?.list) 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) + if (!r.data?.info?.name) throw new Error('无专辑名') + return `${r.data.info.name}(${r.data.list?.length ?? 0} 首)` +}) + await test('参数校验 422(缺少 name)', async() => { const r = await getRaw('/search') if (r.status !== 422) throw new Error(`期望 422,实际 ${r.status}`)