feat(api): 新增歌手与专辑接口,修复各平台死代码中的多处隐藏 bug
This commit is contained in:
1 parent
d142dd6096
commit
23c7ad91e4
14 files changed
+317
-115
No files matched your search
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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: '健康检查、平台信息、文档' },
|
||||
],
|
||||
},
|
||||
|
||||
+52
-1
@@ -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}`)
|
||||
|
||||
Reference in new issue
Block a user