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 /pic/:source/:id` | 获取封面 |
| `GET /boards/:source` | 榜单列表 |
| `GET /boards/:source/:boardId` | 榜单详情 |
| `GET /boards/:source/:boardId[/:page]` | 榜单详情(分页 path 段,第 1 页省略) |
| `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/songs?page=1&limit=30` | 歌手歌曲列表(kg/tx/wy) |
| `GET /singer/:source/:id/albums?page=1&limit=10` | 歌手专辑列表(kg/tx/wy) |
| `GET /singer/:source/:id/songs[/:page]?limit=30` | 歌手歌曲列表(kg/tx/wy) |
| `GET /singer/:source/:id/albums[/:page]?limit=10` | 歌手专辑列表(kg/tx/wy) |
| `GET /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/tx/xm) |
| `POST /login/wy/qr` → `POST /login/wy/qr/check` | 网易云扫码登录(扫码后轮询完成登录) |
| `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) })
}
fastify.get('/boards/:source/:boardId', {
schema: {
summary: '获取榜单详情',
tags: ['榜单'],
params: params({
source: pSource('boards'),
boardId: { type: 'string', description: '榜单 id,兼容榜单列表返回的带前缀 id(kw__93)与裸 bangid(93)', example: 'kw__93' },
}),
querystring: qs({ page: pPage }),
response: responses('BoardDetailResult'),
},
}, async(request) => boardHandler(
request.params.source, request.params.boardId, request.query.page,
// 分页列表接口:页码为可选 path 段(第 1 页省略,下一页 /2),与 /search 同构
const pagedGet = (url: string, schemaFactory: (withPage: boolean) => any, handler: (request) => any) => {
fastify.get(url, { schema: schemaFactory(false) }, handler)
fastify.get(`${url}/:page`, { schema: schemaFactory(true) }, handler)
}
const boardDetailSchema = (withPage: boolean) => ({
summary: '获取榜单详情',
tags: ['榜单'],
params: params({
source: pSource('boards'),
boardId: { type: 'string', description: '榜单 id,兼容榜单列表返回的带前缀 id(kw__93)与裸 bangid(93)', example: 'kw__93' },
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) })
}
fastify.get('/song-list/:source', {
schema: {
summary: '获取歌单列表',
tags: ['歌单'],
params: params({ source: pSource('songList') }),
querystring: qs({
page: pPage,
sortId: { type: ['integer', 'string'], description: '排序 id(平台相关,见 /sources 对应平台文档;省略时用平台默认)' },
tagId: { type: ['integer', 'string'], description: '标签 id(平台相关,kw 为 `173-digest` 形态字符串;0 为全部)' },
}),
response: responses('SongListResult'),
},
}, async(request) => {
const { page, sortId, tagId } = request.query
return songListHandler(request.params.source, page, sortId, tagId)
const songListSchema = (withPage: boolean) => ({
summary: '获取歌单列表',
tags: ['歌单'],
params: params({
source: pSource('songList'),
page: pPage,
}, withPage ? undefined : ['source']),
querystring: qs({
limit: pLimit,
sortId: { type: ['integer', 'string'], description: '排序 id(平台相关,见 /sources 对应平台文档;省略时用平台默认)' },
tagId: { type: ['integer', 'string'], description: '标签 id(平台相关,kw 为 `173-digest` 形态字符串;0 为全部)' },
}),
response: responses('SongListResult'),
})
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) })
})
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 singerSongsSchema = (withPage: boolean) => ({
summary: '获取歌手歌曲列表',
description: '支持 kg/tx/wy。',
tags: ['歌手专辑'],
params: params({
source: pSource('singer'),
id: { type: 'string', description: '平台歌手 id' },
page: pPage,
}, withPage ? undefined : ['source', 'id']),
querystring: qs({ limit: pLimit }),
response: responses('SingerSongListResult'),
})
pagedGet('/singer/:source/:id/songs', singerSongsSchema, 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) })
.getSongList(id, Number(request.params.page) || 1, request.query.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 singerAlbumsSchema = (withPage: boolean) => ({
summary: '获取歌手专辑列表',
description: '支持 kg/tx/wy。',
tags: ['歌手专辑'],
params: params({
source: pSource('singer'),
id: { type: 'string', description: '平台歌手 id' },
page: pPage,
}, withPage ? undefined : ['source', 'id']),
querystring: qs({ limit: pLimit }),
response: responses('SingerAlbumListResult'),
})
pagedGet('/singer/:source/:id/albums', singerAlbumsSchema, 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) })
.getAlbumList(id, Number(request.params.page) || 1, request.query.limit).catch(err => { throw upstream(err.message) })
})
fastify.get('/album/:source/:id', {
schema: {
summary: '获取专辑详情与曲目列表',
description: '支持 kg/kw/tx/xm/me(见 /sources 的 album 能力位)。kw 单页固定 1000 条、xm/me 固定 30 条、limit 不生效。',
tags: ['歌手专辑'],
params: params({ source: pSource('album'), id: { type: 'string', description: '平台专辑 id(搜索结果中的 albumId)' } }),
querystring: qs({ page: pPage, limit: pLimit }),
response: responses('AlbumDetailResult'),
},
}, async(request) => {
const albumSchema = (withPage: boolean) => ({
summary: '获取专辑详情与曲目列表',
description: '支持 kg/kw/tx/xm/me(见 /sources 的 album 能力位)。kw 单页固定 1000 条、xm/me 固定 30 条、limit 不生效。',
tags: ['歌手专辑'],
params: params({
source: pSource('album'),
id: { type: 'string', description: '平台专辑 id(搜索结果中的 albumId)' },
page: pPage,
}, withPage ? undefined : ['source', 'id']),
querystring: qs({ limit: pLimit }),
response: responses('AlbumDetailResult'),
})
pagedGet('/album/:source/:id', albumSchema, async(request) => {
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
// 各平台适配器签名不同:kg 为 getAlbumDetail(id, page, 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() => {
// 用列表返回的带前缀 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.data?.list?.length) throw new Error('空列表')
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() => {
const r = await get(`/find/all/${encodeURIComponent('稻香')}?singer=${encodeURIComponent('周杰伦')}`)
if (r.code != 0) throw new Error(r.msg)
@@ -369,14 +376,14 @@ await test('跨平台匹配 /find', 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.data?.list?.length) throw new Error('空列表')
return `${r.data.list.length} 个歌单`
})
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.data?.list?.length) throw new Error('空列表(sortId 默认值未适配平台)')
return `${r.data.list.length} 个歌单`
@@ -406,14 +413,14 @@ await test('歌手信息 /singer/tx', 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.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')
const r = await get('/singer/kg/3062/albums?limit=3')
if (r.code != 0) throw new Error(r.msg)
if (!r.data?.list?.length) throw new Error('空列表')
return `${r.data.list.length} 张`