feat(api): 列表接口分页统一 path 段——boards/song-list/singer songs与albums/album 支持可选 /:page,弃用 ?page=

This commit is contained in:
snowy-yang committed 2026-10-02 20:57:31 +08:00
1 parent baa34a501c
commit 436b102ae2
3 files changed
+94 -72

No files matched your search

+4 -4
View File
@@ -33,12 +33,12 @@ pnpm typecheck # 仅类型检查,不运行
| `GET /lyric/:source/:id` | 获取歌词(返回 lyric/tlyric/yrc) | | `GET /lyric/:source/:id` | 获取歌词(返回 lyric/tlyric/yrc) |
| `GET /pic/:source/:id` | 获取封面 | | `GET /pic/:source/:id` | 获取封面 |
| `GET /boards/:source` | 榜单列表 | | `GET /boards/:source` | 榜单列表 |
| `GET /boards/:source/:boardId` | 榜单详情 | | `GET /boards/:source/:boardId[/:page]` | 榜单详情(分页 path 段,第 1 页省略) |
| `GET /hot-search/:source` | 热搜列表 | | `GET /hot-search/:source` | 热搜列表 |
| `GET /song-list/:source?page=1` | 歌单列表 | | `GET /song-list/:source[/:page]` | 歌单列表(分页 path 段) |
| `GET /singer/:source/:id` | 歌手信息(kg/tx/wy) | | `GET /singer/:source/:id` | 歌手信息(kg/tx/wy) |
| `GET /singer/:source/:id/songs?page=1&limit=30` | 歌手歌曲列表(kg/tx/wy) | | `GET /singer/:source/:id/songs[/:page]?limit=30` | 歌手歌曲列表(kg/tx/wy) |
| `GET /singer/:source/:id/albums?page=1&limit=10` | 歌手专辑列表(kg/tx/wy) | | `GET /singer/:source/:id/albums[/:page]?limit=10` | 歌手专辑列表(kg/tx/wy) |
| `GET /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/tx/xm) | | `GET /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/tx/xm) |
| `POST /login/wy/qr` → `POST /login/wy/qr/check` | 网易云扫码登录(扫码后轮询完成登录) | | `POST /login/wy/qr` → `POST /login/wy/qr/check` | 网易云扫码登录(扫码后轮询完成登录) |
| `POST /login/wy/cellphone` | 网易云手机号登录(password 或 captcha) | | `POST /login/wy/cellphone` | 网易云手机号登录(password 或 captcha) |
+78 -63
View File
@@ -334,19 +334,24 @@ export const musicRoutes = async(fastify: any) => {
return m.leaderboard.getList(boardId, page).catch(err => { throw upstream(err.message) }) return m.leaderboard.getList(boardId, page).catch(err => { throw upstream(err.message) })
} }
fastify.get('/boards/:source/:boardId', { // 分页列表接口:页码为可选 path 段(第 1 页省略,下一页 /2),与 /search 同构
schema: { const pagedGet = (url: string, schemaFactory: (withPage: boolean) => any, handler: (request) => any) => {
summary: '获取榜单详情', fastify.get(url, { schema: schemaFactory(false) }, handler)
tags: ['榜单'], fastify.get(`${url}/:page`, { schema: schemaFactory(true) }, handler)
params: params({ }
source: pSource('boards'),
boardId: { type: 'string', description: '榜单 id,兼容榜单列表返回的带前缀 id(kw__93)与裸 bangid(93)', example: 'kw__93' }, const boardDetailSchema = (withPage: boolean) => ({
}), summary: '获取榜单详情',
querystring: qs({ page: pPage }), tags: ['榜单'],
response: responses('BoardDetailResult'), params: params({
}, source: pSource('boards'),
}, async(request) => boardHandler( boardId: { type: 'string', description: '榜单 id,兼容榜单列表返回的带前缀 id(kw__93)与裸 bangid(93)', example: 'kw__93' },
request.params.source, request.params.boardId, request.query.page, page: pPage,
}, withPage ? undefined : ['source', 'boardId']),
response: responses('BoardDetailResult'),
})
pagedGet('/boards/:source/:boardId', boardDetailSchema, async(request) => boardHandler(
request.params.source, request.params.boardId, Number(request.params.page) || 1,
)) ))
// ---------- 歌单 & 热搜 ---------- // ---------- 歌单 & 热搜 ----------
@@ -369,21 +374,23 @@ export const musicRoutes = async(fastify: any) => {
return m.songList.getList(sortId ?? m.songList.sortList?.[0]?.id, tagId, page).catch(err => { throw upstream(err.message) }) return m.songList.getList(sortId ?? m.songList.sortList?.[0]?.id, tagId, page).catch(err => { throw upstream(err.message) })
} }
fastify.get('/song-list/:source', { const songListSchema = (withPage: boolean) => ({
schema: { summary: '获取歌单列表',
summary: '获取歌单列表', tags: ['歌单'],
tags: ['歌单'], params: params({
params: params({ source: pSource('songList') }), source: pSource('songList'),
querystring: qs({ page: pPage,
page: pPage, }, withPage ? undefined : ['source']),
sortId: { type: ['integer', 'string'], description: '排序 id(平台相关,见 /sources 对应平台文档;省略时用平台默认)' }, querystring: qs({
tagId: { type: ['integer', 'string'], description: '标签 id(平台相关,kw 为 `173-digest` 形态字符串;0 为全部)' }, limit: pLimit,
}), sortId: { type: ['integer', 'string'], description: '排序 id(平台相关,见 /sources 对应平台文档;省略时用平台默认)' },
response: responses('SongListResult'), tagId: { type: ['integer', 'string'], description: '标签 id(平台相关,kw 为 `173-digest` 形态字符串;0 为全部)' },
}, }),
}, async(request) => { response: responses('SongListResult'),
const { page, sortId, tagId } = request.query })
return songListHandler(request.params.source, page, sortId, tagId) pagedGet('/song-list/:source', songListSchema, async(request) => {
const { sortId, tagId } = request.query
return songListHandler(request.params.source, Number(request.params.page) || 1, sortId, tagId)
}) })
// ---------- 歌手 & 专辑 ---------- // ---------- 歌手 & 专辑 ----------
@@ -402,50 +409,58 @@ export const musicRoutes = async(fastify: any) => {
.getInfo(id).catch(err => { throw upstream(err.message) }) .getInfo(id).catch(err => { throw upstream(err.message) })
}) })
fastify.get('/singer/:source/:id/songs', { const singerSongsSchema = (withPage: boolean) => ({
schema: { summary: '获取歌手歌曲列表',
summary: '获取歌手歌曲列表', description: '支持 kg/tx/wy。',
description: '支持 kg/tx/wy。', tags: ['歌手专辑'],
tags: ['歌手专辑'], params: params({
params: params({ source: pSource('singer'), id: { type: 'string', description: '平台歌手 id' } }), source: pSource('singer'),
querystring: qs({ page: pPage, limit: pLimit }), id: { type: 'string', description: '平台歌手 id' },
response: responses('SingerSongListResult'), page: pPage,
}, }, withPage ? undefined : ['source', 'id']),
}, async(request) => { querystring: qs({ limit: pLimit }),
response: responses('SingerSongListResult'),
})
pagedGet('/singer/:source/:id/songs', singerSongsSchema, async(request) => {
const { source, id } = request.params const { source, id } = request.params
const { page, limit } = request.query
return getSource(musicSdk, source, 'singer').singer return getSource(musicSdk, source, 'singer').singer
.getSongList(id, page, limit).catch(err => { throw upstream(err.message) }) .getSongList(id, Number(request.params.page) || 1, request.query.limit).catch(err => { throw upstream(err.message) })
}) })
fastify.get('/singer/:source/:id/albums', { const singerAlbumsSchema = (withPage: boolean) => ({
schema: { summary: '获取歌手专辑列表',
summary: '获取歌手专辑列表', description: '支持 kg/tx/wy。',
description: '支持 kg/tx/wy。', tags: ['歌手专辑'],
tags: ['歌手专辑'], params: params({
params: params({ source: pSource('singer'), id: { type: 'string', description: '平台歌手 id' } }), source: pSource('singer'),
querystring: qs({ page: pPage, limit: pLimit }), id: { type: 'string', description: '平台歌手 id' },
response: responses('SingerAlbumListResult'), page: pPage,
}, }, withPage ? undefined : ['source', 'id']),
}, async(request) => { querystring: qs({ limit: pLimit }),
response: responses('SingerAlbumListResult'),
})
pagedGet('/singer/:source/:id/albums', singerAlbumsSchema, async(request) => {
const { source, id } = request.params const { source, id } = request.params
const { page, limit } = request.query
return getSource(musicSdk, source, 'singer').singer return getSource(musicSdk, source, 'singer').singer
.getAlbumList(id, page, limit).catch(err => { throw upstream(err.message) }) .getAlbumList(id, Number(request.params.page) || 1, request.query.limit).catch(err => { throw upstream(err.message) })
}) })
fastify.get('/album/:source/:id', { const albumSchema = (withPage: boolean) => ({
schema: { summary: '获取专辑详情与曲目列表',
summary: '获取专辑详情与曲目列表', description: '支持 kg/kw/tx/xm/me(见 /sources 的 album 能力位)。kw 单页固定 1000 条、xm/me 固定 30 条、limit 不生效。',
description: '支持 kg/kw/tx/xm/me(见 /sources 的 album 能力位)。kw 单页固定 1000 条、xm/me 固定 30 条、limit 不生效。', tags: ['歌手专辑'],
tags: ['歌手专辑'], params: params({
params: params({ source: pSource('album'), id: { type: 'string', description: '平台专辑 id(搜索结果中的 albumId)' } }), source: pSource('album'),
querystring: qs({ page: pPage, limit: pLimit }), id: { type: 'string', description: '平台专辑 id(搜索结果中的 albumId)' },
response: responses('AlbumDetailResult'), page: pPage,
}, }, withPage ? undefined : ['source', 'id']),
}, async(request) => { querystring: qs({ limit: pLimit }),
response: responses('AlbumDetailResult'),
})
pagedGet('/album/:source/:id', albumSchema, async(request) => {
const { source, id } = request.params const { source, id } = request.params
const { page, limit } = request.query const { limit } = request.query
const page = Number(request.params.page) || 1
const album = getSource(musicSdk, source, 'album').album const album = getSource(musicSdk, source, 'album').album
// 各平台适配器签名不同:kg 为 getAlbumDetail(id, page, limit), // 各平台适配器签名不同:kg 为 getAlbumDetail(id, page, limit),
// kw 为 getAlbumListDetail(id, page)——第三参数是 retryNum,不能透传 limit // kw 为 getAlbumListDetail(id, page)——第三参数是 retryNum,不能透传 limit
+12 -5
View File
@@ -355,12 +355,19 @@ await test('平台列表 /sources', async() => {
await test('榜单详情 路径风格 /boards/:source/:boardId', async() => { await test('榜单详情 路径风格 /boards/:source/:boardId', async() => {
// 用列表返回的带前缀 id(回归:详情接口应自动归一化为裸 bangid) // 用列表返回的带前缀 id(回归:详情接口应自动归一化为裸 bangid)
const r = await get('/boards/kw/kw__93?page=1') const r = await get('/boards/kw/kw__93')
if (r.code != 0) throw new Error(r.msg) if (r.code != 0) throw new Error(r.msg)
if (!r.data?.list?.length) throw new Error('空列表') if (!r.data?.list?.length) throw new Error('空列表')
return `${r.data.list.length} 首` return `${r.data.list.length} 首`
}) })
// 列表接口分页 path 段(pagedGet 风格:第 1 页省略段,下一页 /2)
await test('榜单分页 /boards/:source/:boardId/:page', async() => {
const r = await get('/boards/kw/kw__93/2')
if (r.code != 0) throw new Error(r.msg)
return `${r.data?.list?.length} 首(第 2 页)`
})
await test('跨平台匹配 /find', async() => { await test('跨平台匹配 /find', async() => {
const r = await get(`/find/all/${encodeURIComponent('稻香')}?singer=${encodeURIComponent('周杰伦')}`) const r = await get(`/find/all/${encodeURIComponent('稻香')}?singer=${encodeURIComponent('周杰伦')}`)
if (r.code != 0) throw new Error(r.msg) if (r.code != 0) throw new Error(r.msg)
@@ -369,14 +376,14 @@ await test('跨平台匹配 /find', async() => {
}) })
await test('歌单 路径风格 /song-list/:source', async() => { await test('歌单 路径风格 /song-list/:source', async() => {
const r = await get('/song-list/kg?page=1') const r = await get('/song-list/kg')
if (r.code != 0) throw new Error(r.msg) if (r.code != 0) throw new Error(r.msg)
if (!r.data?.list?.length) throw new Error('空列表') if (!r.data?.list?.length) throw new Error('空列表')
return `${r.data.list.length} 个歌单` return `${r.data.list.length} 个歌单`
}) })
await test('歌单 默认排序 /song-list/kw', async() => { await test('歌单 默认排序 /song-list/kw', async() => {
const r = await get('/song-list/kw?page=1') const r = await get('/song-list/kw')
if (r.code != 0) throw new Error(r.msg) if (r.code != 0) throw new Error(r.msg)
if (!r.data?.list?.length) throw new Error('空列表(sortId 默认值未适配平台)') if (!r.data?.list?.length) throw new Error('空列表(sortId 默认值未适配平台)')
return `${r.data.list.length} 个歌单` return `${r.data.list.length} 个歌单`
@@ -406,14 +413,14 @@ await test('歌手信息 /singer/tx', async() => {
}) })
await test('歌手歌曲 /singer/tx/:id/songs', async() => { await test('歌手歌曲 /singer/tx/:id/songs', async() => {
const r = await get('/singer/tx/0025NhlN2yWrP4/songs?page=1&limit=3') const r = await get('/singer/tx/0025NhlN2yWrP4/songs?limit=3')
if (r.code != 0) throw new Error(r.msg) if (r.code != 0) throw new Error(r.msg)
if (!r.data?.list?.length) throw new Error('空列表') if (!r.data?.list?.length) throw new Error('空列表')
return `${r.data.list.length} 首 / 共 ${r.data.total}` return `${r.data.list.length} 首 / 共 ${r.data.total}`
}) })
await test('歌手专辑 /singer/kg/:id/albums', async() => { await test('歌手专辑 /singer/kg/:id/albums', async() => {
const r = await get('/singer/kg/3062/albums?page=1&limit=3') const r = await get('/singer/kg/3062/albums?limit=3')
if (r.code != 0) throw new Error(r.msg) if (r.code != 0) throw new Error(r.msg)
if (!r.data?.list?.length) throw new Error('空列表') if (!r.data?.list?.length) throw new Error('空列表')
return `${r.data.list.length} 张` return `${r.data.list.length} 张`