From 3381e9b906eb7ff76d403224663b337d638c9f48 Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Sat, 3 Oct 2026 11:03:06 +0800 Subject: [PATCH] =?UTF-8?q?refactor(api)!:=20=E5=B9=B3=E5=8F=B0=20id=20?= =?UTF-8?q?=E6=94=B9=E4=B8=A4=E5=AD=97=E6=AF=8D=E7=9F=AD=E7=A0=81=E2=80=94?= =?UTF-8?q?=E2=80=94qian=E2=86=92bd=E3=80=81joox=E2=86=92jx=E3=80=815sing?= =?UTF-8?q?=E2=86=925s=E3=80=81bili=E2=86=92bl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对外 id 统一为两字母短码,与既有的 kw/kg/tx/wy/qs/xm/me/qt/lr 对齐; migu 与 apple 保持原样(Apple Music 没有自然的短码)。 **破坏性变更**:id 出现在所有 URL、/sources 返回、OpenAPI 示例以及 data/auth.json 的凭据键里,这四个平台已存的登录态会失效,需重新登录。 - sdk/index.ts:id 与 supportQuality 映射键 - sdk/types.ts:Source 联合类型 - src/routes.ts 与 routes-login.ts:路由路径与 schema 描述 (/login/bili/qr → /login/bl/qr 等)、/login/status 的响应键 - test/smoke-test.ts、README.md、docs/API.md 同步 有意未改:sdk 目录与文件名仍用平台名(sdk/qian/、sdk/apis/joox.ts …), 规则定为「目录=平台名,id=对外短码」,映射只在 SUPPORTED_SOURCES 一处声明。 域名与正则(joox.com、5sing.kugou.com、/joox\.com\//)、jooxSign 等标识符、 错误文案里的中文平台名、以及 kg 榜单名(含「joox本地热歌榜」)全部保持原样—— 它们只是恰好含平台名,并不是 id。 验证:typecheck ✓、pnpm test 112/112 ✓;另起临时实例实证 /sources 与 /login/status 的新 id 与展示名,新旧 id 均无残留。 --- README.md | 40 +++++++++++++++---------------- docs/API.md | 10 ++++---- sdk/5sing/index.ts | 4 ++-- sdk/5sing/musicInfo.ts | 2 +- sdk/5sing/musicSearch.ts | 8 +++---- sdk/5sing/playlist.ts | 14 +++++------ sdk/5sing/util.ts | 12 +++++----- sdk/apis/bili.ts | 2 +- sdk/apis/joox.ts | 2 +- sdk/bili/index.ts | 2 +- sdk/bili/login.ts | 6 ++--- sdk/bili/lyric.ts | 2 +- sdk/bili/musicSearch.ts | 2 +- sdk/bili/playlist.ts | 12 +++++----- sdk/bili/util.ts | 4 ++-- sdk/index.ts | 16 ++++++------- sdk/joox/album.ts | 10 ++++---- sdk/joox/index.ts | 6 ++--- sdk/joox/musicSearch.ts | 4 ++-- sdk/joox/playlist.ts | 16 ++++++------- sdk/joox/song.ts | 4 ++-- sdk/joox/util.ts | 2 +- sdk/qian/album.ts | 8 +++---- sdk/qian/index.ts | 2 +- sdk/qian/musicSearch.ts | 4 ++-- sdk/qian/parse.ts | 2 +- sdk/qian/playlist.ts | 12 +++++----- sdk/qian/util.ts | 6 ++--- sdk/types.ts | 2 +- src/routes-login.ts | 22 ++++++++--------- src/routes.ts | 16 ++++++------- src/server.ts | 4 ++-- test/smoke-test.ts | 52 ++++++++++++++++++++-------------------- 33 files changed, 155 insertions(+), 155 deletions(-) diff --git a/README.md b/README.md index ed812a4..2b043ea 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 功能 -- **多平台聚合**:酷我(kw)、酷狗(kg)、QQ音乐(tx)、网易云(wy)、汽水音乐(qs)、咪咕音乐(migu)、千千音乐(qian)、JOOX(joox)、哔哩哔哩(bili)、5sing;有声平台:喜马拉雅(xm)、猫耳FM(me)、蜻蜓FM(qt)、懒人听书(lr);Apple Music(apple,仅 preview 试听) +- **多平台聚合**:酷我(kw)、酷狗(kg)、QQ音乐(tx)、网易云(wy)、汽水音乐(qs)、咪咕音乐(migu)、千千音乐(bd)、JOOX(jx)、哔哩哔哩(bl)、5sing(5s);有声平台:喜马拉雅(xm)、猫耳FM(me)、蜻蜓FM(qt)、懒人听书(lr);Apple Music(apple,仅 preview 试听) - **直连取流**:各平台播放地址获取(不依赖第三方 API 服务) - **完整链路**:搜索、播放地址、歌词(含各平台加密歌词解码)、封面、榜单、热搜、歌单、专辑、歌手、链接解析(歌单/专辑/歌曲网页链接) - 有声平台(xm/me/qt/lr)与 Apple Music(仅试听)不参与聚合搜索与跨平台匹配,仅支持 source 直查 @@ -19,10 +19,10 @@ | 网易云 | `wy` | ✅ | 128k/320k/flac/flac24bit | ✅ | ✅ | ✅ 列表/详情 | ✅ 搜索/详情 | ✅ 歌曲/专辑/歌单 | 扫码/手机号 | | 汽水音乐 | `qs` | ✅ | 128k/320k(登录解锁无损) | ✅ | ✅ | — | — | — | 扫码 | | 咪咕音乐 | `migu` | ✅ | 64k/128k/320k/flac/flac24bit | ✅ | ✅ | ✅ 搜索/详情/分类 | ✅ 搜索/详情 | ✅ 歌曲/专辑/歌单 | cookie(解锁高档位) | -| 千千音乐 | `qian` | ✅ | 64k/128k/320k/flac | ✅ | ✅ | ✅ 详情/分类 | ✅ 搜索/详情 | ✅ 歌曲/专辑/歌单 | cookie(VIP 曲目) | -| JOOX | `joox` | ✅ | 128k(VIP 为试听片段) | ✅ | ✅ | ✅ 搜索/详情/分类 | ✅ 搜索/详情 | ✅ 歌曲/专辑/歌单 | cookie(VIP 完整曲) | -| 哔哩哔哩 | `bili` | ✅ | 64k/128k/320k/flac/flac24bit(实际命中 B站档位 132k/192k;flac/Hi-Res 需大会员) | ✅ | ✅ | ✅ 搜索/详情(合集/多分P) | — | ✅ 歌曲/歌单 | 扫码 | -| 5sing | `5sing` | ✅ | sq/hq/lq | ✅ | — | ✅ 搜索/详情 | — | ✅ 歌曲/歌单 | cookie(可选) | +| 千千音乐 | `bd` | ✅ | 64k/128k/320k/flac | ✅ | ✅ | ✅ 详情/分类 | ✅ 搜索/详情 | ✅ 歌曲/专辑/歌单 | cookie(VIP 曲目) | +| JOOX | `jx` | ✅ | 128k(VIP 为试听片段) | ✅ | ✅ | ✅ 搜索/详情/分类 | ✅ 搜索/详情 | ✅ 歌曲/专辑/歌单 | cookie(VIP 完整曲) | +| 哔哩哔哩 | `bl` | ✅ | 64k/128k/320k/flac/flac24bit(实际命中 B站档位 132k/192k;flac/Hi-Res 需大会员) | ✅ | ✅ | ✅ 搜索/详情(合集/多分P) | — | ✅ 歌曲/歌单 | 扫码 | +| 5sing | `5s` | ✅ | sq/hq/lq | ✅ | — | ✅ 搜索/详情 | — | ✅ 歌曲/歌单 | cookie(可选) | | Apple Music | `apple` | ✅ | preview 试听片段 | ✅(需登录) | ✅ | ✅ 搜索/详情/分类 | ✅ 搜索/详情 | ✅ 歌曲/专辑/歌单 | cookie(media-user-token) | | 喜马拉雅 | `xm` | ✅ | 64k/128k | — | ✅ | — | ✅ 详情 | — | cookie | | 猫耳FM | `me` | ✅ | 64k/128k | — | ✅ | — | ✅ 详情 | — | token | @@ -62,13 +62,13 @@ pnpm typecheck # 仅类型检查,不运行 | `GET /singer/:source/:id` | 歌手信息(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]` | 专辑详情与曲目(分页为 path 段,第 1 页省略)。支持 kg/kw/tx/wy/xm/me/qt/lr/migu/qian/joox/apple | -| `GET /album/:source/search/:keyword` | 专辑搜索(kg/kw/tx/wy/migu/qian/joox/apple) | -| `GET /playlist/:source/search/:keyword` | 歌单搜索(kg/kw/tx/wy/migu/qian/joox/bili/5sing/apple) | -| `GET /playlist/:source/categories` | 歌单分类(kw/kg/tx/wy/migu/qian/joox/bili/5sing/apple) | +| `GET /album/:source/:id[/:page]` | 专辑详情与曲目(分页为 path 段,第 1 页省略)。支持 kg/kw/tx/wy/xm/me/qt/lr/migu/bd/jx/apple | +| `GET /album/:source/search/:keyword` | 专辑搜索(kg/kw/tx/wy/migu/bd/jx/apple) | +| `GET /playlist/:source/search/:keyword` | 歌单搜索(kg/kw/tx/wy/migu/bd/jx/bl/5s/apple) | +| `GET /playlist/:source/categories` | 歌单分类(kw/kg/tx/wy/migu/bd/jx/bl/5s/apple) | | `GET /playlist/:source/categories/:categoryId` | 分类下的歌单(同上) | -| `GET /playlist/:source/:id[/:page]?limit=` | 歌单详情与曲目(kw/kg/tx/wy/migu/qian/joox/bili/5sing/apple;bili 支持合集/多分P) | -| `POST /parse/song/:source` 等 | 链接解析:body `{ "link": "..." }`,三个端点 `parse/song` / `parse/album` / `parse/playlist`。支持 kw/kg/tx/wy/5sing/apple/bili/joox/qian/migu(其中 bili、5sing 无 album 端点) | +| `GET /playlist/:source/:id[/:page]?limit=` | 歌单详情与曲目(kw/kg/tx/wy/migu/bd/jx/bl/5s/apple;bl 支持合集/多分P) | +| `POST /parse/song/:source` 等 | 链接解析:body `{ "link": "..." }`,三个端点 `parse/song` / `parse/album` / `parse/playlist`。支持 kw/kg/tx/wy/5s/apple/bl/jx/bd/migu(其中 bl、5s 无 album 端点) | | `GET /user-playlist/:source` | 个人歌单(仅 tx/wy 有完整实现;需登录,未登录或平台不支持返回 404/502) | | `POST /login/wy/qr` → `POST /login/wy/qr/check` | 网易云扫码登录(扫码后轮询完成登录) | | `POST /login/wy/cellphone` | 网易云手机号登录(password 或 captcha) | @@ -83,8 +83,8 @@ pnpm typecheck # 仅类型检查,不运行 | `POST /login/xm` | 绑定喜马拉雅 cookie(www.ximalaya.com 复制,须含 `1&_token`,解锁付费/已购内容取流) | | `POST /login/me` | 绑定猫耳FM token(App 抓包或 m.missevan.com 复制,须含 `token=`,解锁付费声音取流) | | `POST /login/lr` | 懒人听书账号登录(account+password,懒人取流要求登录态,免费章节同样) | -| `POST /login/bili/qr` → `POST /login/bili/qr/check` | 哔哩哔哩扫码登录(B站 App 扫一扫,登录后解锁大会员音质) | -| `DELETE /login/{wy,kg,tx,qs,xm,me,lr,bili}` | 清除对应平台凭据(退出登录) | +| `POST /login/bl/qr` → `POST /login/bl/qr/check` | 哔哩哔哩扫码登录(B站 App 扫一扫,登录后解锁大会员音质) | +| `DELETE /login/{wy,kg,tx,qs,xm,me,lr,bl}` | 清除对应平台凭据(退出登录) | | `GET /login/status` | 登录状态(登录后取流/请求自动携带凭据) | | `GET /sources` | 平台列表(音质与能力) | | `GET /health` | 健康检查 | @@ -104,10 +104,10 @@ pnpm typecheck # 仅类型检查,不运行 - **qt**:`:` 复合 id(/album/qt 节目列表产出);audiostream 签名直链(302 到华为云 CDN),匿名可取免费节目 - **lr**:`:
` 复合 id(/album/lr 章节列表产出);取流必须 `POST /login/lr` 登录(免费章节同样要求登录态) - **migu**:`||` 三元组(搜索结果/专辑/歌单产出);取流走 listen-url 多档候选链 + 逐档 Range 探测,命中可访问的最高档(匿名实测可出 flac24bit 真无损),Z3D/I3D 加密流自动改走 `/stream/migu` 解密代理 -- **qian**:track id(T 开头,搜索结果的 `songmid`);免费曲目匿名可出 128k/320k/flac 明文直链,VIP/版权曲目需 cookie -- **joox**:加密后的 track id(含 `=`/`+`,URL 编码后传);游客 128k,VIP 完整曲需 cookie(`wmid`+`session_key`) -- **bili**:`|`(搜索结果/合集曲目产出);合集/歌单 id 形如 `season:::` 或 `bvid:` -- **5sing**:`|`(如 `932557|yc`,songType 为 yc 原创 / fc 翻唱 / bz 伴奏),URL 路径需编码 `|` 为 `%7C` +- **bd**:track id(T 开头,搜索结果的 `songmid`);免费曲目匿名可出 128k/320k/flac 明文直链,VIP/版权曲目需 cookie +- **jx**:加密后的 track id(含 `=`/`+`,URL 编码后传);游客 128k,VIP 完整曲需 cookie(`wmid`+`session_key`) +- **bl**:`|`(搜索结果/合集曲目产出);合集/歌单 id 形如 `season:::` 或 `bvid:` +- **5s**:`|`(如 `932557|yc`,songType 为 yc 原创 / fc 翻唱 / bz 伴奏),URL 路径需编码 `|` 为 `%7C` - **apple**:数字歌曲 id(搜索结果产出);取流固定为 `preview` 试听片段(完整曲为 DRM,不在支持范围) `/url` 在外部音源与平台直连都失败时,自动降级到**酷我**兜底:按歌名/歌手/时长匹配同一首歌后走 kw 通道(匿名可拿无损)。kg 可由 hash 反查歌曲信息;其它平台需要兜底时给 `/url` 传 `name`/`singer`/`interval` 查询参数。 @@ -159,7 +159,7 @@ src/ HTTP 应用层(Fastify 服务器,全部 TypeScript 源 server.ts HTTP 服务器入口(pnpm start 实际启动文件) routes.ts 业务路由(JSON Schema 校验 + OpenAPI 元数据) routes-login.ts 登录路由(wy 扫码/手机号、kg 扫码/短信/账密+风控验证/token、tx 扫码/cookie、qs 汽水扫码、 - bili 扫码、xm/me cookie 绑定、lr 账密) + bl 扫码、xm/me cookie 绑定、lr 账密) source.ts 外部音源候选链(SOURCE_ENABLED 时 /url 取流优先走 sources/ 脚本,失败回落 SDK) source-ivm.ts 社区音源脚本加载器(isolated-vm 硬隔离沙箱) source-simple.ts 简单 API 音源脚本加载器(.mjs 进程内直载) @@ -198,7 +198,7 @@ test/ - **qs 取流**:匿名仅 30s 试听;完整曲目需 `/login/qs/qr` 用**汽水音乐 App** 扫码登录,取流走 `/stream/qs/:id` 解密流代理(一次性下载解密回明文,不支持 Range)。 - **migu 取流**:匿名 PQ(128k) 永远可用,但候选链会优先探测更高档位——多数曲目实际能匿名拿到 SQ/ZQ 真无损直链(上游同款 tone 路径重写 + 逐档探测);Z3D/I3D 加密候选经 `/stream/migu` 解密后同样可播。HQ/SQ 返回「登录后免费试听」时自动降级到可用的最高档。 - **apple 取流**:只有 preview 试听片段(约 30–90s,AAC/m4a),返回的 `type` 固定为 `preview`,不要当作完整曲。 -- **bili 取流**:匿名最高 192k;flac/Hi-Res 需 `/login/bili/qr` 登录大会员账号。CDN 有约 1 小时的 `deadline` 签名,取流结果不宜缓存过久。 -- **5sing 歌单详情**:依赖 5sing 网页(`5sing.kugou.com/.../dj/...html`)解析曲目;2026-10-02 实测该站点网页层整体 502(搜索/取流/歌词等手机端 API 不受影响)。上游同样依赖该页面,待站点恢复即自动可用。 +- **bl 取流**:匿名最高 192k;flac/Hi-Res 需 `/login/bl/qr` 登录大会员账号。CDN 有约 1 小时的 `deadline` 签名,取流结果不宜缓存过久。 +- **5s 歌单详情**:依赖 5sing 网页(`5sing.kugou.com/.../dj/...html`)解析曲目;2026-10-02 实测该站点网页层整体 502(搜索/取流/歌词等手机端 API 不受影响)。上游同样依赖该页面,待站点恢复即自动可用。 - **link 解析**:`/parse/*` 走各平台网页链接解析(需上游有对应实现);未实现的平台返回 404。 - 平台接口随时可能变更,遇失效时可对照 `sdk/apis/` 内的接口文档调整。 diff --git a/docs/API.md b/docs/API.md index 036b8f7..faa052d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -3,7 +3,7 @@ 多平台音乐聚合接口:搜索、播放地址、歌词、封面、榜单、歌单、专辑、歌手、链接解析、登录。 共 15 个平台:`kw`(酷我)/ `kg`(酷狗)/ `tx`(QQ音乐)/ `wy`(网易云)/ `qs`(汽水)/ `migu`(咪咕)/ -`qian`(千千)/ `joox` / `bili`(哔哩哔哩)/ `5sing` / `apple`(Apple Music,仅试听), +`bd`(千千)/ `jx` / `bl`(哔哩哔哩)/ `5s` / `apple`(Apple Music,仅试听), 以及有声平台 `xm`(喜马拉雅)/ `me`(猫耳FM)/ `qt`(蜻蜓FM)/ `lr`(懒人听书)。 > **本文档是人工速查版,可能滞后于代码。** @@ -40,7 +40,7 @@ ### 歌曲 id 形态 各平台 id 约定(含 `kw` 的 songmid、`kg` 的 hash、`qs`/`xm`/`me` 的 id、`migu` 的三元组、 -`qt`/`lr` 的复合 id、`bili` 的 `|` 等)详见 README「歌曲 ID 约定」。 +`qt`/`lr` 的复合 id、`bl` 的 `|` 等)详见 README「歌曲 ID 约定」。 复合 id 含 `|` / `:` 时在 URL 中需编码(`|` → `%7C`)。 ## 接口速查 @@ -78,7 +78,7 @@ | GET | `/boards/:source/:boardId[/:page]` | 榜单详情(分页 path 段)。`boardId` 兼容带前缀 id(`kw__93`)与裸 `bangid`(`93`) | | GET | `/hot-search/:source` | 热搜词 | | GET | `/song-list/:source[/:page]` | 歌单列表;`?sortId=` `?tagId=` 各平台词汇不同,省略时用平台默认 | -| GET | `/playlist/:source/:id[/:page]` | 歌单详情与曲目;`?limit=`。bili 支持合集/多分P | +| GET | `/playlist/:source/:id[/:page]` | 歌单详情与曲目;`?limit=`。bl 支持合集/多分P | | GET | `/playlist/:source/search/:keyword` | 歌单搜索(`?page=` `?limit=`) | | GET | `/playlist/:source/categories` | 歌单分类列表 | | GET | `/playlist/:source/categories/:categoryId` | 分类下的歌单(`?page=` `?limit=`) | @@ -125,8 +125,8 @@ | DELETE | `/login/xm` · `/login/me` | 退出对应平台 | | POST | `/login/lr` | 懒人听书账号登录(`account` + `password`) | | DELETE | `/login/lr` | 退出懒人听书 | -| POST | `/login/bili/qr` → `/login/bili/qr/check` | 哔哩哔哩扫码(解锁大会员音质) | -| DELETE | `/login/bili` | 退出哔哩哔哩 | +| POST | `/login/bl/qr` → `/login/bl/qr/check` | 哔哩哔哩扫码(解锁大会员音质) | +| DELETE | `/login/bl` | 退出哔哩哔哩 | | POST | `/login/refresh` | 手动触发登录态续期(kg/tx) | | GET | `/login/status` | 登录状态总览(各平台 `logged` 与账号资料) | diff --git a/sdk/5sing/index.ts b/sdk/5sing/index.ts index 30f8d8c..d6d7a78 100644 --- a/sdk/5sing/index.ts +++ b/sdk/5sing/index.ts @@ -1,12 +1,12 @@ // 5sing(5sing.kugou.com,酷狗旗下原创/翻唱音乐平台)平台模块 -// 上游 music-lib/fivesing 移植(平台 id 统一为 '5sing',上游 model.Source 为 "fivesing")。 +// 上游 music-lib/fivesing 移植(平台 id 统一为 '5s',上游 model.Source 为 "fivesing")。 // 能力:搜索(原创 yc / 翻唱 fc / 伴奏 bz,统一种类字段 songType)/ 取流(明文直链,档位 sq→hq→lq)/ // 歌词(newget.dynamicWords)/ 歌单(搜索、详情、链接解析)/ 单曲链接解析。 // 不实现:专辑、封面(上游无 GetPic)、分类歌单/个人歌单(上游显式 not supported,保留抛错语义)、 // 榜单/热搜/歌手/评论(上游均无)。 // 取流入口在 sdk/apis/5sing.ts;songmid 约定 "|"(如 "932557|yc",与上游 ID 一致)。 // 登录:上游 New(cookie) 的 cookie 会随请求发送(付费/受限内容可能受益); -// 本移植从 sdk/auth/store.ts 的 getCredentials('5sing') 读取 cookie(无则匿名,匿名实测可搜索/取流/歌词/歌单)。 +// 本移植从 sdk/auth/store.ts 的 getCredentials('5s') 读取 cookie(无则匿名,匿名实测可搜索/取流/歌词/歌单)。 import musicSearch from './musicSearch.ts' import lyric from './lyric.ts' diff --git a/sdk/5sing/musicInfo.ts b/sdk/5sing/musicInfo.ts index 944e678..a66ac7b 100644 --- a/sdk/5sing/musicInfo.ts +++ b/sdk/5sing/musicInfo.ts @@ -106,7 +106,7 @@ export const fetchSongInfo = async(songID: string, songType: string, register?: link: buildSongLink(songType, songID), songType, typeName: kindName(songType), - source: '5sing' as const, + source: '5s' as const, } } diff --git a/sdk/5sing/musicSearch.ts b/sdk/5sing/musicSearch.ts index b24fa5e..6d83bbc 100644 --- a/sdk/5sing/musicSearch.ts +++ b/sdk/5sing/musicSearch.ts @@ -9,7 +9,7 @@ import { formatPlayTime } from '../common/utils.ts' import { SEARCH_API, fetch5singJson, removeEmTags, unescapeHtml, buildSongLink, estimateDuration, kindName } from './util.ts' -/** 上游 Search 的 list 项 → 统一 Song 形状(source 统一 '5sing') */ +/** 上游 Search 的 list 项 → 统一 Song 形状(source 统一 '5s') */ export const mapSearchSong = (item: any) => { const songType = String(item?.typeEname ?? '') const duration = estimateDuration(Number(item?.songSize) || 0) @@ -29,7 +29,7 @@ export const mapSearchSong = (item: any) => { typeName: String(item?.typeName ?? kindName(songType)), size: Number(item?.songSize) || 0, link: buildSongLink(songType, item?.songId), - source: '5sing' as const, + source: '5s' as const, } } @@ -37,7 +37,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => { const word = String(keyword ?? '').trim() const pageNo = Math.max(1, Number(page) || 1) const pageSize = Math.max(1, Number(limit) || 20) - const empty = { list: [] as any[], allPage: 0, total: 0, limit: pageSize, source: '5sing' as const } + const empty = { list: [] as any[], allPage: 0, total: 0, limit: pageSize, source: '5s' as const } if (!word) return empty const url = `${SEARCH_API}?keyword=${encodeURIComponent(word)}&sort=1&page=${pageNo}&filter=0&type=0` @@ -50,7 +50,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => { const total = Number(pageInfo.totalCount) || list.length // 服务端每页 10 条(固定),allPage 直接用 pageInfo.totalPages const allPage = Number(pageInfo.totalPages) || (list.length >= pageSize ? pageNo + 1 : pageNo) - return { list, allPage, total, limit: pageSize, source: '5sing' as const } + return { list, allPage, total, limit: pageSize, source: '5s' as const } } export default { search } diff --git a/sdk/5sing/playlist.ts b/sdk/5sing/playlist.ts index 2dec0ab..d05cc8e 100644 --- a/sdk/5sing/playlist.ts +++ b/sdk/5sing/playlist.ts @@ -32,7 +32,7 @@ const mapPlaylistSong = (songID: string, kind: string, name: string, artist: str songType: kind, typeName: kindName(kind), link: buildSongLink(kind, songID), - source: '5sing' as const, + source: '5s' as const, }) /** @@ -103,7 +103,7 @@ const mapPlaylistSummary = (item: any) => { // 上游怪癖:content 为字符串 "0" 时视为空简介 description: desc === '0' ? '' : desc, link: buildPlaylistLink(userId, id), - source: '5sing' as const, + source: '5s' as const, } } @@ -112,7 +112,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => { const word = String(keyword ?? '').trim() const pageNo = Math.max(1, Number(page) || 1) const pageSize = Math.max(1, Number(limit) || 20) - const empty = { list: [] as any[], total: 0, page: pageNo, limit: pageSize, source: '5sing' as const } + const empty = { list: [] as any[], total: 0, page: pageNo, limit: pageSize, source: '5s' as const } if (!word) return empty const url = `${SEARCH_API}?keyword=${encodeURIComponent(word)}&sort=1&page=${pageNo}&filter=0&type=1` @@ -132,7 +132,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => { } const total = Number(body?.pageInfo?.totalCount) || list.length - return { list, total, page: pageNo, limit: pageSize, source: '5sing' as const } + return { list, total, page: pageNo, limit: pageSize, source: '5s' as const } } /** 元数据 + 全量曲目(上游 fetchPlaylistDetail 核心;HTML 不分页,一次拿全) */ @@ -165,7 +165,7 @@ export const fetchPlaylistAll = async(playlistIdRaw: string) => { description: String(data?.C ?? ''), link: buildPlaylistLink(userId, playlistId), userId, - source: '5sing' as const, + source: '5s' as const, } // 2) 歌单页 HTML(数字 userId 会 302 → 个性域名页,fetch5sing 已跟跳) @@ -191,7 +191,7 @@ export const getPlaylistDetail = async(id: string, page = 1, limit = 50) => { const list = songs.slice(start, start + pageSize) const total = songs.length return { - source: '5sing' as const, + source: '5s' as const, info: { id: playlist.id, name: playlist.name, @@ -222,7 +222,7 @@ export const parsePlaylistLink = async(link: string) => { if (!parsed) throw new Error('invalid 5sing playlist link') const { playlist, songs } = await fetchPlaylistAll(parsed.playlistId) return { - source: '5sing' as const, + source: '5s' as const, info: { id: playlist.id, name: playlist.name, diff --git a/sdk/5sing/util.ts b/sdk/5sing/util.ts index 1f45fb0..2fa4e0b 100644 --- a/sdk/5sing/util.ts +++ b/sdk/5sing/util.ts @@ -1,8 +1,8 @@ // 5sing 公共工具(上游 fivesing/fivesing.go 的 getFirstValid / removeEmTags 等移植) -// 平台 id 统一为 '5sing'(上游包名 fivesing,Go 源码 model.Source 用 "fivesing"; -// 本仓库契约要求 id 为 5sing,故所有对外 source 字段均为 '5sing'。 +// 平台 id 统一为 '5s'(上游包名 fivesing,Go 源码 model.Source 用 "fivesing"; +// 本仓库契约要求 id 为 5s,故所有对外 source 字段均为 '5s'。 // 上游 New(cookie) 的 cookie 实际参与请求(搜索/getSongUrl/newget/歌单页), -// 这里按约定从 sdk/auth/store.ts 的 getCredentials('5sing') 读取(无则匿名)。 +// 这里按约定从 sdk/auth/store.ts 的 getCredentials('5s') 读取(无则匿名)。 import { httpFetch } from '../request.ts' import { getCredentials } from '../auth/store.ts' @@ -65,17 +65,17 @@ export const unescapeHtml = (s: any): string => }) /** 上游 New(cookie):cookie 仅在有值时随请求发送(匿名可用) */ -export const get5singCookie = (): string => String((getCredentials('5sing') as { cookie?: string } | null)?.cookie ?? '') +export const get5singCookie = (): string => String((getCredentials('5s') as { cookie?: string } | null)?.cookie ?? '') /** 登录态(凭据存在性判断,与 me 平台 status 对齐) */ export const getStatus = () => { - const cred = getCredentials('5sing') as { cookie?: string, updatedAt?: string } | null + const cred = getCredentials('5s') as { cookie?: string, updatedAt?: string } | null return cred?.cookie ? { logged: true, updatedAt: cred.updatedAt } : { logged: false } } /** 是否显式声明了非本平台的 source(上游 GetLyrics/GetDownloadURL 的 source mismatch 检查) */ export const isForeignSource = (songInfo: any): boolean => - Boolean(songInfo?.source) && songInfo.source !== '5sing' + Boolean(songInfo?.source) && songInfo.source !== '5s' /** * 带 Cookie/UA 的请求(上游 utils.Get + http.Client 自动跟跳的等价实现) diff --git a/sdk/apis/bili.ts b/sdk/apis/bili.ts index b5dfe8a..bc78824 100644 --- a/sdk/apis/bili.ts +++ b/sdk/apis/bili.ts @@ -120,7 +120,7 @@ const followRedirects = async(url: string, cookie: string, tracker?: { current: } /** - * 统一取流接口:apis('bili').getMusicUrl(songInfo, type) → { promise, cancelHttp } + * 统一取流接口:apis('bl').getMusicUrl(songInfo, type) → { promise, cancelHttp } * promise resolve → { type, url }(type 为实际命中档位,可能高于/低于请求档位) * songInfo.songmid 约定 `|`;仅给 bvid 时回源 view 取首P cid。 */ diff --git a/sdk/apis/joox.ts b/sdk/apis/joox.ts index f8f6da4..00e8e34 100644 --- a/sdk/apis/joox.ts +++ b/sdk/apis/joox.ts @@ -7,7 +7,7 @@ // wmid/session_key 会作为 uid/usk 参与签名(/login/joox 绑定后 VIP 曲目可取完整流)。 // 实测:游客态返回 128k MP3(免费完整曲目)或 VIP 曲目的 preview/refrain 片段; // 未登录请求 VIP 曲目时 play_url_list 为试听片段——与上游行为一致(上游同样只能拿游客流)。 -// 统一接口约定:apis('joox').getMusicUrl(songInfo, type) → { promise, cancelHttp } +// 统一接口约定:apis('jx').getMusicUrl(songInfo, type) → { promise, cancelHttp } // promise resolve → { type, url }(type 为实际命中档位,可能低于请求值) import { httpFetch } from '../request.ts' diff --git a/sdk/bili/index.ts b/sdk/bili/index.ts index db5e2bd..db4394c 100644 --- a/sdk/bili/index.ts +++ b/sdk/bili/index.ts @@ -1,4 +1,4 @@ -// Bilibili(B站)平台模块 —— 上游 music-lib/bilibili 的移植(平台 id: bili) +// Bilibili(B站)平台模块 —— 上游 music-lib/bilibili 的移植(平台 id: bl) // 能力:搜索 / 取流(DASH,VIP 可出 FLAC/Hi-Res)/ 歌单(合集 ugc_season + 多分P稿件)/ // 链接解析(BV/av、分P)/ 封面 / 扫码登录(sdk/bili/login.ts,凭据落 sdk/auth/store.ts) // 不实现:专辑、歌词(上游即空实现)、榜单、热搜、歌手、评论、个人歌单、歌单分类(上游均未实现)。 diff --git a/sdk/bili/login.ts b/sdk/bili/login.ts index d512be1..55a2a89 100644 --- a/sdk/bili/login.ts +++ b/sdk/bili/login.ts @@ -5,7 +5,7 @@ // 0 成功 / 86038 过期 / 86090 已扫码待确认 / 86101 未扫码 // 成功时登录态 Cookie 在 poll 响应的 Set-Cookie 与 data.url 的 crossDomain 查询串里(两处都收集)。 // 凭据落盘到 sdk/auth/store.ts 的 `bili` 记录(cookie / refreshToken / 账号资料),与 kg/wy 一致; -// 上游 New(cookie) 的实例化语义由 getCredentials('bili') 承担(见 util.ts getCookie)。 +// 上游 New(cookie) 的实例化语义由 getCredentials('bl') 承担(见 util.ts getCookie)。 // 状态码沿用本仓库约定:800 过期 / 801 等待扫码 / 802 已扫码待确认 / 803 登录成功。 import QRCode from 'qrcode' import { httpFetch } from '../common/request.ts' @@ -125,7 +125,7 @@ export const checkQRLogin = async(key: string) => { const cookie = cookieString(jar) if (!cookie) throw new Error('B站登录成功但未获取到 Cookie,请重试') const profile = await fetchProfile(cookie) - setCredentials('bili', { + setCredentials('bl', { cookie, refreshToken: String(data.refresh_token || ''), mid: profile.mid, @@ -148,7 +148,7 @@ export const checkQRLogin = async(key: string) => { /** 当前登录态(凭据存在性 + 落盘时保存的账号资料) */ export const status = () => { - const cred = getCredentials('bili') as any + const cred = getCredentials('bl') as any return cred?.cookie ? { logged: true, diff --git a/sdk/bili/lyric.ts b/sdk/bili/lyric.ts index 05185ef..aca5228 100644 --- a/sdk/bili/lyric.ts +++ b/sdk/bili/lyric.ts @@ -1,6 +1,6 @@ // Bilibili 歌词(上游 bilibili/lyric.go 即空实现:GetLyrics 对 bilibili 源恒返回空字符串) // B 站视频没有公开歌词接口(cc_subtitle 需登录且绝大多数视频没有),上游未实现,此处保持一致: -// 返回统一结构 { lyric: '', tlyric: '' },让 /lyric/bili 有响应而不是 404。 +// 返回统一结构 { lyric: '', tlyric: '' },让 /lyric/bl 有响应而不是 404。 export const getLyric = (_songInfo?: any, ..._rest: any[]) => { const requestObj = { promise: Promise.resolve({ lyric: '', tlyric: '' }), diff --git a/sdk/bili/musicSearch.ts b/sdk/bili/musicSearch.ts index 0213237..e4e6610 100644 --- a/sdk/bili/musicSearch.ts +++ b/sdk/bili/musicSearch.ts @@ -50,7 +50,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => { allPage: numPages || p + (list.length >= l ? 1 : 0), total, limit: l, - source: 'bili' as const, + source: 'bl' as const, } } diff --git a/sdk/bili/playlist.ts b/sdk/bili/playlist.ts index 597ea0a..8da80cd 100644 --- a/sdk/bili/playlist.ts +++ b/sdk/bili/playlist.ts @@ -57,7 +57,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => { creator: view?.owner?.name || item.author || '', description: season.intro || '', link: `https://www.bilibili.com/video/${item.bvid}`, - source: 'bili' as const, + source: 'bl' as const, } } const pages: any[] = view?.pages ?? [] @@ -73,14 +73,14 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => { creator: view?.owner?.name || item.author || '', description: '', link: `https://www.bilibili.com/video/${item.bvid}`, - source: 'bili' as const, + source: 'bl' as const, } } return null }) const list = result.filter(Boolean) - return { list, page: p, limit: l, total: list.length, source: 'bili' as const } + return { list, page: p, limit: l, total: list.length, source: 'bl' as const } } /** 由 bvid 构造歌单详情(上游 GetPlaylistSongs 的 bvid 分支 + ParsePlaylist 的元数据) */ @@ -167,7 +167,7 @@ const pageSlice = (detail: { info: any, songs: any[] }, page: any, limit: any) = const p = Math.max(1, Number(page) || 1) const l = Math.min(Math.max(1, Number(limit) || 100), 10000) return { - source: 'bili' as const, + source: 'bl' as const, info: detail.info, list: detail.songs.slice((p - 1) * l, p * l), page: p, @@ -216,7 +216,7 @@ const parsePlaylistView = async(bvid: string, view: any) => { let trackCount = countSeasonEpisodes(season.sections) if (!trackCount) trackCount = detail.songs.length return { - source: 'bili' as const, + source: 'bl' as const, id, info: { ...detail.info, trackCount, playCount: Number(season?.stat?.view) || undefined }, list: detail.songs, @@ -227,7 +227,7 @@ const parsePlaylistView = async(bvid: string, view: any) => { } const detail = await detailFromBvid(bvid) return { - source: 'bili' as const, + source: 'bl' as const, id: bvidKey(bvid), info: { ...detail.info, trackCount: detail.songs.length }, list: detail.songs, diff --git a/sdk/bili/util.ts b/sdk/bili/util.ts index 4bf648d..aa46060 100644 --- a/sdk/bili/util.ts +++ b/sdk/bili/util.ts @@ -25,7 +25,7 @@ export const biliHeaders = (cookie = ''): Record => ({ }) /** 已保存的登录 Cookie(未登录返回空串) */ -export const getLoginCookie = (): string => String((getCredentials('bili') as any)?.cookie || '') +export const getLoginCookie = (): string => String((getCredentials('bl') as any)?.cookie || '') const cookieMapFromHeaders = (headers: any): Record => { const raw = headers?.['set-cookie'] @@ -147,7 +147,7 @@ export const makeSong = ({ bvid, cid, name, artist, album, duration, cover, page lrc: null, types: [] as any[], _types: {} as Record, - source: 'bili' as const, + source: 'bl' as const, bvid, cid: String(cid ?? ''), page, diff --git a/sdk/index.ts b/sdk/index.ts index 4fb382e..ceba3be 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -54,10 +54,10 @@ const SUPPORTED_SOURCES: Array<{ id: string; name: string; module: PlatformModul { id: 'qt', name: '蜻蜓FM', module: qt, noAggregate: true }, { id: 'lr', name: '懒人听书', module: lr, noAggregate: true }, { id: 'migu', name: '咪咕音乐', module: migu }, - { id: 'qian', name: '千千音乐', module: qian }, - { id: 'joox', name: 'JOOX', module: joox }, - { id: 'bili', name: '哔哩哔哩', module: bili }, - { id: '5sing', name: '5sing', module: fivesing }, + { id: 'bd', name: '千千音乐', module: qian }, + { id: 'jx', name: 'JOOX', module: joox }, + { id: 'bl', name: '哔哩哔哩', module: bili }, + { id: '5s', name: '5sing', module: fivesing }, // Apple Music 仅提供 preview 试听片段(完整曲为 DRM),不参与跨平台精确匹配 { id: 'apple', name: 'Apple Music', module: apple, noAggregate: true }, ] @@ -82,10 +82,10 @@ export const supportQuality: Record = { qt: qtQualitys, lr: lrQualitys, migu: miguQualitys, - qian: qianQualitys, - joox: jooxQualitys, - bili: biliQualitys, - '5sing': fivesingQualitys, + bd: qianQualitys, + jx: jooxQualitys, + bl: biliQualitys, + '5s': fivesingQualitys, apple: appleQualitys, } diff --git a/sdk/joox/album.ts b/sdk/joox/album.ts index a68de5a..1ee5de2 100644 --- a/sdk/joox/album.ts +++ b/sdk/joox/album.ts @@ -40,7 +40,7 @@ const fetchAlbumTracks = async(albumId: string, page: number, limit: number) => const total = firstNonZero(body?.tracks?.total_count, body?.tracks?.list_count, list.length) return { - source: 'joox' as const, + source: 'jx' as const, info: { name: String(firstNonEmpty(body?.name, albumId)), img: pickJooxImage(body?.images) || null, @@ -95,7 +95,7 @@ export const getAlbumDetailFromPage = async(rawId: string, page = 1, limit = 50) const total = firstNonZero(albumData?.trackList?.total_count, albumData?.trackList?.list_count, items.length) return { - source: 'joox' as const, + source: 'jx' as const, info: { name: String(firstNonEmpty(albumData?.title, albumId)), img: cover || null, @@ -120,7 +120,7 @@ export const searchAlbum = async(keyword: string, page = 1, limit = 20) => { const url = `${SEARCH_URL}?country=sg&lang=zh_cn&keyword=${encodeURIComponent(String(keyword ?? ''))}` const { statusCode, body } = await httpFetch(url, { headers: jooxHeaders(), timeout: 20000 }).promise as any if (statusCode != 200 || !body?.section_list) { - return { list: [], total: 0, page, limit, source: 'joox' as const } + return { list: [], total: 0, page, limit, source: 'jx' as const } } const list: any[] = [] @@ -142,12 +142,12 @@ export const searchAlbum = async(keyword: string, page = 1, limit = 20) => { publishDate: String(album.publish_date ?? '').trim(), trackCount: 0, link: jooxAlbumLink(id), - source: 'joox' as const, + source: 'jx' as const, }) } } - return { list: list.slice(0, limit), total: list.length, page, limit, source: 'joox' as const } + return { list: list.slice(0, limit), total: list.length, page, limit, source: 'jx' as const } } /** 专辑链接解析(上游 ParseAlbum) */ diff --git a/sdk/joox/index.ts b/sdk/joox/index.ts index b5aaf2f..030be44 100644 --- a/sdk/joox/index.ts +++ b/sdk/joox/index.ts @@ -7,7 +7,7 @@ // - 取流/歌曲详情/专辑曲目/歌单曲目:cache.api.joox.com/openjoox2/v1(需 md5 签名,见 util.jooxSign) // - 歌词:api.joox.com/web-fcgi-bin/web_lyric(旧接口仍可用;失败回退 openjoox2 lrc_content) // - 分类/歌单/专辑 SSR 兜底:www.joox.com __NEXT_DATA__ -// 上游 New(cookie):默认游客 Cookie 来自 joox.go 常量;/login/joox 绑定后从 sdk/auth/store.ts 读取, +// 上游 New(cookie):默认游客 Cookie 来自 joox.go 常量;/login/jx 绑定后从 sdk/auth/store.ts 读取, // 登录 cookie 的 wmid/session_key 会作为 uid/usk 参与 openjoox2 签名(VIP 完整曲目依赖它)。 // userPlaylist:上游 joox/user_playlist.go 返回 ErrUserPlaylistsUnsupported,未实现(不造)。 import jooxApi from '../apis/joox.ts' @@ -35,7 +35,7 @@ const joox = { album, playlist, // 契约别名:JOOX 的「分类/列表」能力即 playlist.go 的歌单分类,没有独立的 songList 接口, - // 这里把同一套分类/分类歌单挂到 songList 键,便于 /song-list/joox 路由复用。 + // 这里把同一套分类/分类歌单挂到 songList 键,便于 /song-list/jx 路由复用。 songList: { sortList: [{ name: '全部', id: '' }], getTags() { @@ -53,7 +53,7 @@ const joox = { /** 登录状态(凭据存在性判断;无在线校验接口) */ export const status = () => { - const cred = getCredentials('joox') as { cookie?: string, updatedAt?: string } | null + const cred = getCredentials('jx') as { cookie?: string, updatedAt?: string } | null return cred?.cookie ? { logged: true, updatedAt: cred.updatedAt } : { logged: false } diff --git a/sdk/joox/musicSearch.ts b/sdk/joox/musicSearch.ts index fe3e201..be708f0 100644 --- a/sdk/joox/musicSearch.ts +++ b/sdk/joox/musicSearch.ts @@ -14,7 +14,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => { const url = `${SEARCH_URL}?country=sg&lang=zh_cn&keyword=${encodeURIComponent(String(keyword ?? ''))}` const { statusCode, body } = await httpFetch(url, { headers: jooxHeaders(), timeout: 20000 }).promise as any if (statusCode != 200 || !body?.section_list) { - return { list: [], allPage: 0, total: 0, limit, source: 'joox' as const } + return { list: [], allPage: 0, total: 0, limit, source: 'jx' as const } } const list: any[] = [] @@ -35,7 +35,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => { allPage: 1, total: list.length, limit, - source: 'joox' as const, + source: 'jx' as const, } } diff --git a/sdk/joox/playlist.ts b/sdk/joox/playlist.ts index ec7cbd3..1edafdc 100644 --- a/sdk/joox/playlist.ts +++ b/sdk/joox/playlist.ts @@ -39,7 +39,7 @@ const mapPlaylistSummary = (item: any) => { creator: '', description: '', link: jooxPlaylistLink(id), - source: 'joox' as const, + source: 'jx' as const, } } @@ -60,7 +60,7 @@ const fetchPlaylistTracks = async(playlistId: string, page: number, limit: numbe const total = firstNonZero(body?.tracks?.total_count, body?.tracks?.list_count, body?.track_count, list.length) return { - source: 'joox' as const, + source: 'jx' as const, info: { name: String(firstNonEmpty(body?.name, playlistId)), img: pickJooxImage(body?.images) || null, @@ -136,7 +136,7 @@ export const getPlaylistDetailFromPage = async(rawId: string) => { .filter(Boolean) if (!list.length) throw new Error('joox 歌单无歌曲') return { - source: 'joox' as const, + source: 'jx' as const, info: { name: String(parsed.name || pageId), img: cover || null, @@ -185,7 +185,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => { const url = `${SEARCH_URL}?country=sg&lang=zh_cn&keyword=${encodeURIComponent(String(keyword ?? ''))}` const { statusCode, body } = await httpFetch(url, { headers: jooxHeaders(), timeout: 20000 }).promise as any if (statusCode != 200 || !body?.section_list) { - return { list: [], total: 0, page, limit, source: 'joox' as const } + return { list: [], total: 0, page, limit, source: 'jx' as const } } const list: any[] = [] @@ -200,7 +200,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 20) => { } } - return { list: list.slice(0, limit), total: list.length, page, limit, source: 'joox' as const } + return { list: list.slice(0, limit), total: list.length, page, limit, source: 'jx' as const } } /** 歌单分类页原始数据(上游 fetchJooxPlaylistCategoriesPage) */ @@ -230,7 +230,7 @@ export const getCategories = async() => { list.push({ id: String(index), name, group: 'JOOX', hot: false, count: items.length }) }) if (list.length === 1) throw new Error('joox 歌单分类不存在') - return { source: 'joox' as const, list } + return { source: 'jx' as const, list } } /** 分类下歌单(上游 GetCategoryPlaylists) */ @@ -282,13 +282,13 @@ export const getCategoryPlaylists = async(categoryId = '', page = 1, limit = 20) link: jooxPlaylistLink(id), categoryId: cid, categoryName, - source: 'joox' as const, + source: 'jx' as const, } }) .filter(Boolean) if (!list.length) throw new Error('joox 分类下无歌单') - return { list, total: items.length, page, limit, source: 'joox' as const } + return { list, total: items.length, page, limit, source: 'jx' as const } } /** 歌单链接解析(上游 ParsePlaylist) */ diff --git a/sdk/joox/song.ts b/sdk/joox/song.ts index c7119aa..0c57c3a 100644 --- a/sdk/joox/song.ts +++ b/sdk/joox/song.ts @@ -45,7 +45,7 @@ export const jooxSongFromTrack = ( vip: Number(item?.vip_flag ?? 0) === 1, types: [], _types: {}, - source: 'joox' as const, + source: 'jx' as const, } } @@ -65,7 +65,7 @@ export const jooxSongFromDetail = (detail: any, playUrl = ''): any => { lrc: null, types: [], _types: {}, - source: 'joox' as const, + source: 'jx' as const, } if (playUrl) song.url = playUrl song.vip = Number(detail?.vip_flag ?? 0) === 1 diff --git a/sdk/joox/util.ts b/sdk/joox/util.ts index 4bf89ce..70b3b36 100644 --- a/sdk/joox/util.ts +++ b/sdk/joox/util.ts @@ -33,7 +33,7 @@ export const jooxSign = (params: Record): string => /** 绑定到 sdk/auth/store.ts 的登录 cookie(未登录返回空串,不回退游客 cookie) */ export const jooxLoginCookie = (): string => { - const cred = getCredentials('joox') as { cookie?: string } | null + const cred = getCredentials('jx') as { cookie?: string } | null return cred?.cookie || '' } diff --git a/sdk/qian/album.ts b/sdk/qian/album.ts index be5ae70..badf2d4 100644 --- a/sdk/qian/album.ts +++ b/sdk/qian/album.ts @@ -29,7 +29,7 @@ const mapAlbumSummary = (item: QianAlbumSearchItem) => { genre: String(item?.genre ?? '').trim(), lang: String(item?.lang ?? '').trim(), link: id ? albumLink(id) : '', - source: 'qian' as const, + source: 'bd' as const, } } @@ -74,7 +74,7 @@ export const searchAlbumItems = async(keyword: string, page = 1, limit = 10): Pr /** 搜索专辑(返回歌单/专辑统一摘要形状) */ export const searchAlbum = async(keyword: string, page = 1, limit = 10) => { const word = String(keyword ?? '').trim() - const empty = { source: 'qian' as const, list: [] as any[], total: 0, page, limit } + const empty = { source: 'bd' as const, list: [] as any[], total: 0, page, limit } if (!word) return empty const items = await searchAlbumItems(word, page, limit) @@ -82,7 +82,7 @@ export const searchAlbum = async(keyword: string, page = 1, limit = 10) => { .map(mapAlbumSummary) .filter((album) => album.id) .slice(0, limit) - return { source: 'qian' as const, list, total: items.length, page, limit } + return { source: 'bd' as const, list, total: items.length, page, limit } } /** 旧数字专辑 id → PSID(P 码);已是 P 码则原样返回 */ @@ -146,7 +146,7 @@ export const getAlbumDetail = async(id: string, page = 1, limit = 30) => { } return { - source: 'qian' as const, + source: 'bd' as const, info: albumInfo, list: paged, page, diff --git a/sdk/qian/index.ts b/sdk/qian/index.ts index c917b85..97657ea 100644 --- a/sdk/qian/index.ts +++ b/sdk/qian/index.ts @@ -6,7 +6,7 @@ // - VIP/版权曲目 tracklink 无 path(需登录态会员 cookie),搜索保留但标记 payPlay; // - 歌单关键词搜索(/v1/search?type=6)服务端已下线(errno 23001),searchPlaylist 返回空列表; // - 上游 GetUserPlaylists 明确 Unsupported,本移植不提供 userPlaylist。 -// 凭据:上游 New(cookie) 的 cookie 从 sdk/auth/store.ts 的 getCredentials('qian') 读取(可选)。 +// 凭据:上游 New(cookie) 的 cookie 从 sdk/auth/store.ts 的 getCredentials('bd') 读取(可选)。 import qianApi from '../apis/qian.ts' import musicSearch from './musicSearch.ts' import lyric from './lyric.ts' diff --git a/sdk/qian/musicSearch.ts b/sdk/qian/musicSearch.ts index 1d94057..8bc468f 100644 --- a/sdk/qian/musicSearch.ts +++ b/sdk/qian/musicSearch.ts @@ -6,7 +6,7 @@ import { APP_ID, apiGet, apiOk, mapTrack } from './util.ts' export const search = async(keyword: string, page = 1, limit = 20) => { const word = String(keyword ?? '').trim() - const empty = { list: [] as any[], allPage: 1, total: 0, limit, source: 'qian' as const } + const empty = { list: [] as any[], allPage: 1, total: 0, limit, source: 'bd' as const } if (!word) return empty const body = await apiGet('/v1/search', { @@ -28,7 +28,7 @@ export const search = async(keyword: string, page = 1, limit = 20) => { allPage: haveMore ? page + 1 : Math.max(page, Math.ceil(total / Math.max(1, limit)) || 1), total, limit, - source: 'qian' as const, + source: 'bd' as const, } } diff --git a/sdk/qian/parse.ts b/sdk/qian/parse.ts index d385111..8a146ed 100644 --- a/sdk/qian/parse.ts +++ b/sdk/qian/parse.ts @@ -26,7 +26,7 @@ export const parseSong = async(link: string) => { } delete song.lyricUrl delete song.raw - return { ...song, songmid: firstNonEmpty(song.songmid, tsid), source: 'qian' as const } + return { ...song, songmid: firstNonEmpty(song.songmid, tsid), source: 'bd' as const } } export const parseAlbum = async(link: string) => parseAlbumLink(link) diff --git a/sdk/qian/playlist.ts b/sdk/qian/playlist.ts index d24ad9f..ff69162 100644 --- a/sdk/qian/playlist.ts +++ b/sdk/qian/playlist.ts @@ -19,14 +19,14 @@ const mapPlaylistSummary = (item: any) => { creator: firstNonEmpty(item?.creator, item?.author, item?.userName, item?.nickName, item?.ownerName), description: rawDesc || tagList.join('、'), link: id ? playlistLink(id) : '', - source: 'qian' as const, + source: 'bd' as const, } } /** 上游 SearchPlaylist(type=6 歌单搜索;服务端已停用,返回空列表) */ export const searchPlaylist = async(keyword: string, page = 1, limit = 10) => { const word = String(keyword ?? '').trim() - const empty = { source: 'qian' as const, list: [] as any[], total: 0, page, limit } + const empty = { source: 'bd' as const, list: [] as any[], total: 0, page, limit } if (!word) return empty const body = await apiGet('/v1/search', { @@ -40,7 +40,7 @@ export const searchPlaylist = async(keyword: string, page = 1, limit = 10) => { const items = Array.isArray(body?.data?.typeSonglist) ? body.data.typeSonglist : [] const list = items.map(mapPlaylistSummary).filter((item: any) => item.id) - return { source: 'qian' as const, list, total: Number(body?.data?.total) || list.length, page, limit } + return { source: 'bd' as const, list, total: Number(body?.data?.total) || list.length, page, limit } } /** 歌单详情(含曲目;tracklist/info 支持 pageNo/pageSize) */ @@ -69,7 +69,7 @@ export const getPlaylistDetail = async(id: string, page = 1, limit = 50) => { const total = Number(data?.trackCount) || list.length return { - source: 'qian' as const, + source: 'bd' as const, info: { id: playlistId, name: title, img: cover, desc: description, author: creator, link: playlistLink(playlistId) }, list, page, @@ -105,7 +105,7 @@ export const getCategories = async() => { list.push({ id, name, group: groupName, count: Number(item?.count ?? 0) || 0 }) } } - return { source: 'qian' as const, list } + return { source: 'bd' as const, list } } /** 上游 GetCategoryPlaylists(page >= 1,limit 1..100) */ @@ -125,7 +125,7 @@ export const getCategoryPlaylists = async(categoryId: string | number, page = 1, .map((item: any) => ({ ...mapPlaylistSummary(item), categoryId: cateId })) .filter((item: any) => item.id) return { - source: 'qian' as const, + source: 'bd' as const, list, total: Number(body?.data?.total) || list.length, page: pageNo, diff --git a/sdk/qian/util.ts b/sdk/qian/util.ts index d9e5b2a..b200ff5 100644 --- a/sdk/qian/util.ts +++ b/sdk/qian/util.ts @@ -2,7 +2,7 @@ // 上游 music-lib/qianqian 的全部接口都走 /v1/*,参数 = 业务参数 + appid + timestamp + sign: // sign = md5(按 key 升序拼接 "k=v&..."(不含 sign)+ Secret) // 该平台匿名即可搜索 / 查专辑 / 查歌单 / 拿免费曲目直链;cookie 仅影响 VIP、收藏等登录态资源, -// MusicServer 从 sdk/auth/store.ts 的 getCredentials('qian') 读取(可选,未登录留空)。 +// MusicServer 从 sdk/auth/store.ts 的 getCredentials('bd') 读取(可选,未登录留空)。 import crypto from 'node:crypto' import { httpFetch } from '../request.ts' import { getCredentials } from '../auth/store.ts' @@ -18,7 +18,7 @@ export const RATE_ORDER = ['3000', '320', '128', '64'] /** 千千平台 cookie(未绑定返回空串;上游所有请求都会带上该 header,此处保持一致) */ export const getQianCookie = (): string => { - const cred = getCredentials('qian') as { cookie?: string } | null + const cred = getCredentials('bd') as { cookie?: string } | null return cred?.cookie ? String(cred.cookie) : '' } @@ -196,7 +196,7 @@ export const mapTrack = (item: any, opts: MapTrackOptions = {}) => { lrcx: '', types, _types, - source: 'qian' as const, + source: 'bd' as const, } } diff --git a/sdk/types.ts b/sdk/types.ts index 07f5a8c..a1bdb74 100644 --- a/sdk/types.ts +++ b/sdk/types.ts @@ -4,7 +4,7 @@ /** 音乐平台标识 */ export type Source = | 'kw' | 'kg' | 'tx' | 'wy' | 'qs' - | 'migu' | 'qian' | 'joox' | 'bili' | '5sing' | 'apple' + | 'migu' | 'bd' | 'jx' | 'bl' | '5s' | 'apple' | 'xm' | 'me' | 'qt' | 'lr' /** 音质标识(wav 仅部分接口声明使用) */ diff --git a/src/routes-login.ts b/src/routes-login.ts index 5dc56f3..453a30a 100644 --- a/src/routes-login.ts +++ b/src/routes-login.ts @@ -425,10 +425,10 @@ export const loginRoutes = async(fastify) => { // ---------- 哔哩哔哩 ---------- - fastify.post('/login/bili/qr', { + fastify.post('/login/bl/qr', { schema: { summary: '获取哔哩哔哩扫码登录二维码', - description: '用 B站 App 扫码后轮询 /login/bili/qr/check 完成登录。返回 key 需原样回传,qrimg 为 data URL 可直接展示。', + description: '用 B站 App 扫码后轮询 /login/bl/qr/check 完成登录。返回 key 需原样回传,qrimg 为 data URL 可直接展示。', tags: ['登录'], }, }, async(request, reply) => { @@ -439,7 +439,7 @@ export const loginRoutes = async(fastify) => { } }) - fastify.post('/login/bili/qr/check', { + fastify.post('/login/bl/qr/check', { schema: { summary: '轮询哔哩哔哩扫码状态', description: 'status:waiting 等待扫码 / scanned 已扫码待确认 / success 登录成功(自动保存凭据并返回账号资料)' + @@ -455,10 +455,10 @@ export const loginRoutes = async(fastify) => { } }) - fastify.delete('/login/bili', { + fastify.delete('/login/bl', { schema: { summary: '退出哔哩哔哩登录', tags: ['登录'] }, }, async() => { - clearCredentials('bili') + clearCredentials('bl') return { logged: false } }) @@ -476,8 +476,8 @@ export const loginRoutes = async(fastify) => { fastify.get('/login/status', { schema: { summary: '查看登录状态', - description: 'wy 为实时校验(未登录或 cookie 失效返回 logged: false),kg/tx/bili 为已存凭据(含 VIP 等级与最近更新时间),' + - 'qs/xm/me/lr/qian/joox/apple/5sing 为已存凭据。', + description: 'wy 为实时校验(未登录或 cookie 失效返回 logged: false),kg/tx/bl 为已存凭据(含 VIP 等级与最近更新时间),' + + 'qs/xm/me/lr/bd/jx/apple/5s 为已存凭据。', tags: ['登录'], }, }, async() => { @@ -493,11 +493,11 @@ export const loginRoutes = async(fastify) => { xm: xmStatus(), me: meStatus(), lr: lrAuth.status(), - bili: biliAuth.status(), - qian: qianStatus(), - joox: jooxStatus(), + bl: biliAuth.status(), + bd: qianStatus(), + jx: jooxStatus(), apple: appleStatus(), - '5sing': fivesingStatus(), + '5s': fivesingStatus(), } }) } diff --git a/src/routes.ts b/src/routes.ts index a85690a..ba8635e 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -513,7 +513,7 @@ export const musicRoutes = async(fastify: any) => { const albumSchema = (withPage: boolean) => ({ summary: '获取专辑详情与曲目列表', - description: '支持 kg/kw/tx/wy/xm/me/qt/lr/migu/qian/joox/apple(见 /sources 的 album 能力位)。' + + description: '支持 kg/kw/tx/wy/xm/me/qt/lr/migu/bd/jx/apple(见 /sources 的 album 能力位)。' + 'kw 单页固定 1000 条、xm/me 固定 30 条、limit 不生效。', tags: ['歌手专辑'], params: params({ @@ -538,7 +538,7 @@ export const musicRoutes = async(fastify: any) => { fastify.get('/album/:source/search/:keyword', { schema: { summary: '搜索专辑', - description: '支持 kg/kw/tx/wy/migu/qian/joox/apple(见 /sources 的 albumSearch 能力位)。', + description: '支持 kg/kw/tx/wy/migu/bd/jx/apple(见 /sources 的 albumSearch 能力位)。', tags: ['歌手专辑'], params: params({ source: pSource('album.searchAlbum'), keyword: { type: 'string', description: '专辑名关键词', example: '魔杰座' } }), querystring: qs({ page: pPage, limit: pLimit }), @@ -563,8 +563,8 @@ export const musicRoutes = async(fastify: any) => { fastify.get('/playlist/:source/search/:keyword', { schema: { summary: '搜索歌单', - description: '支持 kg/kw/tx/wy/migu/qian/joox/bili/5sing/apple(见 /sources 的 playlistSearch 能力位)。' + - '部分平台该能力受限(如 qian 歌单搜索接口已下线、bili 需登录),失败时返回 502。', + description: '支持 kg/kw/tx/wy/migu/bd/jx/bl/5s/apple(见 /sources 的 playlistSearch 能力位)。' + + '部分平台该能力受限(如 bd 歌单搜索接口已下线、bl 需登录),失败时返回 502。', tags: ['歌单'], params: params({ source: pSource('playlist.searchPlaylist'), keyword: { type: 'string', example: '华语' } }), querystring: qs({ page: pPage, limit: pLimit }), @@ -586,7 +586,7 @@ export const musicRoutes = async(fastify: any) => { fastify.get('/playlist/:source/categories', { schema: { summary: '获取歌单分类', - description: '支持 kw/kg/tx/wy/migu/qian/joox/bili/5sing/apple(见 /sources 的 playlistCategories 能力位)。', + description: '支持 kw/kg/tx/wy/migu/bd/jx/bl/5s/apple(见 /sources 的 playlistCategories 能力位)。', tags: ['歌单'], params: params({ source: pSource('playlist.getCategories') }), response: responses('PlaylistCategoriesResult'), @@ -630,8 +630,8 @@ export const musicRoutes = async(fastify: any) => { const playlistSchema = (withPage: boolean) => ({ summary: '获取歌单详情与曲目列表', - description: 'id 为歌单 id 或链接(部分平台支持链接/裸 id,如 bili 的 season:::、' + - 'joox/qian/migu 的网页链接)。列表按 page/limit 切片(聚合接口自身分页的平台由平台决定)。', + description: 'id 为歌单 id 或链接(部分平台支持链接/裸 id,如 bl 的 season:::、' + + 'jx/bd/migu 的网页链接)。列表按 page/limit 切片(聚合接口自身分页的平台由平台决定)。', tags: ['歌单'], params: params({ source: pSource('playlist'), @@ -701,7 +701,7 @@ export const musicRoutes = async(fastify: any) => { schema: { summary: `解析${kind === 'song' ? '歌曲' : kind === 'album' ? '专辑' : '歌单'}链接`, description: '把平台链接(或裸 id)解析成结构化歌曲/曲目数据。支持平台见 /sources 的 parse 能力位;' + - '5sing 歌曲/歌单、apple/bili/joox/qian/migu 三类、kw/kg/tx/wy 部分支持。', + '5s 歌曲/歌单、apple/bl/jx/bd/migu 三类、kw/kg/tx/wy 部分支持。', tags: ['链接解析'], params: params({ source: pSource('parse.parseSong') }), body: parseBodySchema, diff --git a/src/server.ts b/src/server.ts index 8aa95f6..ac71e03 100644 --- a/src/server.ts +++ b/src/server.ts @@ -48,8 +48,8 @@ const PORT = process.env.PORT || 16688 const DOCS_PREFIX = '/docs' const API_TITLE = 'MusicServer 聚合音乐 API' const API_DESCRIPTION = '多平台音乐聚合接口:搜索、播放地址、歌词、封面、榜单、歌单、专辑、歌手、链接解析。' + - '支持 kw(酷我)/ kg(酷狗)/ tx(QQ音乐)/ wy(网易云)/ qs(汽水音乐)/ migu(咪咕)/ qian(千千)/ ' + - 'joox / bili(哔哩哔哩)/ 5sing / apple(Apple Music,仅试听)与有声平台 xm(喜马拉雅)/ me(猫耳FM)/ qt(蜻蜓FM)/ lr(懒人听书)。' + '支持 kw(酷我)/ kg(酷狗)/ tx(QQ音乐)/ wy(网易云)/ qs(汽水音乐)/ migu(咪咕)/ bd(千千)/ ' + + 'jx / bl(哔哩哔哩)/ 5s / apple(Apple Music,仅试听)与有声平台 xm(喜马拉雅)/ me(猫耳FM)/ qt(蜻蜓FM)/ lr(懒人听书)。' const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8')) diff --git a/test/smoke-test.ts b/test/smoke-test.ts index 14d4996..f99afd9 100644 --- a/test/smoke-test.ts +++ b/test/smoke-test.ts @@ -514,11 +514,11 @@ await test('专辑详情 /album/lr', async() => { return `${r.data.info.total} 节` }) -// ---- 新平台(migu/qian/joox/bili/5sing/apple 移植回归) ---- +// ---- 新平台(migu/bd/jx/bl/5s/apple 移植回归) ---- -for (const source of ['migu', 'qian', 'joox', 'bili', '5sing', 'apple']) { +for (const source of ['migu', 'bd', 'jx', 'bl', '5s', 'apple']) { await test(`搜索 ${source} 直查`, async() => { - const r = await get(`/search/${source}/${encodeURIComponent(source === '5sing' ? '董贞' : '周杰伦')}?limit=5`) + const r = await get(`/search/${source}/${encodeURIComponent(source === '5s' ? '董贞' : '周杰伦')}?limit=5`) if (r.code != 0) throw new Error(r.msg) if (!r.data?.list?.length) throw new Error('空列表') return `${r.data.list.length} 条,首条 ${r.data.list[0].name}` @@ -536,21 +536,21 @@ await test('URL migu(多档候选链)', async() => { return `实际 ${u.data.type},${url.slice(0, 60)}` }) -await test('URL qian 免费曲', async() => { - // qian 免费曲目少,用「SSR」(上游测试同款厂牌)搜到带直链的曲目 - const r = await get(`/search/qian/${encodeURIComponent('SSR')}?limit=10`) +await test('URL bd 免费曲', async() => { + // bd 免费曲目少,用「SSR」(上游测试同款厂牌)搜到带直链的曲目 + const r = await get(`/search/bd/${encodeURIComponent('SSR')}?limit=10`) if (r.code != 0 || !r.data?.list?.length) throw new Error('搜索失败,无法取流') const song = r.data.list.find((s: any) => !s.payPlay) if (!song) throw new Error('未搜到免费曲目') - const u = await get(`/url/qian/${encodeURIComponent(song.songmid)}?quality=128k`) + const u = await get(`/url/bd/${encodeURIComponent(song.songmid)}?quality=128k`) if (u.code != 0) throw new Error(u.msg) return `实际 ${u.data.type},${String(u.data.url).slice(0, 60)}` }) -await test('URL 5sing(档位 sq/hq/lq)', async() => { - const r = await get(`/search/5sing/${encodeURIComponent('董贞')}?limit=5`) +await test('URL 5s(档位 sq/hq/lq)', async() => { + const r = await get(`/search/5s/${encodeURIComponent('董贞')}?limit=5`) if (r.code != 0 || !r.data?.list?.length) throw new Error('搜索失败,无法取流') - const u = await get(`/url/5sing/${encodeURIComponent(r.data.list[0].songmid)}?quality=128k`) + const u = await get(`/url/5s/${encodeURIComponent(r.data.list[0].songmid)}?quality=128k`) if (u.code != 0) throw new Error(u.msg) return `实际 ${u.data.type},${String(u.data.url).slice(0, 60)}` }) @@ -565,11 +565,11 @@ await test('歌词 migu', async() => { return l.data.lyric.slice(0, 40) }) -await test('歌词 5sing', async() => { - const r = await get(`/search/5sing/${encodeURIComponent('董贞')}?limit=5`) +await test('歌词 5s', async() => { + const r = await get(`/search/5s/${encodeURIComponent('董贞')}?limit=5`) const song = r.data?.list?.[0] if (!song) throw new Error('搜索失败') - const l = await get(`/lyric/5sing/${encodeURIComponent(song.songmid)}`) + const l = await get(`/lyric/5s/${encodeURIComponent(song.songmid)}`) if (l.code != 0) throw new Error(l.msg) if (!l.data?.lyric) throw new Error('空歌词') return l.data.lyric.slice(0, 40) @@ -585,7 +585,7 @@ await test('封面 migu', async() => { return String(p.data.url).slice(0, 60) }) -for (const source of ['migu', 'qian', 'joox', 'apple']) { +for (const source of ['migu', 'bd', 'jx', 'apple']) { await test(`专辑搜索 /album/${source}/search`, async() => { const r = await get(`/album/${source}/search/${encodeURIComponent('Hello')}?limit=3`) if (r.code != 0) throw new Error(r.msg) @@ -604,7 +604,7 @@ await test('专辑详情 /album/migu', async() => { return `${a.data.info.name}(${a.data.total} 首)` }) -for (const source of ['migu', 'qian', 'joox', 'apple']) { +for (const source of ['migu', 'bd', 'jx', 'apple']) { await test(`歌单分类 /playlist/${source}/categories`, async() => { const r = await get(`/playlist/${source}/categories`) if (r.code != 0) throw new Error(r.msg) @@ -635,11 +635,11 @@ await test('歌单详情 /playlist/migu', async() => { return `${d.data.total} 首,本页 ${d.data.list.length} 首` }) -await test('歌单详情 /playlist/bili(合集)', async() => { - const s = await get(`/playlist/bili/search/${encodeURIComponent('周杰伦')}?limit=3`) +await test('歌单详情 /playlist/bl(合集)', async() => { + const s = await get(`/playlist/bl/search/${encodeURIComponent('周杰伦')}?limit=3`) const id = s.data?.list?.[0]?.id if (!id) throw new Error('搜索无歌单') - const d = await get(`/playlist/bili/${encodeURIComponent(id)}?limit=5`) + const d = await get(`/playlist/bl/${encodeURIComponent(id)}?limit=5`) if (d.code != 0) throw new Error(d.msg) if (!d.data?.list?.length) throw new Error('歌单无曲目') return `${d.data.total} 首` @@ -652,8 +652,8 @@ await test('链接解析 /parse/song/migu', async() => { return `${r.body.data.song.name} / ${r.body.data.song.singer}` }) -await test('链接解析 /parse/playlist/qian', async() => { - const r = await postJson('/parse/playlist/qian', { link: 'https://music.91q.com/songlist/309735' }) +await test('链接解析 /parse/playlist/bd', async() => { + const r = await postJson('/parse/playlist/bd', { link: 'https://music.91q.com/songlist/309735' }) if (r.status !== 200 || r.body?.code != 0) throw new Error(JSON.stringify(r.body).slice(0, 120)) if (!r.body.data?.list?.length) throw new Error('无曲目') return `${r.body.data.playlist?.name ?? ''}(${r.body.data.total} 首)` @@ -682,24 +682,24 @@ await test('链接解析 /parse/song/kw(正向)', async() => { return `kw 解析成功: ${r.body.data.song.name}` }) -await test('扫码二维码创建 POST /login/bili/qr', async() => { - const r = await postRaw('/login/bili/qr') +await test('扫码二维码创建 POST /login/bl/qr', async() => { + const r = await postRaw('/login/bl/qr') const body = JSON.parse(r.body) if (r.status !== 200 || body.code != 0 || !body.data?.key) throw new Error(r.body.slice(0, 120)) return `key=${String(body.data.key).slice(0, 12)}…` }) -await test('扫码轮询 POST /login/bili/qr/check', async() => { - const created = JSON.parse((await postRaw('/login/bili/qr')).body) +await test('扫码轮询 POST /login/bl/qr/check', async() => { + const created = JSON.parse((await postRaw('/login/bl/qr')).body) const key = created.data?.key if (!key) throw new Error('未取到 key') - const r = await postJson('/login/bili/qr/check', { key }) + const r = await postJson('/login/bl/qr/check', { key }) if (r.status !== 200 || r.body?.code != 0) throw new Error(JSON.stringify(r.body).slice(0, 120)) return `status=${r.body.data.status} code=${r.body.data.code}` }) await test('个人歌单 未支持的平台 → 502', async() => { - const r = await getRaw('/user-playlist/5sing') + const r = await getRaw('/user-playlist/5s') if (r.status !== 502 && r.status !== 404) throw new Error(`期望 502/404,实际 ${r.status}`) return JSON.parse(r.body).msg?.slice(0, 60) })