diff --git a/README.md b/README.md index aed3ed6..eaae18b 100644 --- a/README.md +++ b/README.md @@ -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) | diff --git a/src/routes.ts b/src/routes.ts index a21e7c5..b86d60c 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -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 diff --git a/test/smoke-test.ts b/test/smoke-test.ts index 88a88aa..c31361d 100644 --- a/test/smoke-test.ts +++ b/test/smoke-test.ts @@ -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} 张`