From 354df2e739e7e6d50fd44e18f7440e2b6fe45d09 Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Fri, 25 Sep 2026 23:06:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=9E=B6=E6=9E=84=E6=95=B4?= =?UTF-8?q?=E5=9B=BA=E2=80=94=E2=80=94=E5=88=86=E5=B1=82=E5=8D=95=E5=90=91?= =?UTF-8?q?=E5=8C=96=E3=80=81=E5=85=A8=E9=87=8F=20TypeScript=E3=80=81?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E8=BF=90=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 bd/mg 两平台及磁盘残留产物;清理 api-test 死脚本、musicSdk/message+options 死模块、infSign vendor - musicSdk/ 改名 sdk/;src/common、src/auth、src/apis 整体移入 sdk/,依赖方向定为 src → sdk 严格单向 - src/ 手写 JS 全量转 .ts,删除 JS 类型垫片 modules.d.ts;源码 import 统一 .ts 后缀 - Node 原生类型剥离直接运行 .ts(engines >=22.18),取消 tsc 发射产物,tsc 仅类型检查(pnpm typecheck) - 删除 kwdes 备用实现与 src/index.ts 转发垫片;测试移至 test/smoke-test.ts - common/request.ts 补显式导出类型;修复 set-cookie 单值形态、ApiError 类字段等隐患 验证:tsc 零错误,smoke-test 41/41 --- .gitignore | 10 +- README.md | 63 ++- docs/API.md | 6 +- musicSdk/bd/api-test.js | 26 -- musicSdk/bd/hotSearch.ts | 24 -- musicSdk/bd/index.ts | 40 -- musicSdk/bd/leaderboard.ts | 153 -------- musicSdk/bd/musicInfo.ts | 20 - musicSdk/bd/musicSearch.ts | 88 ----- musicSdk/bd/songList.ts | 264 ------------- musicSdk/kg/api-test.js | 50 --- musicSdk/kg/vendors/infSign.min.js | 2 - musicSdk/kw/api-temp.js | 26 -- musicSdk/kw/api-test.js | 37 -- musicSdk/message.ts | 4 - musicSdk/mg/album.js | 49 --- musicSdk/mg/api-test.js | 26 -- musicSdk/mg/comment.ts | 97 ----- musicSdk/mg/hotSearch.ts | 19 - musicSdk/mg/index.ts | 34 -- musicSdk/mg/leaderboard.ts | 252 ------------ musicSdk/mg/lyric.ts | 104 ----- musicSdk/mg/musicInfo.ts | 163 -------- musicSdk/mg/musicSearch.ts | 224 ----------- musicSdk/mg/pic.ts | 28 -- musicSdk/mg/songId.ts | 32 -- musicSdk/mg/songList.ts | 363 ------------------ musicSdk/mg/tipSearch.ts | 25 -- musicSdk/mg/utils/index.ts | 30 -- musicSdk/mg/utils/mrc.ts | 104 ----- musicSdk/options.ts | 9 - musicSdk/request.ts | 8 - musicSdk/tx/api-test.js | 29 -- musicSdk/wy/api-test.js | 50 --- package.json | 11 +- {musicSdk => sdk}/api-source-info.ts | 6 +- {musicSdk => sdk}/api-source.ts | 17 +- {src => sdk}/apis/index.ts | 15 +- src/apis/kg.js => sdk/apis/kg.ts | 10 +- src/apis/kw.js => sdk/apis/kw.ts | 26 +- .../source-ivm.js => sdk/apis/source-ivm.ts | 24 +- src/apis/source.js => sdk/apis/source.ts | 16 +- sdk/apis/tx.ts | 87 +++++ src/apis/wy.js => sdk/apis/wy.ts | 9 +- src/auth/kg.js => sdk/auth/kg.ts | 6 +- {src => sdk}/auth/store.ts | 17 +- src/auth/wy.js => sdk/auth/wy.ts | 8 +- .../ipcNames.js => sdk/common/ipcNames.ts | 0 .../common/lyricDecode.ts | 4 +- .../kg.js => sdk/common/lyricUtils/kg.ts | 10 +- .../message.js => sdk/common/message.ts | 0 .../qrcDecode.js => sdk/common/qrcDecode.ts | 2 +- .../common/rendererIpc.ts | 4 +- .../request.js => sdk/common/request.ts | 66 +++- src/common/utils.js => sdk/common/utils.ts | 0 {musicSdk => sdk}/index.ts | 7 +- {musicSdk => sdk}/kg/album.ts | 0 {musicSdk => sdk}/kg/comment.ts | 2 +- {musicSdk => sdk}/kg/hotSearch.ts | 0 {musicSdk => sdk}/kg/index.ts | 2 +- {musicSdk => sdk}/kg/leaderboard.ts | 0 {musicSdk => sdk}/kg/lyric.ts | 2 +- {musicSdk => sdk}/kg/musicInfo.ts | 0 {musicSdk => sdk}/kg/musicSearch.ts | 0 {musicSdk => sdk}/kg/pic.ts | 0 {musicSdk => sdk}/kg/singer.ts | 0 {musicSdk => sdk}/kg/songList.ts | 0 {musicSdk => sdk}/kg/tipSearch.ts | 0 {musicSdk => sdk}/kg/util.ts | 0 {musicSdk => sdk}/kw/album.ts | 0 {musicSdk => sdk}/kw/comment.ts | 0 {musicSdk => sdk}/kw/hotSearch.ts | 0 {musicSdk => sdk}/kw/index.ts | 0 {musicSdk => sdk}/kw/leaderboard.ts | 0 {musicSdk => sdk}/kw/lyric.ts | 0 {musicSdk => sdk}/kw/musicSearch.ts | 2 +- {musicSdk => sdk}/kw/pic.ts | 0 {musicSdk => sdk}/kw/songList.ts | 0 {musicSdk => sdk}/kw/tipSearch.ts | 0 {musicSdk => sdk}/kw/util.ts | 4 +- sdk/request.ts | 5 + {musicSdk => sdk}/tx/album.ts | 0 {musicSdk => sdk}/tx/comment.ts | 0 {musicSdk => sdk}/tx/hotSearch.ts | 0 {musicSdk => sdk}/tx/index.ts | 0 {musicSdk => sdk}/tx/leaderboard.ts | 0 {musicSdk => sdk}/tx/lyric.ts | 6 +- {musicSdk => sdk}/tx/musicInfo.ts | 0 {musicSdk => sdk}/tx/musicSearch.ts | 0 {musicSdk => sdk}/tx/singer.ts | 0 {musicSdk => sdk}/tx/songList.ts | 0 {musicSdk => sdk}/tx/tipSearch.ts | 0 {musicSdk => sdk}/tx/utils/crypto.ts | 0 {musicSdk => sdk}/tx/utils/index.ts | 0 {musicSdk => sdk}/types.ts | 6 +- {musicSdk => sdk}/utils.ts | 2 +- {musicSdk => sdk}/wy/comment.ts | 0 {musicSdk => sdk}/wy/hotSearch.ts | 0 {musicSdk => sdk}/wy/index.ts | 2 +- {musicSdk => sdk}/wy/leaderboard.ts | 0 {musicSdk => sdk}/wy/lyric.ts | 2 +- {musicSdk => sdk}/wy/musicDetail.ts | 0 {musicSdk => sdk}/wy/musicInfo.ts | 0 {musicSdk => sdk}/wy/musicSearch.ts | 6 +- {musicSdk => sdk}/wy/singer.ts | 0 {musicSdk => sdk}/wy/songList.ts | 0 {musicSdk => sdk}/wy/tipSearch.ts | 0 {musicSdk => sdk}/wy/utils/crypto.ts | 0 {musicSdk => sdk}/wy/utils/index.ts | 2 +- src/apis/bd.js | 37 -- src/apis/index.js | 17 - src/apis/mg.js | 84 ---- src/apis/tx.js | 55 --- src/auth/store.js | 45 --- src/http/api-error.js | 22 -- src/http/api-error.ts | 24 ++ src/index.js | 11 - src/openapi/{components.js => components.ts} | 2 +- src/openapi/{ui.js => ui.ts} | 0 src/renderer/store.js | 15 - src/{routes-login.js => routes-login.ts} | 42 +- src/{routes.js => routes.ts} | 32 +- src/{server.js => server.ts} | 30 +- src/types/modules.d.ts | 56 --- src/smoke-test.js => test/smoke-test.ts | 31 +- tsconfig.json | 5 +- 126 files changed, 413 insertions(+), 2942 deletions(-) delete mode 100644 musicSdk/bd/api-test.js delete mode 100644 musicSdk/bd/hotSearch.ts delete mode 100644 musicSdk/bd/index.ts delete mode 100644 musicSdk/bd/leaderboard.ts delete mode 100644 musicSdk/bd/musicInfo.ts delete mode 100644 musicSdk/bd/musicSearch.ts delete mode 100644 musicSdk/bd/songList.ts delete mode 100644 musicSdk/kg/api-test.js delete mode 100644 musicSdk/kg/vendors/infSign.min.js delete mode 100644 musicSdk/kw/api-temp.js delete mode 100644 musicSdk/kw/api-test.js delete mode 100644 musicSdk/message.ts delete mode 100644 musicSdk/mg/album.js delete mode 100644 musicSdk/mg/api-test.js delete mode 100644 musicSdk/mg/comment.ts delete mode 100644 musicSdk/mg/hotSearch.ts delete mode 100644 musicSdk/mg/index.ts delete mode 100644 musicSdk/mg/leaderboard.ts delete mode 100644 musicSdk/mg/lyric.ts delete mode 100644 musicSdk/mg/musicInfo.ts delete mode 100644 musicSdk/mg/musicSearch.ts delete mode 100644 musicSdk/mg/pic.ts delete mode 100644 musicSdk/mg/songId.ts delete mode 100644 musicSdk/mg/songList.ts delete mode 100644 musicSdk/mg/tipSearch.ts delete mode 100644 musicSdk/mg/utils/index.ts delete mode 100644 musicSdk/mg/utils/mrc.ts delete mode 100644 musicSdk/options.ts delete mode 100644 musicSdk/request.ts delete mode 100644 musicSdk/tx/api-test.js delete mode 100644 musicSdk/wy/api-test.js rename {musicSdk => sdk}/api-source-info.ts (77%) rename {musicSdk => sdk}/api-source.ts (72%) rename {src => sdk}/apis/index.ts (70%) rename src/apis/kg.js => sdk/apis/kg.ts (92%) rename src/apis/kw.js => sdk/apis/kw.ts (71%) rename src/apis/source-ivm.js => sdk/apis/source-ivm.ts (98%) rename src/apis/source.js => sdk/apis/source.ts (85%) create mode 100644 sdk/apis/tx.ts rename src/apis/wy.js => sdk/apis/wy.ts (81%) rename src/auth/kg.js => sdk/auth/kg.ts (96%) rename {src => sdk}/auth/store.ts (77%) rename src/auth/wy.js => sdk/auth/wy.ts (95%) rename src/common/ipcNames.js => sdk/common/ipcNames.ts (100%) rename src/common/lyricDecode.js => sdk/common/lyricDecode.ts (93%) rename src/common/lyricUtils/kg.js => sdk/common/lyricUtils/kg.ts (88%) rename src/common/message.js => sdk/common/message.ts (100%) rename src/common/qrcDecode.js => sdk/common/qrcDecode.ts (99%) rename src/common/rendererIpc.js => sdk/common/rendererIpc.ts (94%) rename src/common/request.js => sdk/common/request.ts (74%) rename src/common/utils.js => sdk/common/utils.ts (100%) rename {musicSdk => sdk}/index.ts (95%) rename {musicSdk => sdk}/kg/album.ts (100%) rename {musicSdk => sdk}/kg/comment.ts (98%) rename {musicSdk => sdk}/kg/hotSearch.ts (100%) rename {musicSdk => sdk}/kg/index.ts (95%) rename {musicSdk => sdk}/kg/leaderboard.ts (100%) rename {musicSdk => sdk}/kg/lyric.ts (98%) rename {musicSdk => sdk}/kg/musicInfo.ts (100%) rename {musicSdk => sdk}/kg/musicSearch.ts (100%) rename {musicSdk => sdk}/kg/pic.ts (100%) rename {musicSdk => sdk}/kg/singer.ts (100%) rename {musicSdk => sdk}/kg/songList.ts (100%) rename {musicSdk => sdk}/kg/tipSearch.ts (100%) rename {musicSdk => sdk}/kg/util.ts (100%) rename {musicSdk => sdk}/kw/album.ts (100%) rename {musicSdk => sdk}/kw/comment.ts (100%) rename {musicSdk => sdk}/kw/hotSearch.ts (100%) rename {musicSdk => sdk}/kw/index.ts (100%) rename {musicSdk => sdk}/kw/leaderboard.ts (100%) rename {musicSdk => sdk}/kw/lyric.ts (100%) rename {musicSdk => sdk}/kw/musicSearch.ts (98%) rename {musicSdk => sdk}/kw/pic.ts (100%) rename {musicSdk => sdk}/kw/songList.ts (100%) rename {musicSdk => sdk}/kw/tipSearch.ts (100%) rename {musicSdk => sdk}/kw/util.ts (98%) create mode 100644 sdk/request.ts rename {musicSdk => sdk}/tx/album.ts (100%) rename {musicSdk => sdk}/tx/comment.ts (100%) rename {musicSdk => sdk}/tx/hotSearch.ts (100%) rename {musicSdk => sdk}/tx/index.ts (100%) rename {musicSdk => sdk}/tx/leaderboard.ts (100%) rename {musicSdk => sdk}/tx/lyric.ts (96%) rename {musicSdk => sdk}/tx/musicInfo.ts (100%) rename {musicSdk => sdk}/tx/musicSearch.ts (100%) rename {musicSdk => sdk}/tx/singer.ts (100%) rename {musicSdk => sdk}/tx/songList.ts (100%) rename {musicSdk => sdk}/tx/tipSearch.ts (100%) rename {musicSdk => sdk}/tx/utils/crypto.ts (100%) rename {musicSdk => sdk}/tx/utils/index.ts (100%) rename {musicSdk => sdk}/types.ts (95%) rename {musicSdk => sdk}/utils.ts (96%) rename {musicSdk => sdk}/wy/comment.ts (100%) rename {musicSdk => sdk}/wy/hotSearch.ts (100%) rename {musicSdk => sdk}/wy/index.ts (95%) rename {musicSdk => sdk}/wy/leaderboard.ts (100%) rename {musicSdk => sdk}/wy/lyric.ts (99%) rename {musicSdk => sdk}/wy/musicDetail.ts (100%) rename {musicSdk => sdk}/wy/musicInfo.ts (100%) rename {musicSdk => sdk}/wy/musicSearch.ts (95%) rename {musicSdk => sdk}/wy/singer.ts (100%) rename {musicSdk => sdk}/wy/songList.ts (100%) rename {musicSdk => sdk}/wy/tipSearch.ts (100%) rename {musicSdk => sdk}/wy/utils/crypto.ts (100%) rename {musicSdk => sdk}/wy/utils/index.ts (94%) delete mode 100644 src/apis/bd.js delete mode 100644 src/apis/index.js delete mode 100644 src/apis/mg.js delete mode 100644 src/apis/tx.js delete mode 100644 src/auth/store.js delete mode 100644 src/http/api-error.js create mode 100644 src/http/api-error.ts delete mode 100644 src/index.js rename src/openapi/{components.js => components.ts} (99%) rename src/openapi/{ui.js => ui.ts} (100%) delete mode 100644 src/renderer/store.js rename src/{routes-login.js => routes-login.ts} (72%) rename src/{routes.js => routes.ts} (93%) rename src/{server.js => server.ts} (90%) delete mode 100644 src/types/modules.d.ts rename src/smoke-test.js => test/smoke-test.ts (94%) diff --git a/.gitignore b/.gitignore index d0ce6b0..3264739 100644 --- a/.gitignore +++ b/.gitignore @@ -35,9 +35,7 @@ dist/ coverage/ .nyc_output/ -# TypeScript in-place emit (musicSdk source is .ts) -musicSdk/**/*.js -!musicSdk/**/api-test.js -!musicSdk/**/api-temp.js -!musicSdk/**/temp/*.js -!musicSdk/**/vendors/*.js +# TypeScript:源码全部 .ts、由 Node 直接运行(类型剥离),不再生成 .js 产物。 +# 以下规则仅作保险,防止旧的发射产物被误生成后提交。 +src/**/*.js +sdk/**/*.js diff --git a/README.md b/README.md index b1dba48..e4a1bfc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 功能 -- **多平台聚合**:酷我(kw)、酷狗(kg)、QQ音乐(tx)、网易云(wy)、咪咕(mg) +- **多平台聚合**:酷我(kw)、酷狗(kg)、QQ音乐(tx)、网易云(wy) - **直连取流**:各平台播放地址获取(不依赖第三方 API 服务) - **完整链路**:搜索、播放地址、歌词(含各平台加密歌词解码)、封面、榜单、热搜、歌单 @@ -12,8 +12,9 @@ ```bash pnpm install -pnpm start # 默认端口 16688,可用 PORT 环境变量修改 -pnpm test # 运行冒烟测试(自动启动/关闭本地服务器) +pnpm start # 默认端口 16688,可用 PORT 环境变量修改(需 Node 22.18+,直接运行 .ts) +pnpm test # 类型检查(tsc)+ 冒烟测试(自动启动/关闭本地服务器) +pnpm typecheck # 仅类型检查,不运行 ``` ## HTTP 接口 @@ -34,7 +35,7 @@ pnpm test # 运行冒烟测试(自动启动/关闭本地服务器) | `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 /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/mg/tx) | +| `GET /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/tx) | | `POST /login/wy/qr` → `POST /login/wy/qr/check` | 网易云扫码登录(扫码后轮询完成登录) | | `POST /login/wy/cellphone` | 网易云手机号登录(password 或 captcha) | | `POST /login/kg` + `userid` | 绑定酷狗 token(App 抓包获取,服务端验证) | @@ -51,40 +52,38 @@ pnpm test # 运行冒烟测试(自动启动/关闭本地服务器) - **kw / tx / wy**:`songmid`(如 `228908`、`0039MnYb0qxYhV`) - **kg**:`hash`(搜索结果的 `hash` 字段;当前环境 kg 取流接口被服务端限制,URL 获取可能失败) -- **mg**:`songmid`(搜索结果的 `songmid` 字段,内部自动换取 contentId) ## 项目结构 ``` -src/ - server.js HTTP 服务器入口(pnpm start 实际启动文件,Fastify) - routes.js 业务路由(JSON Schema 校验 + OpenAPI 元数据) - smoke-test.js 冒烟测试(自动启动本地服务器) - index.js SDK 根模块(工具函数聚合导出) - http/ HTTP 层 - api-error.js 业务错误类型(404/502 等 → 统一 JSON 响应) - openapi/ 文档层 - components.js OpenAPI 共享 schema(与路由校验同源) - ui.js ReDoc 页面模板 - apis/ 各平台 URL 直连实现(kw/kg/tx/wy/mg/bd) - source.js 外部音源懒加载入口(sources/ 目录脚本,不入库) - source-ivm.js isolated-vm 隔离执行器 - common/ 公共模块 - request.js HTTP 请求(可取消、自动 JSON 解析、gzip/deflate 解压) - utils.js 工具函数(sizeFormate/formatPlayTime/dateFormat 等) - message.js 请求错误消息 - ipcNames.js IPC 事件名(兼容 SDK) - rendererIpc.js IPC 本地实现(kw/tx 歌词解码) - lyricDecode.js 酷我/QQ 歌词解码(替代原 Electron 主进程处理器) - qrcDecode.js QQ 歌词 3DES 解密(纯 JS 移植) - lyricUtils/kg.js 酷狗 krc 歌词解码 -musicSdk/ 音乐聚合 SDK(搜索/歌词/榜单等核心逻辑,TypeScript 源码) - index.js 平台聚合入口 - request.js SDK 请求模块(转接 src/common/request.js) - message.js SDK 消息模块(转接 src/common/message.js) +src/ HTTP 应用层(Fastify 服务器,全部 TypeScript 源码) + server.ts HTTP 服务器入口(pnpm start 实际启动文件) + routes.ts 业务路由(JSON Schema 校验 + OpenAPI 元数据) + routes-login.ts 登录路由(wy 扫码/手机号、kg token、tx cookie) + http/ + api-error.ts 业务错误类型(404/502 等 → 统一 JSON 响应) + openapi/ + components.ts OpenAPI 共享 schema(与路由校验同源) + ui.ts ReDoc 页面模板 +sdk/ 音乐聚合 SDK(自包含:平台模块 + 取流 + 凭据 + 基础设施;kw/kg/tx/wy 四平台) + index.ts 平台聚合入口(通用工具转发,实现在 common/utils.ts) + api-source.ts 接口选择层(internal 内置直连 / sources/ 外部音源脚本,SOURCE_ENABLED 切换) + apis/ 各平台 URL 直连取流实现(kw/kg/tx/wy)+ 外部音源加载器(source.ts / source-ivm.ts) + auth/ 登录态与登录协议(store.ts 持久化凭据到 data/auth.json;wy.ts 扫码/手机号、kg.ts token) + common/ SDK 基础设施 + request.ts HTTP 请求(可取消、自动 JSON 解析、gzip/deflate 解压) + utils.ts 工具函数(sizeFormate/formatPlayTime/dateFormat 等) + message.ts 请求错误消息 + ipcNames.ts IPC 事件名(兼容 lx-music 结构) + rendererIpc.ts IPC 本地实现(kw/tx 歌词解码) + lyricDecode.ts 酷我/QQ 歌词解码 + qrcDecode.ts QQ 歌词 3DES 解密(纯 JS 移植) + lyricUtils/kg.ts 酷狗 krc 歌词解码 +test/ + smoke-test.ts 冒烟测试(pnpm test 运行,自动启动/关闭本地服务器) ``` -> TypeScript 源码(`src/apis/index.ts`、`musicSdk/**/*.ts`)经 `pnpm run build`(tsc)就地发射为 `.js` 产物后运行。 +> 分层依赖严格单向:`src` → `sdk`,`sdk` 内部自包含(不反向依赖 src)。源码统一为 TypeScript(import 一律写 `.ts` 后缀),由 Node ≥22.18 的原生类型剥离直接运行——**无编译产物**,改完即跑;tsc 仅用于类型检查(`pnpm typecheck`)。代码只使用可剥离的 TS 语法(无 enum/namespace/参数属性),新增代码也须遵守。 ## 已知限制 diff --git a/docs/API.md b/docs/API.md index 3482a73..097390c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # MusicServer 聚合音乐 API -多平台音乐聚合接口:搜索、播放地址、歌词、封面、榜单、歌单。支持平台:**kw**(酷我)/ **kg**(酷狗)/ **tx**(QQ音乐)/ **wy**(网易云)/ **mg**(咪咕)。 +多平台音乐聚合接口:搜索、播放地址、歌词、封面、榜单、歌单。支持平台:**kw**(酷我)/ **kg**(酷狗)/ **tx**(QQ音乐)/ **wy**(网易云)。 > 本文档为静态速查版。交互式文档以服务器实际输出为准: > @@ -20,7 +20,7 @@ - `502` 上游平台请求失败 - `500` 服务器内部错误 - 资源类接口统一为**路径参数风格**;`/search`、`/find` 搜索类接口使用 query 参数。 -- 平台歌曲 id 形态:kw/tx/wy/mg 用 `songmid`,kg 用 `hash`(mg 内部自动换取 contentId)。 +- 平台歌曲 id 形态:kw/tx/wy 用 `songmid`,kg 用 `hash`。 ## 接口速查 @@ -71,7 +71,7 @@ | GET | `/singer/:source/:id` | 歌手信息(name/desc/avatar 与歌曲/专辑数量)。支持 kg/tx/wy | | GET | `/singer/:source/:id/songs` | 歌手歌曲列表;可带 `page`/`limit`(默认 20)。支持 kg/tx/wy | | GET | `/singer/:source/:id/albums` | 歌手专辑列表;可带 `page`/`limit`。支持 kg/tx/wy | -| GET | `/album/:source/:id` | 专辑详情与曲目列表;可带 `page`/`limit`(limit 仅 kg 生效,kw 单页固定 1000 条)。支持 kg/kw/mg/tx;tx 的专辑名/作者由曲目数据合成(desc 为空) | +| GET | `/album/:source/:id` | 专辑详情与曲目列表;可带 `page`/`limit`(limit 仅 kg 生效,kw 单页固定 1000 条)。支持 kg/kw/tx;tx 的专辑名/作者由曲目数据合成(desc 为空) | 歌手/专辑 id 来源:搜索结果条目包含 `albumId`;歌手 id 需从平台方获取(不在搜索结果中)。 diff --git a/musicSdk/bd/api-test.js b/musicSdk/bd/api-test.js deleted file mode 100644 index 0491c48..0000000 --- a/musicSdk/bd/api-test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { httpFetch } from '../request.js' -import { requestMsg } from '../message.js' -import { headers, timeout } from '../options.js' -import { dnsLookup } from '../utils.js' - -const api_test = { - getMusicUrl(songInfo, type) { - const requestObj = httpFetch(`http://ts.tempmusics.tk/url/bd/${songInfo.songmid}/${type}`, { - method: 'get', - timeout, - headers, - lookup: dnsLookup, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { - if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) - switch (body.code) { - case 0: return Promise.resolve({ type, url: body.data }) - default: return Promise.reject(new Error(requestMsg.fail)) - } - }) - return requestObj - }, -} - -export default api_test diff --git a/musicSdk/bd/hotSearch.ts b/musicSdk/bd/hotSearch.ts deleted file mode 100644 index 965ae20..0000000 --- a/musicSdk/bd/hotSearch.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { httpFetch } from '../request.ts' -import type { HttpRequestObject } from '../types.ts' - -export default { - _requestObj: null as HttpRequestObject | null, - async getList(retryNum = 0) { - if (this._requestObj) this._requestObj.cancelHttp() - if (retryNum > 2) return Promise.reject(new Error('try max num')) - - const _requestObj = httpFetch('http://musicapi.qianqian.com/v1/restserver/ting?from=android&version=7.0.2.0&channel=ppzs&operator=0&method=baidu.ting.search.hot', { - method: 'get', - headers: { - 'User-Agent': 'android_7.0.2.0;baiduyinyue', - }, - }) - const { body, statusCode } = await _requestObj.promise - if (statusCode != 200 || body.error_code !== 22000) throw new Error('获取热搜词失败') - // console.log(body, statusCode) - return { source: 'bd', list: this.filterList(body.result) } - }, - filterList(rawList) { - return rawList.map(item => item.word) - }, -} diff --git a/musicSdk/bd/index.ts b/musicSdk/bd/index.ts deleted file mode 100644 index 1dc8e56..0000000 --- a/musicSdk/bd/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import leaderboard from './leaderboard.ts' -import { apis } from '../api-source.ts' -import musicInfo from './musicInfo.ts' -import songList from './songList.ts' -import { httpFetch } from '../request.ts' -import musicSearch from './musicSearch.ts' -import hotSearch from './hotSearch.ts' - -const bd = { - leaderboard, - songList, - musicSearch, - hotSearch, - getMusicUrl(songInfo, type) { - return apis('bd').getMusicUrl!(songInfo, type) - }, - getPic(songInfo) { - const requestObj = this.getMusicInfo(songInfo) - return requestObj.promise.then(info => info.pic_premium) - }, - getLyric(songInfo) { - const requestObj = this.getMusicInfo(songInfo) - requestObj.promise = requestObj.promise.then(info => httpFetch(info.lrclink).promise.then(resp => ({ lyric: resp.body, tlyric: '' }))) - return requestObj - }, - // getLyric(songInfo) { - // return apis('bd').getLyric(songInfo) - // }, - // getPic(songInfo) { - // return apis('bd').getPic(songInfo) - // }, - getMusicInfo(songInfo) { - return musicInfo.getMusicInfo(songInfo.songmid) - }, - getMusicDetailPageUrl(songInfo) { - return `http://music.taihe.com/song/${songInfo.songmid}` - }, -} - -export default bd diff --git a/musicSdk/bd/leaderboard.ts b/musicSdk/bd/leaderboard.ts deleted file mode 100644 index 36de6f4..0000000 --- a/musicSdk/bd/leaderboard.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { httpFetch } from '../request.ts' -// import { formatPlayTime } from '../index.js' - - -const boardList = [ - // { id: 'bd__601', name: '歌单榜', bangid: '601' }, - { id: 'bd__2', name: '热歌榜', bangid: '2' }, - { id: 'bd__20', name: '华语金曲榜', bangid: '20' }, - { id: 'bd__25', name: '网络歌曲榜', bangid: '25' }, - { id: 'bd__1', name: '新歌榜', bangid: '1' }, - { id: 'bd__21', name: '欧美金曲榜', bangid: '21' }, - { id: 'bd__200', name: '原创音乐榜', bangid: '200' }, - { id: 'bd__22', name: '经典老歌榜', bangid: '22' }, - { id: 'bd__24', name: '影视金曲榜', bangid: '24' }, - { id: 'bd__23', name: '情歌对唱榜', bangid: '23' }, - { id: 'bd__11', name: '摇滚榜', bangid: '11' }, - { id: 'bd__105', name: '好童星榜', bangid: '105' }, - { id: 'bd__106', name: '雅克•藏羌彝原创音乐榜', bangid: '106' }, -] - -export default { - limit: 20, - list: [ - { - id: 'bdrgb', - name: '热歌榜', - bangid: '2', - }, - { - id: 'bdxgb', - name: '新歌榜', - bangid: '1', - }, - { - id: 'bdycb', - name: '原创榜', - bangid: '200', - }, - { - id: 'bdhyjqb', - name: '华语榜', - bangid: '20', - }, - { - id: 'bdomjqb', - name: '欧美榜', - bangid: '21', - }, - { - id: 'bdwugqb', - name: '网络榜', - bangid: '25', - }, - { - id: 'bdjdlgb', - name: '老歌榜', - bangid: '22', - }, - { - id: 'bdysjqb', - name: '影视金曲榜', - bangid: '24', - }, - { - id: 'bdqgdcb', - name: '情歌对唱榜', - bangid: '23', - }, - { - id: 'bdygb', - name: '摇滚榜', - bangid: '11', - }, - ], - getUrl(id, p) { - return `http://musicmini.qianqian.com/2018/static/bangdan/bangdanList_${id}_${p}.html` - }, - regExps: { - item: /data-song="({.+?})"/g, - info: /{total[\s:]+"(\d+)", size[\s:]+"(\d+)", page[\s:]+"(\d+)"}/, - }, - getData(url) { - const requestObj = httpFetch(url) - return requestObj.promise - }, - filterData(rawList) { - // console.log(rawList) - return rawList.map(item => { - const types: any[] = [] - const _types: Record = {} - let size = null - types.push({ type: '128k', size }) - _types['128k'] = { - size, - } - if (item.biaoshi) { - types.push({ type: '320k', size }) - _types['320k'] = { - size, - } - types.push({ type: 'flac', size }) - _types.flac = { - size, - } - } - // types.reverse() - - return { - singer: item.song_artist.replace(',', '、'), - name: item.song_title, - albumName: item.album_title, - albumId: item.album_id, - source: 'bd', - interval: '', - songmid: item.song_id, - img: null, - lrc: null, - types, - _types, - typeUrl: {}, - } - }) - }, - parseData(rawData) { - // return rawData.map(item => JSON.parse(item.replace(this.regExps.item, '$1').replace(/"/g, '"').replace(/\\\//g, '/').replace(/(@s_1,w_)\d+(,h_)\d+/, '$1500$2500'))) - return rawData.map(item => JSON.parse(item.replace(this.regExps.item, '$1').replace(/"/g, '"').replace(/\\\//g, '/'))) - }, - async getBoards(retryNum = 0) { - this.list = boardList - return { - list: boardList, - source: 'bd', - } - }, - getList(bangid, page, retryNum = 0) { - if (++retryNum > 3) return Promise.reject(new Error('try max num')) - return this.getData(this.getUrl(bangid, page)).then(({ body }) => { - let result = body.match(this.regExps.item) - if (!result) return this.getList(bangid, page, retryNum) - let info = body.match(this.regExps.info) - if (!info) return this.getList(bangid, page, retryNum) - const list = this.filterData(this.parseData(result)) - this.limit = parseInt(info[2]) - return { - total: parseInt(info[1]), - list, - limit: this.limit, - page: parseInt(info[3]), - source: 'bd', - } - }) - }, -} diff --git a/musicSdk/bd/musicInfo.ts b/musicSdk/bd/musicInfo.ts deleted file mode 100644 index 8f114b6..0000000 --- a/musicSdk/bd/musicInfo.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { httpFetch } from '../request.ts' - -export default { - cache: {}, - getMusicInfo(songmid) { - if (this.cache[songmid]) { - return { promise: Promise.resolve(this.cache[songmid]) } - } - const requestObj = httpFetch(`https://musicapi.qianqian.com/v1/restserver/ting?method=baidu.ting.song.getSongLink&format=json&from=bmpc&version=1.0.0&version_d=11.1.6.0&songid=${songmid}&type=1&res=1&s_protocol=1&aac=2&project=tpass`) - requestObj.promise = requestObj.promise.then(({ body }) => { - // console.log(body) - if (body.error_code == 22000) { - this.cache[songmid] = body.result.songinfo - return body.result.songinfo - } - return Promise.reject(new Error('获取音乐信息失败')) - }) - return requestObj - }, -} diff --git a/musicSdk/bd/musicSearch.ts b/musicSdk/bd/musicSearch.ts deleted file mode 100644 index 6d49325..0000000 --- a/musicSdk/bd/musicSearch.ts +++ /dev/null @@ -1,88 +0,0 @@ -// import '../../polyfill/array.find.js' - -import { httpFetch } from '../request.ts' -import { formatPlayTime } from '../index.ts' -// import { debug } from '../../utils/env.js' -// import { formatSinger } from './util.js' - -export default { - limit: 30, - total: 0, - page: 0, - allPage: 1, - musicSearch(str, page, limit) { - const searchRequest = httpFetch(`http://tingapi.ting.baidu.com/v1/restserver/ting?from=android&version=5.6.5.6&method=baidu.ting.search.merge&format=json&query=${encodeURIComponent(str)}&page_no=${page}&page_size=${limit}&type=0&data_source=0&use_cluster=1`) - return searchRequest.promise.then(({ body }) => body) - }, - handleResult(rawData) { - let ids = new Set() - const list: any[] = [] - if (!rawData) return list - rawData.forEach(item => { - if (ids.has(item.song_id)) return - ids.add(item.song_id) - const types: any[] = [] - const _types: Record = {} - let size = null - let itemTypes = item.all_rate.split(',') - if (itemTypes.includes('128')) { - types.push({ type: '128k', size }) - _types['128k'] = { - size, - } - } - if (itemTypes.includes('320')) { - types.push({ type: '320k', size }) - _types['320k'] = { - size, - } - } - if (itemTypes.includes('flac')) { - types.push({ type: 'flac', size }) - _types.flac = { - size, - } - } - // types.reverse() - - list.push({ - singer: item.author.replace(',', '、'), - name: item.title, - albumName: item.album_title, - albumId: item.album_id, - source: 'bd', - interval: formatPlayTime(parseInt(item.file_duration)), - songmid: item.song_id, - img: null, - lrc: null, - types, - _types, - typeUrl: {}, - }) - }) - return list - }, - search(str, page = 1, limit, retryNum = 0) { - if (++retryNum > 3) return Promise.reject(new Error('try max num')) - if (limit == null) limit = this.limit - - return this.musicSearch(str, page, limit).then(result => { - if (!result || result.error_code !== 22000) return this.search(str, page, limit, retryNum) - let list = this.handleResult(result.result.song_info.song_list) - - if (list == null) return this.search(str, page, limit, retryNum) - - this.total = result.result.song_info.total - this.page = page - this.allPage = Math.ceil(this.total / limit) - - return Promise.resolve({ - list, - allPage: this.allPage, - limit, - total: this.total, - source: 'bd', - }) - }) - }, -} diff --git a/musicSdk/bd/songList.ts b/musicSdk/bd/songList.ts deleted file mode 100644 index 4ca8869..0000000 --- a/musicSdk/bd/songList.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { httpFetch } from '../request.ts' -import { formatPlayTime } from '../index.ts' -import { toMD5 } from '../utils.ts' -import CryptoJS from 'crypto-js' -import type { HttpRequestObject } from '../types.ts' - -export default { - _requestObj_tags: null as HttpRequestObject | null, - _requestObj_list: null as HttpRequestObject | null, - _requestObj_listRecommend: null as HttpRequestObject | null, - limit_list: 30, - limit_song: 10000, - successCode: 22000, - sortList: [ - { - name: '最热', - id: '1', - }, - { - name: '最新', - id: '0', - }, - ], - regExps: { - // http://music.taihe.com/songlist/566347741 - listDetailLink: /^.+\/songlist\/(\d+)(?:\?.*|&.*$|#.*$|$)/, - }, - aesPassEncod(jsonData) { - let timestamp = Math.floor(Date.now() / 1000) - let privateKey = toMD5('baidu_taihe_music_secret_key' + timestamp).substr(8, 16) - let key = CryptoJS.enc.Utf8.parse(privateKey) - let iv = CryptoJS.enc.Utf8.parse(privateKey) - let arrData: string[] = [] - let strData = '' - for (let key in jsonData) arrData.push(key) - arrData.sort() - for (let i = 0; i < arrData.length; i++) { - let key = arrData[i] - strData += - (i === 0 ? '' : '&') + key + '=' + encodeURIComponent(jsonData[key]) - } - let JsonFormatter = { - stringify(cipherParams) { - let jsonObj: Record = { - ct: cipherParams.ciphertext.toString(CryptoJS.enc.Base64), - } - if (cipherParams.iv) { - jsonObj.iv = cipherParams.iv.toString() - } - if (cipherParams.salt) { - jsonObj.s = cipherParams.salt.toString() - } - return jsonObj - }, - parse(jsonStr) { - let jsonObj = JSON.parse(jsonStr) - let cipherParams = CryptoJS.lib.CipherParams.create({ - ciphertext: CryptoJS.enc.Base64.parse(jsonObj.ct), - }) - if (jsonObj.iv) { - cipherParams.iv = CryptoJS.enc.Hex.parse(jsonObj.iv) - } - if (jsonObj.s) { - cipherParams.salt = CryptoJS.enc.Hex.parse(jsonObj.s) - } - return cipherParams - }, - } - let encrypted = CryptoJS.AES.encrypt(strData, key, { - iv, - blockSize: 16, - mode: CryptoJS.mode.CBC, - format: JsonFormatter, - }) - let ciphertext = encrypted.toString().ct - let sign = toMD5('baidu_taihe_music' + ciphertext + timestamp) - let jsonRet = { - timestamp, - param: ciphertext, - sign, - } - return jsonRet - }, - createUrl(param, method) { - let data = this.aesPassEncod(param) - return `http://musicmini.qianqian.com/v1/restserver/ting?method=${method}&time=${Date.now()}×tamp=${data.timestamp}¶m=${data.param}&sign=${data.sign}` - }, - getTagsUrl() { - return this.createUrl({ - from: 'qianqianmini', - type: 'diy', - version: '10.1.8', - }, 'baidu.ting.ugcdiy.getChannels') - }, - getListUrl(sortType, tagName, page) { - return this.createUrl({ - channelname: tagName || '全部', - from: 'qianqianmini', - offset: (page - 1) * this.limit_list, - order_type: sortType, - size: this.limit_list, - version: '10.1.8', - }, 'baidu.ting.ugcdiy.getChanneldiy') - }, - getListDetailUrl(list_id, page) { - return this.createUrl({ - list_id, - offset: (page - 1) * this.limit_song, - size: this.limit_song, - withcount: '1', - withsong: '1', - }, 'baidu.ting.ugcdiy.getBaseInfo') - }, - - // 获取标签 - getTags(tryNum = 0) { - if (this._requestObj_tags) this._requestObj_tags.cancelHttp() - if (tryNum > 2) return Promise.reject(new Error('try max num')) - this._requestObj_tags = httpFetch(this.getTagsUrl()) - return this._requestObj_tags!.promise.then(({ body }) => { - if (body.error_code !== this.successCode) return this.getTags(++tryNum) - return { - hotTag: this.filterInfoHotTag(body.result.hot), - tags: this.filterTagInfo(body.result.tags), - source: 'bd', - } - }) - }, - filterInfoHotTag(rawList) { - return rawList.map(item => ({ - name: item, - id: item, - source: 'bd', - })) - }, - filterTagInfo(rawList) { - return rawList.map(type => ({ - name: type.first, - list: type.second.map(item => ({ - parent_id: type.first, - parent_name: type.first, - id: item, - name: item, - source: 'bd', - })), - })) - }, - - // 获取列表数据 - getList(sortId, tagId, page, tryNum = 0) { - if (this._requestObj_list) this._requestObj_list.cancelHttp() - if (tryNum > 2) return Promise.reject(new Error('try max num')) - this._requestObj_list = httpFetch(this.getListUrl(sortId, tagId, page)) - return this._requestObj_list!.promise.then(({ body }) => { - if (body.error_code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) - return { - list: this.filterList(body.diyInfo), - total: body.nums, - page, - limit: this.limit_list, - source: 'bd', - } - }) - }, - - - /** - * 格式化播放数量 - * @param {*} num - */ - formatPlayCount(num) { - if (num > 100000000) return parseInt((num / 10000000) as any) / 10 + '亿' - if (num > 10000) return parseInt((num / 1000) as any) / 10 + '万' - return num - }, - filterList(rawData) { - return rawData.map(item => ({ - play_count: this.formatPlayCount(item.listen_num), - id: String(item.list_id), - author: item.username, - name: item.title, - // time: item.publish_time, - img: item.list_pic_large || item.list_pic, - grade: item.grade, - desc: item.desc || item.tag, - source: 'bd', - })) - }, - - // 获取歌曲列表内的音乐 - getListDetail(id, page, tryNum = 0) { - if (tryNum > 2) return Promise.reject(new Error('try max num')) - - if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') - - const requestObj_listDetail = httpFetch(this.getListDetailUrl(id, page)) - return requestObj_listDetail.promise.then(({ body }) => { - if (body.error_code !== this.successCode) return this.getListDetail(id, page, ++tryNum) - let listData = this.filterData(body.result.songlist) - return { - list: listData, - page, - limit: this.limit_song, - total: body.result.song_num, - source: 'bd', - info: { - name: body.result.info.list_title, - img: body.result.info.list_pic, - desc: body.result.info.list_desc, - author: body.result.info.userinfo.username, - play_count: this.formatPlayCount(body.result.listen_num), - }, - } - }) - }, - filterData(rawList) { - // console.log(rawList) - return rawList.map(item => { - const types: any[] = [] - const _types: Record = {} - let size = null - let itemTypes = item.all_rate.split(',') - if (itemTypes.includes('128')) { - types.push({ type: '128k', size }) - _types['128k'] = { - size, - } - } - if (itemTypes.includes('320')) { - types.push({ type: '320k', size }) - _types['320k'] = { - size, - } - } - if (itemTypes.includes('flac')) { - types.push({ type: 'flac', size }) - _types.flac = { - size, - } - } - // types.reverse() - - return { - singer: item.author.replace(',', '、'), - name: item.title, - albumName: item.album_title, - albumId: item.album_id, - source: 'bd', - interval: formatPlayTime(parseInt(item.file_duration)), - songmid: item.song_id, - img: item.pic_s500, - lrc: null, - types, - _types, - typeUrl: {}, - } - }) - }, - -} - -// getList -// getTags -// getListDetail diff --git a/musicSdk/kg/api-test.js b/musicSdk/kg/api-test.js deleted file mode 100644 index 53b79e9..0000000 --- a/musicSdk/kg/api-test.js +++ /dev/null @@ -1,50 +0,0 @@ -import { httpFetch } from '../request.js' -import { requestMsg } from '../message.js' -import { headers, timeout } from '../options.js' -import { dnsLookup } from '../utils.js' - -const api_test = { - getMusicUrl(songInfo, type) { - const requestObj = httpFetch(`http://ts.tempmusics.tk/url/kg/${songInfo._types[type].hash}/${type}`, { - method: 'get', - timeout, - headers, - lookup: dnsLookup, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { - if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) - switch (body.code) { - case 0: return Promise.resolve({ type, url: body.data }) - default: return Promise.reject(new Error(requestMsg.fail)) - } - }) - return requestObj - }, - getPic(songInfo) { - const requestObj = httpFetch(`http://ts.tempmusics.tk/pic/kg/${songInfo.hash}`, { - method: 'get', - timeout, - headers, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ body }) => { - return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) - }) - return requestObj - }, - getLyric(songInfo) { - const requestObj = httpFetch(`http://ts.tempmusics.tk/lrc/kg/${songInfo.hash}`, { - method: 'get', - timeout, - headers, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ body }) => { - return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) - }) - return requestObj - }, -} - -export default api_test diff --git a/musicSdk/kg/vendors/infSign.min.js b/musicSdk/kg/vendors/infSign.min.js deleted file mode 100644 index c1a9d43..0000000 --- a/musicSdk/kg/vendors/infSign.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self,t.infSign=n())}(this,function(){"use strict";function t(t,n,r){return n in t?Object.defineProperty(t,n,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[n]=r,t}function n(t,n){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var e=Object.getOwnPropertySymbols(t);n&&(e=e.filter(function(n){return Object.getOwnPropertyDescriptor(t,n).enumerable})),r.push.apply(r,e)}return r}function r(r){for(var e=1;e0&&void 0!==arguments[0]?arguments[0]:{},e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=!1,c=!1,a="json",l=r({},n),u=s.isInClient();"function"==typeof o?t=o:(t=o.callback,i=o.useH5||!1,a=o.postType||"json",c=o.isCDN||!1),e&&("[object Object]"!=Object.prototype.toString.call(e)?u=!1:"urlencoded"==a&&(u=!1));var f=function(){var n=(new Date).getTime(),i=[],s=[],u="NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt",f={srcappid:"2919",clientver:"20000",clienttime:n,mid:n,uuid:n,dfid:"-"};c&&(delete f.clienttime,delete f.mid,delete f.uuid,delete f.dfid),l=r({},f,{},l);for(var g in l)i.push(g);if(i.sort(),i.forEach(function(t){s.push(t+"="+l[t])}),e)if("[object Object]"==Object.prototype.toString.call(e))if("json"==a)s.push(JSON.stringify(e));else{var b=[];for(var g in e)b.push(g+"="+e[g]);s.push(b.join("&"))}else s.push(e);s.unshift(u),s.push(u),l.signature=d(s.join("")),o.log&&(console.log("H5签名前参数",s),console.log("H5签名后返回",l)),e?t&&t(l,"[object Object]"==Object.prototype.toString.call(e)&&"json"==a?JSON.stringify(e):e):t&&t(l)};if(u&&!i){var g=!1;s.mobileCall(764,{get:l,post:e},function(n){return!g&&(g=!0,n&&n.status?(delete n.status,o.log&&(console.log("客户端签名前参数",{get:l,post:e}),console.log("客户端签名后返回",r({},l,{},n))),l=r({},l,{},n),e?t&&t(l,"[object Object]"==Object.prototype.toString.call(e)&&"json"==a?JSON.stringify(e):e):t&&t(l),!1):(u=!1,void f()))})}else u=!1,f()}"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;var s=e(function(t,n){!function(n,r){t.exports=function(){var t={str2Json:function(t){var n={};if("[object String]"===Object.prototype.toString.call(t))try{n=JSON.parse(t)}catch(t){n={}}return n},json2Str:function(t){var n=t;if("string"!=typeof t)try{n=JSON.stringify(t)}catch(t){n=""}return n},_extend:function(t,n){if(n)for(var r in t)n.hasOwnProperty(r)||(n[r]=t[r]);return n},formatURL:{browser:"",url:""},formatSong:{filename:"",filesize:"",hash:"",bitrate:"",extname:"",duration:"",mvhash:"",m4afilesize:"","320hash":"","320filesize":"",sqhash:"",sqfilesize:0,feetype:0,isfirst:0},formatMV:{filename:"",singername:"",hash:"",imgurl:""},formatShare:{shareName:"",topicName:"",hash:"",listID:"",type:"",suid:"",slid:"",imgUrl:"",filename:"",duration:"",shareData:{linkUrl:"",picUrl:"",content:"",title:""}},cbNum:0,isIOS:!!navigator.userAgent.match(/KGBrowser/gi),isKugouAndroid:!!navigator.userAgent.match(/kugouandroid/gi),isAndroid:"undefined"!=typeof external&&void 0!==external.superCall,loadUrl:function(t){var n=document.createElement("iframe");n.setAttribute("src",t),n.setAttribute("style","display:none;"),n.setAttribute("height","0px"),n.setAttribute("width","0px"),n.setAttribute("frameborder","0"),document.body.appendChild(n),n.parentNode.removeChild(n),n=null},callCmd:function(n){var r=t;if(r.isKugouAndroid){var e={},o="";if(n.cmd&&(e.cmd=n.cmd),n.jsonStr&&(e.jsonStr=n.jsonStr),n.callback&&(o="kgandroidmobilecall"+ ++r.cbNum+Math.random().toString().substr(2,9),e.callback=o,window[o]=function(t,e){void 0!==t&&("[object String]"===Object.prototype.toString.call(t)?(t="#"===e?decodeURIComponent(t):decodeURIComponent(decodeURIComponent(t)),n.callback(r.str2Json(t))):n.callback(t))}),n.AndroidCallback){var i=r.str2Json(n.jsonStr);i.AndroidCallback=o,n.jsonStr=r.json2Str(i),n.jsonStr&&(e.jsonStr=n.jsonStr)}var c=encodeURIComponent(JSON.stringify(e)),s="kugoujsbridge://start.kugou_jsbridge/?".concat(c);r.loadUrl(s)}else if(r.isAndroid){var a="",l="";if(n.jsonStr){if(n.callback&&""!==n.callback&&!0===n.AndroidCallback){l="kgmobilecall"+ ++r.cbNum+Math.random().toString().substr(2,9),window[l]=function(t,e){void 0!==t&&("[object String]"===Object.prototype.toString.call(t)?(t="#"===e?decodeURIComponent(t):decodeURIComponent(decodeURIComponent(t)),n.callback(r.str2Json(t))):n.callback(t))};var u=r.str2Json(n.jsonStr);u.AndroidCallback=l,n.jsonStr=r.json2Str(u)}try{a=external.superCall(n.cmd,n.jsonStr)}catch(t){}}else try{a=external.superCall(n.cmd)}catch(t){}n.callback&&""!==n.callback&&"AndroidCallback"!=a&&(a=r.str2Json(a),n.callback(a))}else{var f="",d="";n.callback&&(d="kgmobilecall"+ ++r.cbNum+Math.random().toString().substr(2,9),window[d]=function(t){void 0!==t&&n.callback&&("[object String]"===Object.prototype.toString.call(t)?n.callback(r.str2Json(t)):n.callback(t))}),d&&""!=d&&n.jsonStr&&(f='kugouurl://start.music/?{"cmd":'+n.cmd+', "jsonStr":'+n.jsonStr+', "callback":"'+d+'"}'),d&&""!=d&&!n.jsonStr&&(f='kugouurl://start.music/?{"cmd":'+n.cmd+', "callback":"'+d+'"}'),""==d&&n.jsonStr&&(f='kugouurl://start.music/?{"cmd":'+n.cmd+', "jsonStr":'+n.jsonStr+"}"),""!=d||n.jsonStr||(f='kugouurl://start.music/?{"cmd":'+n.cmd+"}"),r.loadUrl(f)}},formartData:function(n,r){n&&123==n&&r&&(r=t._extend(t.formatURL,r)),n&&123==n&&r&&(r=t._extend(t.formatURL,r))}};return{isIOS:t.isIOS,isKugouAndroid:t.isKugouAndroid,isAndroid:t.isAndroid,isInClient:function(){return!(!t.isAndroid&&!t.isKugouAndroid&&!t.isIOS)},mobileCall:function(n,r,e){var o="";if(r&&(o=t.json2Str(r)),!n)return console.error("请输入命令号!"),!1;var i={};n&&(i.cmd=n),""!=o&&(i.jsonStr=o),e&&(i.callback=e),n&&186==n&&e&&(i.AndroidCallback=!0),t.callCmd(i)},KgWebMobileCall:function(t,n){if(t)try{var r=t.split(".");r.reduce(function(e,o){if(e[o]){if(o===r[r.length-1]){var i=e[o];return"function"==typeof i?(e[o]=function(t){i&&i(t),n&&n(t)},e[o]):(console.error("请检查,当前环境变量已注册了对象:"+t+",且该对象不是方法"),null)}return e[o]}return o===r[r.length-1]?e[o]=function(t){n&&n(t)}:e[o]=new Object,e[o]},window)}catch(t){}}}}()}()}),a=e(function(t){!function(){var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r={rotl:function(t,n){return t<>>32-n},rotr:function(t,n){return t<<32-n|t>>>n},endian:function(t){if(t.constructor==Number)return 16711935&r.rotl(t,8)|4278255360&r.rotl(t,24);for(var n=0;n0;t--)n.push(Math.floor(256*Math.random()));return n},bytesToWords:function(t){for(var n=[],r=0,e=0;r>>5]|=t[r]<<24-e%32;return n},wordsToBytes:function(t){for(var n=[],r=0;r<32*t.length;r+=8)n.push(t[r>>>5]>>>24-r%32&255);return n},bytesToHex:function(t){for(var n=[],r=0;r>>4).toString(16)),n.push((15&t[r]).toString(16));return n.join("")},hexToBytes:function(t){for(var n=[],r=0;r>>6*(3-i)&63)):r.push("=");return r.join("")},base64ToBytes:function(t){t=t.replace(/[^A-Z0-9+\/]/gi,"");for(var r=[],e=0,o=0;e>>6-2*o);return r}};t.exports=r}()}),l={utf8:{stringToBytes:function(t){return l.bin.stringToBytes(unescape(encodeURIComponent(t)))},bytesToString:function(t){return decodeURIComponent(escape(l.bin.bytesToString(t)))}},bin:{stringToBytes:function(t){for(var n=[],r=0;r>>24)|4278255360&(s[g]<<24|s[g]>>>8);s[a>>>5]|=128<>>9<<4)]=a;for(var b=i._ff,p=i._gg,h=i._hh,m=i._ii,g=0;g>>0,u=u+j>>>0,f=f+S>>>0,d=d+v>>>0}return n.endian([l,u,f,d])};i._ff=function(t,n,r,e,o,i,c){var s=t+(n&r|~n&e)+(o>>>0)+c;return(s<>>32-i)+n},i._gg=function(t,n,r,e,o,i,c){var s=t+(n&e|r&~e)+(o>>>0)+c;return(s<>>32-i)+n},i._hh=function(t,n,r,e,o,i,c){var s=t+(n^r^e)+(o>>>0)+c;return(s<>>32-i)+n},i._ii=function(t,n,r,e,o,i,c){var s=t+(r^(n|~e))+(o>>>0)+c;return(s<>>32-i)+n},i._blocksize=16,i._digestsize=16,t.exports=function(t,r){if(void 0===t||null===t)throw new Error("Illegal argument "+t);var e=n.wordsToBytes(i(t,r));return r&&r.asBytes?e:r&&r.asString?o.bytesToString(e):n.bytesToHex(e)}}()});return c}); - diff --git a/musicSdk/kw/api-temp.js b/musicSdk/kw/api-temp.js deleted file mode 100644 index 9e53f47..0000000 --- a/musicSdk/kw/api-temp.js +++ /dev/null @@ -1,26 +0,0 @@ -import { httpFetch } from '../request.js' -import { requestMsg } from '../message.js' -import { headers, timeout } from '../options.js' -import { dnsLookup } from '../utils.js' - -const api_temp = { - getMusicUrl(songInfo, type) { - const requestObj = httpFetch(`http://tm.tempmusics.tk/url/kw/${songInfo.songmid}/${type}`, { - method: 'get', - headers, - timeout, - lookup: dnsLookup, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { - if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) - switch (body.code) { - case 0: return Promise.resolve({ type, url: body.data }) - default: return Promise.reject(new Error(body.msg)) - } - }) - return requestObj - }, -} - -export default api_temp diff --git a/musicSdk/kw/api-test.js b/musicSdk/kw/api-test.js deleted file mode 100644 index fca67d9..0000000 --- a/musicSdk/kw/api-test.js +++ /dev/null @@ -1,37 +0,0 @@ -import { httpFetch } from '../request.js' -import { requestMsg } from '../message.js' -import { headers, timeout } from '../options.js' -import { dnsLookup } from '../utils.js' - -const api_test = { - // getMusicUrl(songInfo, type) { - // const requestObj = httpFetch(`http://45.32.53.128:3002/m/kw/u/${songInfo.songmid}/${type}`, { - // method: 'get', - // headers, - // timeout, - // }) - // requestObj.promise = requestObj.promise.then(({ body }) => { - // return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(body.msg)) - // }) - // return requestObj - // }, - getMusicUrl(songInfo, type) { - const requestObj = httpFetch(`http://ts.tempmusics.tk/url/kw/${songInfo.songmid}/${type}`, { - method: 'get', - timeout, - headers, - lookup: dnsLookup, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { - if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) - switch (body.code) { - case 0: return Promise.resolve({ type, url: body.data }) - default: return Promise.reject(new Error(requestMsg.fail)) - } - }) - return requestObj - }, -} - -export default api_test diff --git a/musicSdk/message.ts b/musicSdk/message.ts deleted file mode 100644 index d7cce9d..0000000 --- a/musicSdk/message.ts +++ /dev/null @@ -1,4 +0,0 @@ -// SDK 根消息模块(转接至 src/common/message.js) -// musicSdk 内 `import { requestMsg } from '../message.js'` 的解析目标 - -export { requestMsg } from '../src/common/message.js' diff --git a/musicSdk/mg/album.js b/musicSdk/mg/album.js deleted file mode 100644 index c192972..0000000 --- a/musicSdk/mg/album.js +++ /dev/null @@ -1,49 +0,0 @@ -import { createHttpFetch } from './utils/index.js' -import { filterMusicInfoList } from './musicInfo.js' -import { formatPlayCount } from '../index.js' - -export default { - /** - * 通过AlbumId获取专辑 - * @param {*} id - * @param {*} page - */ - async getAlbumDetail(id, page = 1) { - const list = await createHttpFetch(`http://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/queryAlbumSong?albumId=${id}&pageNo=${page}`) - if (!list.songList) return Promise.reject(new Error('Get album list error.')) - - const songList = filterMusicInfoList(list.songList) - const listInfo = await this.getAlbumInfo(id) - - return { - list: songList || [], - page, - limit: listInfo.total, - total: listInfo.total, - source: 'mg', - info: { - name: listInfo.name, - img: listInfo.image, - desc: listInfo.desc, - author: listInfo.author, - play_count: listInfo.play_count, - }, - } - }, /** - * 通过AlbumId获取专辑信息 - * @param {*} id - */ - async getAlbumInfo(id) { - const info = await createHttpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/resource/album/v2.0?albumId=${id}`) - if (!info) return Promise.reject(new Error('Get album info error.')) - - return { - name: info.title ?? null, - image: info.imgItems?.length ? info.imgItems[0].img : null, - desc: info.summary ?? null, - author: info.singer ?? null, - play_count: formatPlayCount(info.opNumItem?.playNum ?? 0), - total: parseInt(info.totalCount) || null, - } - }, -} diff --git a/musicSdk/mg/api-test.js b/musicSdk/mg/api-test.js deleted file mode 100644 index 4b81328..0000000 --- a/musicSdk/mg/api-test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { httpFetch } from '../request.js' -import { requestMsg } from '../message.js' -import { headers, timeout } from '../options.js' -import { dnsLookup } from '../utils.js' - -const api_test = { - getMusicUrl(songInfo, type) { - const requestObj = httpFetch(`http://ts.tempmusics.tk/url/mg/${songInfo.copyrightId}/${type}`, { - method: 'get', - timeout, - headers, - lookup: dnsLookup, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { - if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) - switch (body.code) { - case 0: return Promise.resolve({ type, url: body.data }) - default: return Promise.reject(new Error(requestMsg.fail)) - } - }) - return requestObj - }, -} - -export default api_test diff --git a/musicSdk/mg/comment.ts b/musicSdk/mg/comment.ts deleted file mode 100644 index b790f7a..0000000 --- a/musicSdk/mg/comment.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { httpFetch } from '../request.ts' -import getSongId from './songId.ts' -import { dateFormat2 } from '../index.ts' -import type { HttpRequestObject } from '../types.ts' - -export default { - _requestObj: null as HttpRequestObject | null, - _requestObj2: null as HttpRequestObject | null, - _requestObj3: null as HttpRequestObject | null, - lastCommentIds: new Map(), - async getComment(musicInfo, page = 1, limit = 20) { - if (this._requestObj) this._requestObj.cancelHttp() - if (!musicInfo.songId) { - let id = await getSongId(musicInfo) - if (!id) throw new Error('获取评论失败') - musicInfo.songId = id - } - if (page === 1) this.lastCommentIds.clear() - const lastCommentId = this.lastCommentIds.get(String(page)) || '' - if (!lastCommentId && page > 1) throw new Error('获取评论失败') - // const _requestObj = httpFetch(`https://music.migu.cn/v3/api/comment/listComments?targetId=${musicInfo.songId}&pageSize=${limit}&pageNo=${page}`, { - const _requestObj = httpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/user/comment/stack/v1.0?pageSize=${limit}&queryType=1&resourceId=${musicInfo.songId}&resourceType=2&commentId=${lastCommentId}`, { - headers: { - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', - // Referer: 'https://music.migu.cn', - }, - }) - const { body, statusCode } = await _requestObj.promise - // console.log(body) - if (statusCode != 200 || body.code !== '000000') throw new Error('获取评论失败') - const total = parseInt(body.data.commentNums) - const list = this.filterComment(body.data.comments) - this.lastCommentIds.set(String(page + 1), list.length ? list[list.length - 1].id : '') - return { source: 'mg', comments: list, total, page, limit, maxPage: Math.ceil(total / limit) || 1 } - }, - async getHotComment(musicInfo, page = 1, limit = 20) { - if (this._requestObj2) this._requestObj2.cancelHttp() - - if (!musicInfo.songId) { - let id = await getSongId(musicInfo) - if (!id) throw new Error('获取评论失败') - musicInfo.songId = id - } - - // const _requestObj2 = httpFetch(`https://music.migu.cn/v3/api/comment/listTopComments?targetId=${musicInfo.songId}&pageSize=${limit}&pageNo=${page}`, { - const _requestObj2 = httpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/user/comment/stack/v1.0?pageSize=${limit}&queryType=2&resourceId=${musicInfo.songId}&resourceType=2&hotCommentStart=${(page - 1) * limit}`, { - headers: { - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', - // Referer: 'https://music.migu.cn', - }, - }) - const { body, statusCode } = await _requestObj2.promise - // console.log(body) - if (statusCode != 200 || body.code !== '000000') throw new Error('获取热门评论失败') - const total = parseInt(body.data.cfgHotCount) - return { source: 'mg', comments: this.filterComment(body.data.hotComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 } - }, - async getReplyComment(musicInfo, replyId, page = 1, limit = 10) { - if (this._requestObj2) this._requestObj2.cancelHttp() - - // const _requestObj2 = httpFetch(`https://music.migu.cn/v3/api/comment/listCommentsById?commentId=${replyId}&pageSize=${limit}&pageNo=${page}`, { - const _requestObj2 = httpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/user/comment/stack/${replyId}/v1.0?pageSize=${limit}&queryType=2&resourceId=${musicInfo.songId}&resourceType=2&start=${(page - 1) * limit}`, { - headers: { - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', - }, - }) - const { body, statusCode } = await _requestObj2.promise - // console.log(body) - if (statusCode != 200 || body.code !== '000000') throw new Error('获取回复评论失败') - const total = parseInt(body.data.replyTotalCount) - return { source: 'mg', comments: this.filterComment(body.data.mainCommentItem.replyComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 } - }, - filterComment(rawList) { - return rawList.map(item => ({ - id: item.commentId, - text: item.commentInfo, - time: item.commentTime, - timeStr: dateFormat2(new Date(item.commentTime).getTime()), - userName: item.user.nickName, - avatar: item.user.middleIcon || item.user.bigIcon || item.user.smallIcon, - userId: item.user.userId, - likedCount: item.opNumItem.thumbNum, - replyNum: item.replyTotalCount, - reply: item.replyComments.map(c => ({ - id: c.replyId, - text: c.replyInfo, - time: c.replyTime, - timeStr: dateFormat2(new Date(c.replyTime).getTime()), - userName: c.user.nickName, - avatar: c.user.middleIcon || c.user.bigIcon || c.user.smallIcon, - userId: c.user.userId, - likedCount: null, - replyNum: null, - })), - })) - }, -} diff --git a/musicSdk/mg/hotSearch.ts b/musicSdk/mg/hotSearch.ts deleted file mode 100644 index 8b38abd..0000000 --- a/musicSdk/mg/hotSearch.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { httpFetch } from '../request.ts' -import type { HttpRequestObject } from '../types.ts' - -export default { - _requestObj: null as HttpRequestObject | null, - async getList(retryNum = 0) { - if (this._requestObj) this._requestObj.cancelHttp() - if (retryNum > 2) return Promise.reject(new Error('try max num')) - - const _requestObj = httpFetch('http://jadeite.migu.cn:7090/music_search/v3/search/hotword') - const { body, statusCode } = await _requestObj.promise - if (statusCode != 200 || body.code !== '000000') throw new Error('获取热搜词失败') - // console.log(body, statusCode) - return { source: 'mg', list: this.filterList(body.data.hotwords[0].hotwordList) } - }, - filterList(rawList) { - return rawList.filter(item => item.resourceType == 'song').map(item => item.word) - }, -} diff --git a/musicSdk/mg/index.ts b/musicSdk/mg/index.ts deleted file mode 100644 index 4cdf5b2..0000000 --- a/musicSdk/mg/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { apis } from '../api-source.ts' -import leaderboard from './leaderboard.ts' -import songList from './songList.ts' -import musicSearch from './musicSearch.ts' -import pic from './pic.ts' -import lyric from './lyric.ts' -import hotSearch from './hotSearch.ts' -import comment from './comment.ts' -import album from './album.js' -// import tipSearch from './tipSearch.ts' - -const mg = { - // tipSearch, - songList, - musicSearch, - leaderboard, - hotSearch, - comment, - album, - getMusicUrl(songInfo, type) { - return apis('mg').getMusicUrl!(songInfo, type) - }, - getLyric(songInfo) { - return lyric.getLyric(songInfo) - }, - getPic(songInfo) { - return pic.getPic(songInfo) - }, - getMusicDetailPageUrl(songInfo) { - return `http://music.migu.cn/v3/music/song/${songInfo.copyrightId}` - }, -} - -export default mg diff --git a/musicSdk/mg/leaderboard.ts b/musicSdk/mg/leaderboard.ts deleted file mode 100644 index 18f01df..0000000 --- a/musicSdk/mg/leaderboard.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { httpFetch } from '../request.ts' -import { filterMusicInfoList } from './musicInfo.ts' -import type { Source, HttpRequestObject } from '../types.ts' - -// const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }] - -// const boardList = [ -// { id: 'mg__27553319', name: '尖叫新歌榜', bangid: '27553319', webId: 'jianjiao_newsong' }, -// { id: 'mg__27186466', name: '尖叫热歌榜', bangid: '27186466', webId: 'jianjiao_hotsong' }, -// { id: 'mg__27553408', name: '尖叫原创榜', bangid: '27553408', webId: 'jianjiao_original' }, -// { id: 'mg__23189800', name: '港台榜', bangid: '23189800', webId: 'hktw' }, -// { id: 'mg__23189399', name: '内地榜', bangid: '23189399', webId: 'mainland' }, -// { id: 'mg__19190036', name: '欧美榜', bangid: '19190036', webId: 'eur_usa' }, -// { id: 'mg__23189813', name: '日韩榜', bangid: '23189813', webId: 'jpn_kor' }, -// { id: 'mg__23190126', name: '彩铃榜', bangid: '23190126', webId: 'coloring' }, -// { id: 'mg__15140045', name: 'KTV榜', bangid: '15140045', webId: 'ktv' }, -// { id: 'mg__15140034', name: '网络榜', bangid: '15140034', webId: 'network' }, -// // { id: 'mg__21958042', name: '美国iTunes榜', bangid: '21958042', webId: 'itunes' }, -// // { id: 'mg__21975570', name: '美国billboard榜', bangid: '21975570', webId: 'billboard' }, -// // { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815', webId: 'hito' }, -// // { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943', webId: 'mnet' }, -// // { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437', webId: 'uk' }, -// ] -interface BoardItem { - id: string - name: string - bangid: string - source: Source - // 旧数据字段,部分条目缺失(缺失时 getDetailPageUrl 拼出 undefined,与原实现一致) - webId?: string -} -const boardList: BoardItem[] = [ - { - id: 'mg__27553319', - name: '新歌榜', - bangid: '27553319', - source: 'mg', - }, - { - id: 'mg__27186466', - name: '热歌榜', - bangid: '27186466', - source: 'mg', - }, - { - id: 'mg__27553408', - name: '原创榜', - bangid: '27553408', - source: 'mg', - }, - { - id: 'mg__75959118', - name: '音乐风向榜', - bangid: '75959118', - source: 'mg', - }, - { - id: 'mg__76557036', - name: '彩铃分贝榜', - bangid: '76557036', - source: 'mg', - }, - { - id: 'mg__76557745', - name: '会员臻爱榜', - bangid: '76557745', - source: 'mg', - }, - { - id: 'mg__23189800', - name: '港台榜', - bangid: '23189800', - source: 'mg', - }, - { - id: 'mg__23189399', - name: '内地榜', - bangid: '23189399', - source: 'mg', - }, - { - id: 'mg__19190036', - name: '欧美榜', - bangid: '19190036', - source: 'mg', - }, - { - id: 'mg__83176390', - name: '国风金曲榜', - bangid: '83176390', - source: 'mg', - }, -] -export default { - limit: 200, - list: [ - { - id: 'mgyyb', - name: '音乐榜', - bangid: '27553319', - }, - { - id: 'mgysb', - name: '影视榜', - bangid: '23603721', - }, - { - id: 'mghybnd', - name: '华语内地榜', - bangid: '23603926', - }, - { - id: 'mghyjqbgt', - name: '华语港台榜', - bangid: '23603954', - }, - { - id: 'mgomb', - name: '欧美榜', - bangid: '23603974', - }, - { - id: 'mgrhb', - name: '日韩榜', - bangid: '23603982', - }, - { - id: 'mgwlb', - name: '网络榜', - bangid: '23604058', - }, - { - id: 'mgclb', - name: '彩铃榜', - bangid: '23604023', - }, - { - id: 'mgktvb', - name: 'KTV榜', - bangid: '23604040', - }, - { - id: 'mgrcb', - name: '原创榜', - bangid: '23604032', - }, - ], - getUrl(id, page) { - return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/querycontentbyId.do?columnId=${id}&needAll=0` - // return `http://m.music.migu.cn/migu/remoting/cms_list_tag?nid=${id}&pageSize=${this.limit}&pageNo=${page - 1}` - }, - successCode: '000000', - requestBoardsObj: null as HttpRequestObject | null, - getBoardsData() { - if (this.requestBoardsObj) (this as any)._requestBoardsObj.cancelHttp() - this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/pc/bmw/rank/rank-index/v1.0', { - // this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/rank-list/release', { - // this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/indexrank.do?templateVersion=8', { - headers: { - Referer: 'https://app.c.nf.migu.cn/', - 'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36', - channel: '0146921', - }, - }) - return this.requestBoardsObj!.promise - }, - getData(url) { - const requestObj = httpFetch(url) - return requestObj.promise - }, - // filterBoardsData(listData, list = [], ids = new Set()) { - // for (const item of listData) { - // if (item.rankId && !ids.has(item.rankId)) { - // ids.add(item.rankId) - // list.push({ - // id: 'mg__' + item.rankId, - // name: item.rankName, - // bangid: String(item.rankId), - // source: 'mg', - // }) - // } else if (item.contents) this.filterBoardsData(item.contents, list, ids) - // } - // return list - // }, - // filterBoardsData(rawList) { - // // console.log(rawList) - // let list = [] - // for (const board of rawList) { - // if (board.template != 'group1') continue - // for (const item of board.itemList) { - // if ((item.template != 'row1' && item.template != 'grid1' && !item.actionUrl) || !item.actionUrl.includes('rank-info')) continue - - // let data = item.displayLogId.param - // list.push({ - // id: 'mg__' + data.rankId, - // name: data.rankName, - // bangid: String(data.rankId), - // }) - // } - // } - // return list - // }, - async getBoards(retryNum = 0) { - // if (++retryNum > 3) return Promise.reject(new Error('try max num')) - // let response - // try { - // response = await this.getBoardsData() - // } catch (error) { - // return this.getBoards(retryNum) - // } - // // console.log(response.body.data.contentItemList) - // if (response.statusCode !== 200 || response.body.code !== this.successCode) return this.getBoards(retryNum) - // const list = this.filterBoardsData(response.body.data.contents) - // console.log(list) - // // console.log(JSON.stringify(list)) - // this.list = list - // return { - // list, - // source: 'mg', - // } - this.list = boardList - return { - list: boardList, - source: 'mg', - } - }, - getList(bangid, page, retryNum = 0) { - if (++retryNum > 3) return Promise.reject(new Error('try max num')) - return this.getData(this.getUrl(bangid, page)).then(({ statusCode, body }) => { - // console.log(body) - if (statusCode !== 200 || body.code !== this.successCode) return this.getList(bangid, page, retryNum) - const list = filterMusicInfoList(body.columnInfo.contents.map(m => m.objectInfo)) - return { - total: list.length, - list, - limit: this.limit, - page, - source: 'mg', - } - }) - }, - - getDetailPageUrl(id) { - if (typeof id == 'string') id = id.replace('mg__', '') - for (const item of boardList) { - if (item.bangid == id) { - return `https://music.migu.cn/v3/music/top/${item.webId}` - } - } - return null - }, -} diff --git a/musicSdk/mg/lyric.ts b/musicSdk/mg/lyric.ts deleted file mode 100644 index 1d6c575..0000000 --- a/musicSdk/mg/lyric.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { httpFetch } from '../request.ts' -import { getMusicInfo } from './musicInfo.ts' -import { decrypt } from './utils/mrc.ts' - -const mrcTools = { - rxps: { - lineTime: /^\s*\[(\d+),\d+\]/, - wordTime: /\(\d+,\d+\)/, - wordTimeAll: /(\(\d+,\d+\))/g, - }, - parseLyric(str) { - str = str.replace(/\r/g, '') - const lines = str.split('\n') - const yrcLines: string[] = [] - const lrcLines: string[] = [] - - for (const line of lines) { - if (line.length < 6) continue - let result = this.rxps.lineTime.exec(line) - if (!result) continue - - const startTime = parseInt(result[1]) - let time: any = startTime - let ms = time % 1000 - time /= 1000 - let m = parseInt((time / 60) as unknown as string).toString().padStart(2, '0') - time %= 60 - let s = parseInt(time as unknown as string).toString().padStart(2, '0') - time = `${m}:${s}.${ms}` - - let words = line.replace(this.rxps.lineTime, '') - - lrcLines.push(`[${time}]${words.replace(this.rxps.wordTimeAll, '')}`) - - let times = words.match(this.rxps.wordTimeAll) - if (!times) continue - times = times.map(time => { - const result = /\((\d+),(\d+)\)/.exec(time) - return `<${parseInt(result![1]) - startTime},${result![2]}>` - }) - const wordArr = words.split(this.rxps.wordTime) - const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('') - yrcLines.push(`[${time}]${newWords}`) - } - return { - lyric: lrcLines.join('\n'), - yrc: yrcLines.join('\n'), - } - }, - getText(url, tryNum = 0) { - const requestObj = httpFetch(url, { - headers: { - Referer: 'https://app.c.nf.migu.cn/', - 'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36', - channel: '0146921', - }, - }) - return requestObj.promise.then(({ statusCode, body }) => { - if (statusCode == 200) return body - if (tryNum > 5 || statusCode == 404) return Promise.reject(new Error('歌词获取失败')) - return this.getText(url, ++tryNum) - }) - }, - getMrc(url) { - return this.getText(url).then(text => { - return this.parseLyric(decrypt(text)) - }) - }, - getLrc(url) { - return this.getText(url).then(text => ({ yrc: '', lyric: text })) - }, - getTrc(url) { - if (!url) return Promise.resolve('') - return this.getText(url) - }, - async getMusicInfo(songInfo) { - return songInfo.mrcUrl == null - ? getMusicInfo(songInfo.songmid || songInfo.copyrightId) - : songInfo - }, - getLyric(songInfo) { - return { - promise: this.getMusicInfo(songInfo).then(info => { - let p - if (info.mrcUrl) p = this.getMrc(info.mrcUrl) - else if (info.lrcUrl) p = this.getLrc(info.lrcUrl) - else if (info.lyricUrl) p = this.getLrc(info.lyricUrl) - if (p == null) return Promise.reject(new Error('获取歌词失败')) - return Promise.all([p, this.getTrc(info.trcUrl)]).then(([lrcInfo, tlyric]) => { - lrcInfo.tlyric = tlyric - return lrcInfo - }) - }), - cancelHttp() {}, - } - }, -} - -export default { - getLyric(songInfo) { - let requestObj = mrcTools.getLyric(songInfo) - return requestObj - }, -} diff --git a/musicSdk/mg/musicInfo.ts b/musicSdk/mg/musicInfo.ts deleted file mode 100644 index 7dfd05b..0000000 --- a/musicSdk/mg/musicInfo.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { sizeFormate, formatPlayTime } from '../index.ts' -import { createHttpFetch } from './utils/index.ts' -import { formatSingerName } from '../utils.ts' -import type { SongInfo, SongType, QualityInfo, Quality } from '../types.ts' - -const createGetMusicInfosTask = (ids: Array) => { - let list = ids - const tasks: Array> = [] - while (list.length) { - tasks.push(list.slice(0, 100)) - if (list.length < 100) break - list = list.slice(100) - } - let url = 'https://c.musicapp.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceType=2' - return Promise.all(tasks.map(task => createHttpFetch(url, { - method: 'POST', - form: { - resourceId: task.join('|'), - }, - }).then(data => data.resource))) -} - -export const filterMusicInfoList = (rawList: any[]): SongInfo[] => { - // console.log(rawList) - let ids = new Set() - const list: any[] = [] - rawList.forEach(item => { - if (!item.songId || ids.has(item.songId)) return - ids.add(item.songId) - const types: SongType[] = [] - const _types: Partial> = {} - item.newRateFormats?.forEach(type => { - let size - switch (type.formatType) { - case 'PQ': - size = sizeFormate(type.size ?? type.androidSize) - types.push({ type: '128k', size }) - _types['128k'] = { - size, - } - break - case 'HQ': - size = sizeFormate(type.size ?? type.androidSize) - types.push({ type: '320k', size }) - _types['320k'] = { - size, - } - break - case 'SQ': - size = sizeFormate(type.size ?? type.androidSize) - types.push({ type: 'flac', size }) - _types.flac = { - size, - } - break - case 'ZQ': - size = sizeFormate(type.size ?? type.androidSize) - types.push({ type: 'flac24bit', size }) - _types.flac24bit = { - size, - } - break - } - }) - - const intervalTest = /(\d\d:\d\d)$/.test(item.length) - - list.push({ - singer: formatSingerName(item.artists, 'name'), - name: item.songName, - albumName: item.album, - albumId: item.albumId, - songmid: item.songId, - copyrightId: item.copyrightId, - source: 'mg', - interval: intervalTest ? RegExp.$1 : null, - img: item.albumImgs?.length ? item.albumImgs[0].img : null, - lrc: null, - lrcUrl: item.lrcUrl, - mrcUrl: item.mrcUrl, - trcUrl: item.trcUrl, - otherSource: null, - types, - _types, - typeUrl: {}, - }) - }) - return list -} - -export const filterMusicInfoListV5 = (rawList) => { - // console.log(rawList) - let ids = new Set() - const list: any[] = [] - rawList.forEach(item => { - if (!item.songId || ids.has(item.songId)) return - ids.add(item.songId) - const types: SongType[] = [] - const _types: Partial> = {} - item.audioFormats?.forEach(type => { - let size - switch (type.formatType) { - case 'PQ': - size = sizeFormate(type.size ?? type.androidSize) - types.push({ type: '128k', size }) - _types['128k'] = { - size, - } - break - case 'HQ': - size = sizeFormate(type.size ?? type.androidSize) - types.push({ type: '320k', size }) - _types['320k'] = { - size, - } - break - case 'SQ': - size = sizeFormate(type.size ?? type.androidSize) - types.push({ type: 'flac', size }) - _types.flac = { - size, - } - break - case 'ZQ': - size = sizeFormate(type.size ?? type.androidSize) - types.push({ type: 'flac24bit', size }) - _types.flac24bit = { - size, - } - break - } - }) - - list.push({ - singer: formatSingerName(item.singerList, 'name'), - name: item.songName, - albumName: item.album, - albumId: item.albumId, - songmid: item.songId, - copyrightId: item.copyrightId, - source: 'mg', - interval: formatPlayTime(item.duration), - img: item.img3 || item.img2 || item.img1 || null, - lrc: null, - lrcUrl: item.lrcUrl, - mrcUrl: item.mrcUrl, - trcUrl: item.trcUrl, - otherSource: null, - types, - _types, - typeUrl: {}, - }) - }) - return list -} - -export const getMusicInfo = async(copyrightId?: string): Promise => { - return getMusicInfos([copyrightId]).then(data => data[0]) -} - -export const getMusicInfos = async(copyrightIds: Array): Promise => { - return filterMusicInfoList((await createGetMusicInfosTask(copyrightIds)).flat()) -} diff --git a/musicSdk/mg/musicSearch.ts b/musicSdk/mg/musicSearch.ts deleted file mode 100644 index 064e5e1..0000000 --- a/musicSdk/mg/musicSearch.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { httpFetch } from '../request.ts' -import { sizeFormate, formatPlayTime } from '../index.ts' -import { toMD5, formatSingerName } from '../utils.ts' -import type { SongType, QualityInfo, Quality } from '../types.ts' - -export const createSignature = (time: string, str: string): { sign: string, deviceId: string } => { - const deviceId = '963B7AA0D21511ED807EE5846EC87D20' - const signatureMd5 = '6cdc72a439cef99a3418d2a78aa28c73' - const sign = toMD5(`${str}${signatureMd5}yyapp2d16148780a1dcc7408e06336b98cfd50${deviceId}${time}`) - return { sign, deviceId } -} - -export default { - limit: 20, - total: 0, - page: 0, - allPage: 1, - - // 旧版API - // musicSearch(str, page, limit) { - // const searchRequest = httpFetch(`http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=${encodeURIComponent(str)}&pageNo=${page}&pageSize=${limit}&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22songlist%22%3A0%2C%22bestShow%22%3A1%7D`, { - // searchRequest = httpFetch(`http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=${encodeURIComponent(str)}&pageNo=${page}&pageSize=${limit}&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22songlist%22%3A0%2C%22bestShow%22%3A1%7D`, { - // searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, { - // searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`) - // // searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, { - // headers: { - // // sign: 'c3b7ae985e2206e97f1b2de8f88691e2', - // // timestamp: 1578225871982, - // // appId: 'yyapp2', - // // mode: 'android', - // // ua: 'Android_migu', - // // version: '6.9.4', - // osVersion: 'android 7.0', - // 'User-Agent': 'okhttp/3.9.1', - // }, - // }) - // // searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`) - // return searchRequest.promise.then(({ body }) => body) - // }, - // handleResult(rawData) { - // // console.log(rawData) - // let ids = new Set() - // const list = [] - // rawData.forEach(item => { - // if (ids.has(item.id)) return - // ids.add(item.id) - // const types = [] - // const _types = {} - // item.newRateFormats && item.newRateFormats.forEach(type => { - // let size - // switch (type.formatType) { - // case 'PQ': - // size = sizeFormate(type.size ?? type.androidSize) - // types.push({ type: '128k', size }) - // _types['128k'] = { - // size, - // } - // break - // case 'HQ': - // size = sizeFormate(type.size ?? type.androidSize) - // types.push({ type: '320k', size }) - // _types['320k'] = { - // size, - // } - // break - // case 'SQ': - // size = sizeFormate(type.size ?? type.androidSize) - // types.push({ type: 'flac', size }) - // _types.flac = { - // size, - // } - // break - // case 'ZQ': - // size = sizeFormate(type.size ?? type.androidSize) - // types.push({ type: 'flac24bit', size }) - // _types.flac24bit = { - // size, - // } - // break - // } - // }) - - // const albumNInfo = item.albums && item.albums.length - // ? { - // id: item.albums[0].id, - // name: item.albums[0].name, - // } - // : {} - - // list.push({ - // singer: this.getSinger(item.singers), - // name: item.name, - // albumName: albumNInfo.name, - // albumId: albumNInfo.id, - // songmid: item.songId, - // copyrightId: item.copyrightId, - // source: 'mg', - // interval: null, - // img: item.imgItems && item.imgItems.length ? item.imgItems[0].img : null, - // lrc: null, - // lrcUrl: item.lyricUrl, - // mrcUrl: item.mrcurl, - // trcUrl: item.trcUrl, - // otherSource: null, - // types, - // _types, - // typeUrl: {}, - // }) - // }) - // return list - // }, - - musicSearch(str, page, limit) { - const time = Date.now().toString() - const signData = createSignature(time, str) - const searchRequest = httpFetch(`https://jadeite.migu.cn/music_search/v3/search/searchAll?isCorrect=0&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0&sid=USS`, { - headers: { - uiVersion: 'A_music_3.6.1', - deviceId: signData.deviceId, - timestamp: time, - sign: signData.sign, - channel: '0146921', - 'User-Agent': 'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', - }, - }) - return searchRequest.promise.then(({ body }) => body) - }, - filterData(rawData) { - // console.log(rawData) - const list: any[] = [] - const ids = new Set() - - rawData.forEach(item => { - item.forEach(data => { - if (!data.songId || !data.copyrightId || ids.has(data.copyrightId)) return - ids.add(data.copyrightId) - - const types: SongType[] = [] - const _types: Partial> = {} - data.audioFormats && data.audioFormats.forEach(type => { - let size - switch (type.formatType) { - case 'PQ': - size = sizeFormate(type.asize ?? type.isize) - types.push({ type: '128k', size }) - _types['128k'] = { - size, - } - break - case 'HQ': - size = sizeFormate(type.asize ?? type.isize) - types.push({ type: '320k', size }) - _types['320k'] = { - size, - } - break - case 'SQ': - size = sizeFormate(type.asize ?? type.isize) - types.push({ type: 'flac', size }) - _types.flac = { - size, - } - break - case 'ZQ24': - size = sizeFormate(type.asize ?? type.isize) - types.push({ type: 'flac24bit', size }) - _types.flac24bit = { - size, - } - break - } - }) - - let img = data.img3 || data.img2 || data.img1 || null - if (img && !/https?:/.test(data.img3)) img = 'http://d.musicapp.migu.cn' + img - - list.push({ - singer: formatSingerName(data.singerList), - name: data.name, - albumName: data.album, - albumId: data.albumId, - songmid: data.songId, - copyrightId: data.copyrightId, - source: 'mg', - interval: formatPlayTime(data.duration), - img, - lrc: null, - lrcUrl: data.lrcUrl, - mrcUrl: data.mrcurl, - trcUrl: data.trcUrl, - types, - _types, - typeUrl: {}, - }) - }) - }) - return list - }, - search(str, page = 1, limit, retryNum = 0) { - if (++retryNum > 3) return Promise.reject(new Error('try max num')) - if (limit == null) limit = this.limit - // http://newlyric.kuwo.cn/newlyric.lrc?62355680 - return this.musicSearch(str, page, limit).then(result => { - // console.log(result) - if (!result || result.code !== '000000') return Promise.reject(new Error(result ? result.info : '搜索失败')) - const songResultData = result.songResultData || { resultList: [], totalCount: 0 } - - let list = this.filterData(songResultData.resultList) - if (list == null) return this.search(str, page, limit, retryNum) - - this.total = parseInt(songResultData.totalCount) - this.page = page - this.allPage = Math.ceil(this.total / limit) - - return { - list, - allPage: this.allPage, - limit, - total: this.total, - source: 'mg', - } - }) - }, -} diff --git a/musicSdk/mg/pic.ts b/musicSdk/mg/pic.ts deleted file mode 100644 index e28396c..0000000 --- a/musicSdk/mg/pic.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { httpFetch } from '../request.ts' -import getSongId from './songId.ts' - -export default { - async getPicUrl(songId, tryNum = 0) { - let requestObj = httpFetch(`http://music.migu.cn/v3/api/music/audioPlayer/getSongPic?songId=${songId}`, { - headers: { - Referer: 'http://music.migu.cn/v3/music/player/audio?from=migu', - }, - }) - requestObj.promise.then(({ body }) => { - if (body.returnCode !== '000000') { - if (tryNum > 5) return Promise.reject(new Error('图片获取失败')) - let tryRequestObj = (this as any).getPic(songId, ++tryNum) - requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) - return tryRequestObj.promise - } - let url = body.largePic || body.mediumPic || body.smallPic - if (!/https?:/.test(url)) url = 'http:' + url - return url - }) - return requestObj - }, - async getPic(songInfo) { - const songId = await getSongId(songInfo) - return this.getPicUrl(songId) - }, -} diff --git a/musicSdk/mg/songId.ts b/musicSdk/mg/songId.ts deleted file mode 100644 index 596fcac..0000000 --- a/musicSdk/mg/songId.ts +++ /dev/null @@ -1,32 +0,0 @@ -// import { httpFetch } from '../request.js' -import { getMusicInfo } from './musicInfo.ts' -import type { SongInfo } from '../types.ts' - -const getSongId = async(mInfo: SongInfo): Promise => { - if (mInfo.songmid != mInfo.copyrightId) return mInfo.songmid - const musicInfo = await getMusicInfo(mInfo.copyrightId) - return musicInfo!.songmid -} - - -// export const getSongId = async(musicInfo, retry = 0) => { -// if (musicInfo.songmid != musicInfo.copyrightId) return musicInfo.songmid -// if (++retry > 2) return Promise.reject(new Error('max retry')) - -// const requestObj = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/listen-url?netType=00&resourceType=2&songId=${musicInfo.copyrightId}&toneFlag=PQ`, { -// headers: { -// 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36', -// channel: '0146921', -// }, -// }) - -// return requestObj.promise.then(({ body }) => { -// console.log(body) -// if (!body || body.code !== '000000') return this.getSongId(musicInfo, retry) -// const id = body.data.songItem.songId -// if (!id) throw new Error('failed') -// return id -// }) -// } - -export default getSongId diff --git a/musicSdk/mg/songList.ts b/musicSdk/mg/songList.ts deleted file mode 100644 index 13f8ce2..0000000 --- a/musicSdk/mg/songList.ts +++ /dev/null @@ -1,363 +0,0 @@ -import { httpFetch } from '../request.ts' -import { formatPlayCount } from '../index.ts' -import { filterMusicInfoListV5 } from './musicInfo.ts' -import { createSignature } from './musicSearch.ts' -import { createHttpFetch } from './utils/index.ts' -import type { HttpRequestObject } from '../types.ts' - -// const tagData = { code: '000000', info: 'SUCCESS', columnInfo: { columnTitle: '分类', columnId: '15244430', columnPid: '15031270', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 6, columnStatus: 1, columnCreateTime: '2016-11-10 10:53:05.077', columntype: 2011, contents: [{ contentId: '18464615', relationType: 2011, objectInfo: { columnTitle: '热门', columnId: '18464615', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 8, columnStatus: 1, columnCreateTime: '2017-02-20 16:09:13.400', columntype: 2011, contents: [{ contentId: '1000001672', relationType: 4034, objectInfo: { tagId: '1000001672', tagName: '流行', resourceType: '2034' }, relationSort: 9 }, { contentId: '1003449727', relationType: 4034, objectInfo: { tagId: '1003449727', tagName: '厂牌', resourceType: '2034' }, relationSort: 8 }, { contentId: '1000001795', relationType: 4034, objectInfo: { tagId: '1000001795', tagName: '伤感', resourceType: '2034' }, relationSort: 7 }, { contentId: '1001076080', relationType: 4034, objectInfo: { tagId: '1001076080', tagName: '电影', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001675', relationType: 4034, objectInfo: { tagId: '1000001675', tagName: '中国风', resourceType: '2034' }, relationSort: 5 }, { contentId: '1000001635', relationType: 4034, objectInfo: { tagId: '1000001635', tagName: '经典老歌', resourceType: '2034' }, relationSort: 4 }, { contentId: '1000001831', relationType: 4034, objectInfo: { tagId: '1000001831', tagName: '翻唱', resourceType: '2034' }, relationSort: 3 }, { contentId: '1000001762', relationType: 4034, objectInfo: { tagId: '1000001762', tagName: '国语', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620410266029', customizedPicUrls: [] }, relationSort: 6 }, { contentId: '15244503', relationType: 2011, objectInfo: { columnTitle: '主题', columnId: '15244503', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 23, columnStatus: 1, columnCreateTime: '2016-11-10 10:54:10.261', columntype: 2011, contents: [{ contentId: '1003449727', relationType: 4034, objectInfo: { tagId: '1003449727', tagName: '厂牌', resourceType: '2034' }, relationSort: 29 }, { contentId: '1001076080', relationType: 4034, objectInfo: { tagId: '1001076080', tagName: '电影', resourceType: '2034' }, relationSort: 28 }, { contentId: '1001076078', relationType: 4034, objectInfo: { tagId: '1001076078', tagName: '电视剧', resourceType: '2034' }, relationSort: 27 }, { contentId: '1001076083', relationType: 4034, objectInfo: { tagId: '1001076083', tagName: '综艺', resourceType: '2034' }, relationSort: 26 }, { contentId: '1000001827', relationType: 4034, objectInfo: { tagId: '1000001827', tagName: 'KTV', resourceType: '2034' }, relationSort: 23 }, { contentId: '1000001698', relationType: 4034, objectInfo: { tagId: '1000001698', tagName: '爱情', resourceType: '2034' }, relationSort: 22 }, { contentId: '1000001635', relationType: 4034, objectInfo: { tagId: '1000001635', tagName: '经典老歌', resourceType: '2034' }, relationSort: 21 }, { contentId: '1001076096', relationType: 4034, objectInfo: { tagId: '1001076096', tagName: '网络热歌', resourceType: '2034' }, relationSort: 20 }, { contentId: '1000001780', relationType: 4034, objectInfo: { tagId: '1000001780', tagName: '儿童歌曲', resourceType: '2034' }, relationSort: 19 }, { contentId: '1000587702', relationType: 4034, objectInfo: { tagId: '1000587702', tagName: '广场舞', resourceType: '2034' }, relationSort: 18 }, { contentId: '1000587717', relationType: 4034, objectInfo: { tagId: '1000587717', tagName: '70后', resourceType: '2034' }, relationSort: 17 }, { contentId: '1000587718', relationType: 4034, objectInfo: { tagId: '1000587718', tagName: '80后', resourceType: '2034' }, relationSort: 16 }, { contentId: '1000587726', relationType: 4034, objectInfo: { tagId: '1000587726', tagName: '90后', resourceType: '2034' }, relationSort: 15 }, { contentId: '1000001670', relationType: 4034, objectInfo: { tagId: '1000001670', tagName: '红歌', resourceType: '2034' }, relationSort: 14 }, { contentId: '1000587698', relationType: 4034, objectInfo: { tagId: '1000587698', tagName: '游戏', resourceType: '2034' }, relationSort: 13 }, { contentId: '1000587706', relationType: 4034, objectInfo: { tagId: '1000587706', tagName: '动漫', resourceType: '2034' }, relationSort: 12 }, { contentId: '1000001675', relationType: 4034, objectInfo: { tagId: '1000001675', tagName: '中国风', resourceType: '2034' }, relationSort: 11 }, { contentId: '1000587712', relationType: 4034, objectInfo: { tagId: '1000587712', tagName: '青春校园', resourceType: '2034' }, relationSort: 10 }, { contentId: '1000587673', relationType: 4034, objectInfo: { tagId: '1000587673', tagName: '小清新', resourceType: '2034' }, relationSort: 9 }, { contentId: '1000093902', relationType: 4034, objectInfo: { tagId: '1000093902', tagName: 'DJ舞曲', resourceType: '2034' }, relationSort: 7 }, { contentId: '1000093963', relationType: 4034, objectInfo: { tagId: '1000093963', tagName: '广告', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001831', relationType: 4034, objectInfo: { tagId: '1000001831', tagName: '翻唱', resourceType: '2034' }, relationSort: 2 }, { contentId: '1003449726', relationType: 4034, objectInfo: { tagId: '1003449726', tagName: '读书', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620410266055', customizedPicUrls: [] }, relationSort: 5 }, { contentId: '15244509', relationType: 2011, objectInfo: { columnTitle: '风格', columnId: '15244509', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 12, columnStatus: 1, columnCreateTime: '2016-11-10 10:54:57.257', columntype: 2011, contents: [{ contentId: '1000001672', relationType: 4034, objectInfo: { tagId: '1000001672', tagName: '流行', resourceType: '2034' }, relationSort: 14 }, { contentId: '1000001808', relationType: 4034, objectInfo: { tagId: '1000001808', tagName: 'R&B', resourceType: '2034' }, relationSort: 13 }, { contentId: '1000001809', relationType: 4034, objectInfo: { tagId: '1000001809', tagName: '嘻哈', resourceType: '2034' }, relationSort: 12 }, { contentId: '1000001674', relationType: 4034, objectInfo: { tagId: '1000001674', tagName: '摇滚', resourceType: '2034' }, relationSort: 11 }, { contentId: '1000001682', relationType: 4034, objectInfo: { tagId: '1000001682', tagName: '电子', resourceType: '2034' }, relationSort: 10 }, { contentId: '1000001852', relationType: 4034, objectInfo: { tagId: '1000001852', tagName: '电子舞曲', resourceType: '2034' }, relationSort: 9 }, { contentId: '1000001681', relationType: 4034, objectInfo: { tagId: '1000001681', tagName: '爵士', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001683', relationType: 4034, objectInfo: { tagId: '1000001683', tagName: '乡村', resourceType: '2034' }, relationSort: 5 }, { contentId: '1000001851', relationType: 4034, objectInfo: { tagId: '1000001851', tagName: '蓝调', resourceType: '2034' }, relationSort: 4 }, { contentId: '1000001775', relationType: 4034, objectInfo: { tagId: '1000001775', tagName: '民谣', resourceType: '2034' }, relationSort: 3 }, { contentId: '1000001807', relationType: 4034, objectInfo: { tagId: '1000001807', tagName: '纯音乐', resourceType: '2034' }, relationSort: 2 }, { contentId: '1000001783', relationType: 4034, objectInfo: { tagId: '1000001783', tagName: '古典', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620410266033', customizedPicUrls: [] }, relationSort: 4 }, { contentId: '18464665', relationType: 2011, objectInfo: { columnTitle: '语种', columnId: '18464665', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 6, columnStatus: 1, columnCreateTime: '2017-02-20 16:07:16.566', columntype: 2011, contents: [{ contentId: '1000001762', relationType: 4034, objectInfo: { tagId: '1000001762', tagName: '国语', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001763', relationType: 4034, objectInfo: { tagId: '1000001763', tagName: '粤语', resourceType: '2034' }, relationSort: 5 }, { contentId: '1000001766', relationType: 4034, objectInfo: { tagId: '1000001766', tagName: '英语', resourceType: '2034' }, relationSort: 4 }, { contentId: '1000001599', relationType: 4034, objectInfo: { tagId: '1000001599', tagName: '韩语', resourceType: '2034' }, relationSort: 3 }, { contentId: '1000001767', relationType: 4034, objectInfo: { tagId: '1000001767', tagName: '日语', resourceType: '2034' }, relationSort: 2 }, { contentId: '1003449724', relationType: 4034, objectInfo: { tagId: '1003449724', tagName: '小语种', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620410266036', customizedPicUrls: [] }, relationSort: 3 }, { contentId: '18464583', relationType: 2011, objectInfo: { columnTitle: '心情', columnId: '18464583', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 13, columnStatus: 1, columnCreateTime: '2017-02-20 15:59:03.412', columntype: 2011, contents: [{ contentId: '1000587677', relationType: 4034, objectInfo: { tagId: '1000587677', tagName: '幸福', resourceType: '2034' }, relationSort: 15 }, { contentId: '1000587710', relationType: 4034, objectInfo: { tagId: '1000587710', tagName: '治愈', resourceType: '2034' }, relationSort: 14 }, { contentId: '1000001703', relationType: 4034, objectInfo: { tagId: '1000001703', tagName: '思念', resourceType: '2034' }, relationSort: 13 }, { contentId: '1000587667', relationType: 4034, objectInfo: { tagId: '1000587667', tagName: '期待', resourceType: '2034' }, relationSort: 12 }, { contentId: '1000001700', relationType: 4034, objectInfo: { tagId: '1000001700', tagName: '励志', resourceType: '2034' }, relationSort: 11 }, { contentId: '1000001694', relationType: 4034, objectInfo: { tagId: '1000001694', tagName: '欢快', resourceType: '2034' }, relationSort: 10 }, { contentId: '1002600588', relationType: 4034, objectInfo: { tagId: '1002600588', tagName: '叛逆', resourceType: '2034' }, relationSort: 9 }, { contentId: '1002600585', relationType: 4034, objectInfo: { tagId: '1002600585', tagName: '宣泄', resourceType: '2034' }, relationSort: 8 }, { contentId: '1000001696', relationType: 4034, objectInfo: { tagId: '1000001696', tagName: '怀旧', resourceType: '2034' }, relationSort: 7 }, { contentId: '1000587679', relationType: 4034, objectInfo: { tagId: '1000587679', tagName: '减压', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001699', relationType: 4034, objectInfo: { tagId: '1000001699', tagName: '寂寞', resourceType: '2034' }, relationSort: 5 }, { contentId: '1002600579', relationType: 4034, objectInfo: { tagId: '1002600579', tagName: '忧郁', resourceType: '2034' }, relationSort: 4 }, { contentId: '1000001795', relationType: 4034, objectInfo: { tagId: '1000001795', tagName: '伤感', resourceType: '2034' }, relationSort: 3 }], dataVersion: '1620410266187', customizedPicUrls: [] }, relationSort: 2 }, { contentId: '18464638', relationType: 2011, objectInfo: { columnTitle: '场景', columnId: '18464638', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 13, columnStatus: 1, columnCreateTime: '2017-02-20 16:02:59.711', columntype: 2011, contents: [{ contentId: '1000587689', relationType: 4034, objectInfo: { tagId: '1000587689', tagName: '清晨', resourceType: '2034' }, relationSort: 21 }, { contentId: '1000587690', relationType: 4034, objectInfo: { tagId: '1000587690', tagName: '夜晚', resourceType: '2034' }, relationSort: 20 }, { contentId: '1000587688', relationType: 4034, objectInfo: { tagId: '1000587688', tagName: '睡前安眠', resourceType: '2034' }, relationSort: 19 }, { contentId: '1003449726', relationType: 4034, objectInfo: { tagId: '1003449726', tagName: '读书', resourceType: '2034' }, relationSort: 18 }, { contentId: '1003449723', relationType: 4034, objectInfo: { tagId: '1003449723', tagName: '下午·茶', resourceType: '2034' }, relationSort: 16 }, { contentId: '1000093923', relationType: 4034, objectInfo: { tagId: '1000093923', tagName: '驾车', resourceType: '2034' }, relationSort: 15 }, { contentId: '1003449615', relationType: 4034, objectInfo: { tagId: '1003449615', tagName: '运动', resourceType: '2034' }, relationSort: 13 }, { contentId: '1000587694', relationType: 4034, objectInfo: { tagId: '1000587694', tagName: '散步', resourceType: '2034' }, relationSort: 12 }, { contentId: '1000001749', relationType: 4034, objectInfo: { tagId: '1000001749', tagName: '旅行', resourceType: '2034' }, relationSort: 11 }, { contentId: '1000587686', relationType: 4034, objectInfo: { tagId: '1000587686', tagName: '夜店', resourceType: '2034' }, relationSort: 10 }, { contentId: '1002600606', relationType: 4034, objectInfo: { tagId: '1002600606', tagName: '派对', resourceType: '2034' }, relationSort: 9 }, { contentId: '1000001634', relationType: 4034, objectInfo: { tagId: '1000001634', tagName: '咖啡馆', resourceType: '2034' }, relationSort: 3 }, { contentId: '1000587692', relationType: 4034, objectInfo: { tagId: '1000587692', tagName: '瑜伽', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620846028994', customizedPicUrls: [] }, relationSort: 1 }], dataVersion: '1620846028941', customizedPicUrls: [] } } - -export default { - _requestObj_tags: null as HttpRequestObject | null, - _requestObj_list: null as HttpRequestObject | null, - limit_list: 30, - limit_song: 50, - successCode: '000000', - cachedDetailInfo: {}, - cachedUrl: {}, - sortList: [ - { - name: '推荐', - id: '15127315', - // id: '1', - }, - // { - // name: '最新', - // id: '15127272', - // // id: '2', - // }, - ], - regExps: { - list: /
  • .+?<\/li>/g, - listInfo: /.+data-original="(.+?)".*data-id="(\d+)".*
    (.+?)<\/a>.+<\/i>(.+?)<\/div>/, - - // https://music.migu.cn/v3/music/playlist/161044573?page=1 - listDetailLink: /^.+\/playlist\/(\d+)(?:\?.*|&.*$|#.*$|$)/, - }, - tagsUrl: 'https://app.c.nf.migu.cn/pc/v1.0/template/musiclistplaza-taglist/release', - // tagsUrl: 'https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/indexTagPage.do?needAll=0', - getSongListUrl(sortId, tagId, page) { - // if (tagId == null) { - // return sortId == 'recommend' - // ? `https://music.migu.cn/v3/music/playlist?page=${page}&from=migu` - // : `https://music.migu.cn/v3/music/playlist?sort=${sortId}&page=${page}&from=migu` - // } - // return `https://music.migu.cn/v3/music/playlist?tagId=${tagId}&page=${page}&from=migu` - if (!tagId) { - // return `https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?count=${this.limit_list}&start=${page}&templateVersion=5&type=1` - // return `https://c.musicapp.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?count=${this.limit_list}&start=${page}&templateVersion=5&type=${sortId}` - // https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?count=50&start=2&templateVersion=5&type=1 - // return `https://m.music.migu.cn/migu/remoting/playlist_bycolumnid_tag?playListType=2&type=1&columnId=${sortId}&startIndex=${(page - 1) * 10}` - return `https://app.c.nf.migu.cn/pc/bmw/page-data/playlist-square-recommend/v1.0?templateVersion=2&pageNo=${page}` - } - // return `https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?area=2&count=${this.limit_list}&start=${page}&tags=${tagId}&templateVersion=5&type=3` - return `https://app.c.nf.migu.cn/pc/v1.0/template/musiclistplaza-listbytag/release?pageNumber=${page}&templateVersion=2&tagId=${tagId}` - // return `https://m.music.migu.cn/migu/remoting/playlist_bycolumnid_tag?playListType=2&type=1&tagId=${tagId}&startIndex=${(page - 1) * 10}` - }, - getSongListDetailUrl(id, page) { - return `https://app.c.nf.migu.cn/MIGUM3.0/resource/playlist/song/v2.0?pageNo=${page}&pageSize=${this.limit_song}&playlistId=${id}` - // return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/user/queryMusicListSongs.do?musicListId=${id}&pageNo=${page}&pageSize=${this.limit_song}` - }, - defaultHeaders: { - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', - Referer: 'https://m.music.migu.cn/', - // language: 'Chinese', - // ua: 'Android_migu', - // mode: 'android', - // version: '6.8.5', - }, - - getListDetailList(id, page, tryNum = 0) { - if (tryNum > 2) return Promise.reject(new Error('try max num')) - // https://h5.nf.migu.cn/app/v4/p/share/playlist/index.html?id=184187437&channel=0146921 - - // if (/playlist\/index\.html\?/.test(id)) { - // id = id.replace(/.*(?:\?|&)id=(\d+)(?:&.*|$)/, '$1') - // } else if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') - - const requestObj_listDetail = httpFetch(this.getSongListDetailUrl(id, page), { headers: this.defaultHeaders }) - return requestObj_listDetail.promise.then(({ body }) => { - if (body.code !== this.successCode) return this.getListDetailList(id, page, ++tryNum) - // console.log(JSON.stringify(body)) - // console.log(body) - return { - list: filterMusicInfoListV5(body.data.songList), - page, - limit: this.limit_song, - total: body.data.totalCount, - source: 'mg', - } - }) - }, - - getListDetailInfo(id, tryNum = 0) { - if (tryNum > 2) return Promise.reject(new Error('try max num')) - - if (this.cachedDetailInfo[id]) return Promise.resolve(this.cachedDetailInfo[id]) - const requestObj_listDetailInfo = httpFetch(`https://c.musicapp.migu.cn/MIGUM3.0/resource/playlist/v2.0?playlistId=${id}`, { - headers: this.defaultHeaders, - }) - return requestObj_listDetailInfo.promise.then(({ body }) => { - if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum) - // console.log(JSON.stringify(body)) - // console.log(body) - const cachedDetailInfo = this.cachedDetailInfo[id] = { - name: body.data.title, - img: body.data.imgItem.img, - desc: body.data.summary, - author: body.data.ownerName, - play_count: formatPlayCount(body.data.opNumItem.playNum), - } - return cachedDetailInfo - }) - }, - - async getDetailUrl(link, page, retryNum = 0) { - if (retryNum > 3) return Promise.reject(new Error('link try max num')) - - const requestObj_listDetailLink = httpFetch(link, { - headers: { - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', - Referer: link, - }, - }) - const { headers: { location }, statusCode } = await requestObj_listDetailLink.promise - // console.log(body, location) - if (statusCode > 400) return this.getDetailUrl(link, page, ++retryNum) - if (location) { - this.cachedUrl[link] = location - return this.getListDetail(location, page) - } - return Promise.reject(new Error('link get failed')) - }, - - getListDetail(id, page, retryNum = 0) { // 获取歌曲列表内的音乐 - // https://h5.nf.migu.cn/app/v4/p/share/playlist/index.html?id=184187437&channel=0146921 - // http://c.migu.cn/00bTY6?ifrom=babddaadfde4ebeda289d671ab62f236 - // https://music.migu.cn/v5/#/playlist?playlistId=221573417 - if (/\/playlist[/?]/.test(id)) { - id = /(?:playlistId|id)=(\d+)/.exec(id)?.[1] - if (!id) throw new Error('list detail id parse failed') - } else if (this.regExps.listDetailLink.test(id)) { - id = id.replace(this.regExps.listDetailLink, '$1') - } else if ((/[?&:/]/.test(id))) { - const url = this.cachedUrl[id] - return url ? this.getListDetail(url, page) : this.getDetailUrl(id, page) - } - - return Promise.all([ - this.getListDetailList(id, page, retryNum), - this.getListDetailInfo(id, retryNum), - ]).then(([listData, info]) => { - listData.info = info - return listData - }) - }, - - // 获取列表数据 - getList(sortId, tagId, page, tryNum = 0) { - if (this._requestObj_list) this._requestObj_list.cancelHttp() - if (tryNum > 2) return Promise.reject(new Error('try max num')) - this._requestObj_list = httpFetch(this.getSongListUrl(sortId, tagId, page), { - headers: this.defaultHeaders, - // headers: { - // sign: 'c3b7ae985e2206e97f1b2de8f88691e2', - // timestamp: 1578225871982, - // appId: 'yyapp2', - // mode: 'android', - // ua: 'Android_migu', - // version: '6.9.4', - // osVersion: 'android 7.0', - // 'User-Agent': 'okhttp/3.9.1', - // }, - }) - // return this._requestObj_list.promise.then(({ statusCode, body }) => { - // if (statusCode !== 200) return this.getList(sortId, tagId, page) - // let list = body.replace(/[\r\n]/g, '').match(this.regExps.list) - // if (!list) return Promise.reject(new Error('获取列表失败')) - // return list.map(item => { - // let info = item.match(this.regExps.listInfo) - // return { - // play_count: info[4], - // id: info[2], - // author: '', - // name: info[3], - // time: '', - // img: info[1], - // grade: 0, - // desc: '', - // source: 'mg', - // } - // }) - // }) - // return this._requestObj_list.promise.then(({ body }) => { - // console.log(body) - // if (body.retCode !== '100000' || body.retMsg.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) - // return { - // list: this.filterList(body.retMsg.playlist), - // total: parseInt(body.retMsg.countSize), - // page, - // limit: this.limit_list, - // source: 'mg', - // } - // }) - return this._requestObj_list!.promise.then(({ body }) => { - // console.log(body) - // if (body.retCode !== '000000') return this.getList(sortId, tagId, page, ++tryNum) - if (body.code !== '000000') return this.getList(sortId, tagId, page, ++tryNum) - const list = body.data.contents ? this.filterList2(body.data.contents) : this.filterList(body.data.contentItemList[1].itemList) - return { - list, - total: 99999, - page, - limit: this.limit_list, - source: 'mg', - } - }) - }, - filterList2(listData, list: any[] = [], ids = new Set()) { - for (const item of listData) { - if (item.contents) this.filterList2(item.contents, list, ids) - else if (item.resType == '2021' && !ids.has(item.resId)) { - ids.add(item.resId) - list.push({ - id: String(item.resId), - author: '', - name: item.txt, - // time: dateFormat(item.createTime, 'Y-M-D'), - img: item.img, - // grade: item.grade, - // total: item.contentCount, - desc: item.txt2, - source: 'mg', - }) - } - } - return list - }, - filterList(rawData) { - // console.log(rawData) - return rawData.map(item => ({ - play_count: item.barList[0]?.title, - id: String(item.logEvent.contentId), - author: '', - name: item.title, - // time: dateFormat(item.createTime, 'Y-M-D'), - img: item.imageUrl, - // grade: item.grade, - // total: item.contentCount, - desc: '', - source: 'mg', - })) - }, - - // 获取标签 - getTag(tryNum = 0) { - if (this._requestObj_tags) this._requestObj_tags.cancelHttp() - if (tryNum > 2) return Promise.reject(new Error('try max num')) - this._requestObj_tags = httpFetch(this.tagsUrl, { headers: this.defaultHeaders }) - return this._requestObj_tags!.promise.then(({ body }) => { - if (body.code !== this.successCode) return this.getTag(++tryNum) - // console.log(body) - return this.filterTagInfo(body.data) - }) - // return Promise.resolve(this.filterTagInfo(tagData.columnInfo.contents)) - }, - filterTagInfo(rawList) { - return { - hotTag: rawList[0].content.map(({ texts: [name, id] }) => ({ - id, - name, - source: 'mg', - })), - tags: rawList.slice(1).map(({ header, content }) => ({ - name: header.title, - list: content.map(({ texts: [name, id] }) => ({ - // parent_id: objectInfo.columnId, - // parent_name: objectInfo.columnTitle, - id, - name, - source: 'mg', - })), - })), - source: 'mg', - } - // return { - // hotTag: rawList[0].objectInfo.contents.map(item => ({ - // id: item.objectInfo.tagId, - // name: item.objectInfo.tagName, - // source: 'mg', - // })), - // tags: rawList.slice(1).map(({ objectInfo }) => ({ - // name: objectInfo.columnTitle, - // list: objectInfo.contents.map(item => ({ - // parent_id: objectInfo.columnId, - // parent_name: objectInfo.columnTitle, - // id: item.objectInfo.tagId, - // name: item.objectInfo.tagName, - // source: 'mg', - // })), - // })), - // source: 'mg', - // } - }, - getTags() { - return this.getTag() - }, - - getDetailPageUrl(id) { - if (/playlist\/index\.html\?/.test(id)) { - id = id.replace(/.*(?:\?|&)id=(\d+)(?:&.*|$)/, '$1') - } else if (this.regExps.listDetailLink.test(id)) { - id = id.replace(this.regExps.listDetailLink, '$1') - } - return `https://music.migu.cn/v3/music/playlist/${id}` - }, - - filterSongListResult(raw) { - const list: any[] = [] - raw.forEach(item => { - if (!item.id) return - - const playCount = parseInt(item.playNum) - list.push({ - play_count: isNaN(playCount) ? 0 : formatPlayCount(playCount), - id: item.id, - author: item.userName, - name: item.name, - img: item.musicListPicUrl, - total: item.musicNum, - source: 'mg', - }) - }) - return list - }, - search(text, page, limit = 20) { - const timeStr = Date.now().toString() - const signResult = createSignature(timeStr, text) - return createHttpFetch(`https://jadeite.migu.cn/music_search/v3/search/searchAll?isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A0%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22bestShow%22%3A0%2C%22songlist%22%3A1%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(text)}&pageNo=${page}&sort=0&sid=USS`, { - headers: { - uiVersion: 'A_music_3.6.1', - deviceId: signResult.deviceId, - timestamp: timeStr, - sign: signResult.sign, - channel: '0146921', - 'User-Agent': 'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', - }, - }).then(body => { - if (!body.songListResultData) throw new Error('get song list faild.') - - const list = this.filterSongListResult(body.songListResultData.result) - return { - list, - limit, - total: parseInt(body.songListResultData.totalCount), - source: 'mg', - } - }) - }, -} - -// getList -// getTags -// getListDetail diff --git a/musicSdk/mg/tipSearch.ts b/musicSdk/mg/tipSearch.ts deleted file mode 100644 index f25e016..0000000 --- a/musicSdk/mg/tipSearch.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createHttpFetch } from './utils/index.ts' - -export default { - requestObj: null as any, - cancelTipSearch() { - if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp() - }, - tipSearchBySong(str) { - this.cancelTipSearch() - this.requestObj = createHttpFetch(`https://music.migu.cn/v3/api/search/suggest?keyword=${encodeURIComponent(str)}`, { - headers: { - referer: 'https://music.migu.cn/v3', - }, - }) - return this.requestObj.then(body => { - return body.songs - }) - }, - handleResult(rawData) { - return rawData.map(info => `${info.name} - ${info.singerName}`) - }, - async search(str) { - return this.tipSearchBySong(str).then(result => this.handleResult(result)) - }, -} diff --git a/musicSdk/mg/utils/index.ts b/musicSdk/mg/utils/index.ts deleted file mode 100644 index 5d6416e..0000000 --- a/musicSdk/mg/utils/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { httpFetch } from '../../request.ts' -import type { HttpOptions } from '../../types.ts' - -/** - * 创建一个适用于MG的Http请求 - * @param {*} url - * @param {*} options - * @param {*} retryNum - */ -export const createHttpFetch = async(url: string, options?: HttpOptions, retryNum = 0): Promise => { - if (retryNum > 2) throw new Error('try max num') - let result - try { - result = await httpFetch(url, options).promise - } catch (err) { - console.log(err) - return createHttpFetch(url, options, ++retryNum) - } - if (result.statusCode !== 200 || - ( - (result.body.code !== undefined - ? result.body.code - : result.body.returnCode !== undefined - ? result.body.returnCode - : result.body.code - ) !== '000000') - ) return createHttpFetch(url, options, ++retryNum) - if (result.body.data) return result.body.data - return result.body -} diff --git a/musicSdk/mg/utils/mrc.ts b/musicSdk/mg/utils/mrc.ts deleted file mode 100644 index 6498f25..0000000 --- a/musicSdk/mg/utils/mrc.ts +++ /dev/null @@ -1,104 +0,0 @@ - -// const key = 'karakal@123Qcomyidongtiantianhaoting' -const DELTA = 2654435769n -const MIN_LENGTH = 32 -// const SPECIAL_CHAR = '0' -const keyArr = [ - 27303562373562475n, - 18014862372307051n, - 22799692160172081n, - 34058940340699235n, - 30962724186095721n, - 27303523720101991n, - 27303523720101998n, - 31244139033526382n, - 28992395054481524n, -] - - -const teaDecrypt = (data: bigint[], key: bigint[]): bigint[] => { - const length = data.length - const lengthBitint = BigInt(length) - if (length >= 1) { - // let j = data[data.length - 1]; - let j2 = data[0] - let j3 = toLong((6n + (52n / lengthBitint)) * DELTA) - while (true) { - let j4 = j3 - if (j4 == 0n) break - let j5 = toLong(3n & toLong(j4 >> 2n)) - let j6 = lengthBitint - while (true) { - j6-- - if (j6 > 0n) { - let j7 = data[(j6 - 1n) as unknown as number] - let i = j6 as unknown as number - j2 = toLong(data[i] - (toLong(toLong(j2 ^ j4) + toLong(j7 ^ key[toLong(toLong(3n & j6) ^ j5) as unknown as number])) ^ toLong(toLong(toLong(j7 >> 5n) ^ toLong(j2 << 2n)) + toLong(toLong(j2 >> 3n) ^ toLong(j7 << 4n))))) - data[i] = j2 - } else break - } - let j8 = data[lengthBitint - 1n as unknown as number] - j2 = toLong(data[0n as unknown as number] - toLong(toLong(toLong(key[toLong(toLong(j6 & 3n) ^ j5) as unknown as number] ^ j8) + toLong(j2 ^ j4)) ^ toLong(toLong(toLong(j8 >> 5n) ^ toLong(j2 << 2n)) + toLong(toLong(j2 >> 3n) ^ toLong(j8 << 4n))))) - data[0] = j2 - j3 = toLong(j4 - DELTA) - } - } - return data -} - -const longArrToString = (data: bigint[]): string => { - const arrayList: string[] = [] - for (const j of data) arrayList.push(longToBytes(j).toString('utf16le')) - return arrayList.join('') -} - -// https://stackoverflow.com/a/29132118 -const longToBytes = (l: bigint): Buffer => { - const result = Buffer.alloc(8) - for (let i = 0; i < 8; i++) { - result[i] = parseInt((l & 0xFFn) as unknown as string) - l >>= 8n - } - return result -} - - -const toBigintArray = (data: string): bigint[] => { - const length = Math.floor(data.length / 16) - const jArr: bigint[] = Array(length) - for (let i = 0; i < length; i++) { - jArr[i] = toLong(data.substring(i * 16, (i * 16) + 16)) - } - return jArr -} - -// 基于社区通用的 mrc 逐字歌词解析思路 -const MAX = 9223372036854775807n -const MIN = -9223372036854775808n -const toLong = (str: string | bigint): bigint => { - const num = typeof str == 'string' ? BigInt('0x' + str) : str - if (num > MAX) return toLong(num - (1n << 64n)) - else if (num < MIN) return toLong(num + (1n << 64n)) - return num -} - -export const decrypt = (data: string | null): string | null => { - // console.log(data.length) - // -3551594764563790630 - // console.log(toLongArrayFromArr(Buffer.from(key))) - // console.log(teaDecrypt(toBigintArray(data), keyArr)) - // console.log(longArrToString(teaDecrypt(toBigintArray(data), keyArr))) - // console.log(toByteArray(teaDecrypt(toBigintArray(data), keyArr))) - return (data == null || data.length < MIN_LENGTH) - ? data - : longArrToString(teaDecrypt(toBigintArray(data), keyArr)) -} - -// console.log(14895149309145760986n - ) -// console.log(toLong('14895149309145760986')) -// console.log(decrypt(str)) -// console.log(decrypt(str)) -// console.log(toByteArray([6048138644744000495n])) -// console.log(toByteArray([16325999628386395n])) -// console.log(toLong(90994076459972177136n)) - diff --git a/musicSdk/options.ts b/musicSdk/options.ts deleted file mode 100644 index 5147d94..0000000 --- a/musicSdk/options.ts +++ /dev/null @@ -1,9 +0,0 @@ -// SDK 公共请求常量 -export const bHh = '624868746c' - -export const headers: Record = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - [bHh]: [bHh], -} - -export const timeout = 15000 diff --git a/musicSdk/request.ts b/musicSdk/request.ts deleted file mode 100644 index 7af1270..0000000 --- a/musicSdk/request.ts +++ /dev/null @@ -1,8 +0,0 @@ -// SDK 根请求模块(转接至 src/common/request.js) -// musicSdk 内 `import { ... } from '../request.js'` 的解析目标 - -import { httpFetch as rawHttpFetch } from '../src/common/request.js' -import type { HttpRequestObject, HttpOptions } from './types.ts' - -export const httpFetch: (url: string, options?: HttpOptions) => HttpRequestObject = - rawHttpFetch as (url: string, options?: HttpOptions) => HttpRequestObject diff --git a/musicSdk/tx/api-test.js b/musicSdk/tx/api-test.js deleted file mode 100644 index c68f42e..0000000 --- a/musicSdk/tx/api-test.js +++ /dev/null @@ -1,29 +0,0 @@ -import { httpFetch } from '../request.js' -import { requestMsg } from '../message.js' -import { headers, timeout } from '../options.js' -import { dnsLookup } from '../utils.js' - -const api_messoer = { - getMusicUrl(songInfo, type) { - const requestObj = httpFetch(`http://ts.tempmusics.tk/url/tx/${songInfo.songmid}/${type}`, { - method: 'get', - timeout, - headers, - lookup: dnsLookup, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { - if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) - switch (body.code) { - case 0: return Promise.resolve({ type, url: body.data }) - default: return Promise.reject(new Error(requestMsg.fail)) - } - }) - return requestObj - }, - getPic(songInfo) { - return Promise.resolve(`https://y.gtimg.cn/music/photo_new/T002R500x500M000${songInfo.albumId}.jpg`) - }, -} - -export default api_messoer diff --git a/musicSdk/wy/api-test.js b/musicSdk/wy/api-test.js deleted file mode 100644 index 1502cf6..0000000 --- a/musicSdk/wy/api-test.js +++ /dev/null @@ -1,50 +0,0 @@ -import { httpFetch } from '../request.js' -import { requestMsg } from '../message.js' -import { headers, timeout } from '../options.js' -import { dnsLookup } from '../utils.js' - -const api_test = { - getMusicUrl(songInfo, type) { - const requestObj = httpFetch(`http://ts.tempmusics.tk/url/wy/${songInfo.songmid}/${type}`, { - method: 'get', - timeout, - headers, - lookup: dnsLookup, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ statusCode, body }) => { - if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests)) - switch (body.code) { - case 0: return Promise.resolve({ type, url: body.data }) - default: return Promise.reject(new Error(requestMsg.fail)) - } - }) - return requestObj - }, -/* getPic(songInfo) { - const requestObj = httpFetch(`http://localhost:3100/pic/wy/${songInfo.songmid}`, { - method: 'get', - timeout, - headers, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ body }) => { - return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) - }) - return requestObj - }, - getLyric(songInfo) { - const requestObj = httpFetch(`http://localhost:3100/lrc/wy/${songInfo.songmid}`, { - method: 'get', - timeout, - headers, - family: 4, - }) - requestObj.promise = requestObj.promise.then(({ body }) => { - return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) - }) - return requestObj - }, */ -} - -export default api_test diff --git a/package.json b/package.json index b22026e..d4274e1 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,12 @@ "version": "1.0.0", "description": "多平台音乐聚合服务器", "type": "module", - "main": "src/index.js", + "main": "sdk/index.ts", "packageManager": "pnpm@12.4.1", "scripts": { - "build": "tsc", - "start": "pnpm run build && node src/server.js", - "test": "pnpm run build && node src/smoke-test.js" + "typecheck": "tsc", + "start": "node src/server.ts", + "test": "pnpm run typecheck && node test/smoke-test.ts" }, "dependencies": { "@fastify/cors": "^11.3.0", @@ -25,5 +25,8 @@ "@types/node": "^26.4.0", "playwright": "^1.62.1", "typescript": "^7.0.2" + }, + "engines": { + "node": ">=22.18" } } diff --git a/musicSdk/api-source-info.ts b/sdk/api-source-info.ts similarity index 77% rename from musicSdk/api-source-info.ts rename to sdk/api-source-info.ts index 3831796..ed6f8c9 100644 --- a/musicSdk/api-source-info.ts +++ b/sdk/api-source-info.ts @@ -5,7 +5,7 @@ interface ApiSourceInfo { id: string name: string disabled: boolean - supportQualitys: Partial> + supportQualitys: Partial> } const sources: ApiSourceInfo[] = [ @@ -18,8 +18,6 @@ const sources: ApiSourceInfo[] = [ // kg: ['128k'], // tx: ['128k'], // wy: ['128k'], - // mg: ['128k'], - // // bd: ['128k'], // }, // }, { @@ -31,8 +29,6 @@ const sources: ApiSourceInfo[] = [ kg: ['128k', '320k', 'flac'], tx: ['128k', '320k', 'flac'], wy: ['128k', '320k', 'flac'], - mg: ['128k', '320k', 'flac'], - // bd: ['128k', '320k', 'flac'], }, }, ] diff --git a/musicSdk/api-source.ts b/sdk/api-source.ts similarity index 72% rename from musicSdk/api-source.ts rename to sdk/api-source.ts index f04fc7f..6d85c55 100644 --- a/musicSdk/api-source.ts +++ b/sdk/api-source.ts @@ -1,8 +1,18 @@ // 接口选择层:按 apiSource 配置把请求路由到外部聚合音源或服务器内置直连接口 import apiSourceInfo from './api-source-info.ts' -import { apiSource, userApi } from '../src/renderer/store.js' -import internalApi from '../src/apis/index.js' -import { createApi as createSourceApi } from '../src/apis/source.js' +// 接口模式开关(替代原 lx-music 渲染进程 store 的服务器端替身): +// 默认 internal 使用内置直连接口;设置 SOURCE_ENABLED(任意非 0/false 值)启用外部聚合音源 +const enabled = process.env.SOURCE_ENABLED + +export const apiSource = { + value: enabled && enabled !== '0' && enabled !== 'false' ? 'source' : 'internal', +} + +export const userApi = { + apis: {}, +} +import internalApi from './apis/index.ts' +import { createApi as createSourceApi } from './apis/source.ts' interface SourceApi { getMusicUrl(songInfo: any, type: string): { promise: Promise; cancelHttp(): void } @@ -14,7 +24,6 @@ const allApi: Record = { source_kg: createSourceApi('kg'), source_tx: createSourceApi('tx'), source_wy: createSourceApi('wy'), - source_mg: createSourceApi('mg'), } const apiList: Record = {} diff --git a/src/apis/index.ts b/sdk/apis/index.ts similarity index 70% rename from src/apis/index.ts rename to sdk/apis/index.ts index 63d3a96..6c52ff8 100644 --- a/src/apis/index.ts +++ b/sdk/apis/index.ts @@ -1,13 +1,12 @@ // 各平台 URL 直连接口汇总 // 服务器环境固定使用内置直连接口(apiSource.value === 'internal') -// 外部聚合音源(sources/ 目录脚本)不走这里,由 musicSdk/api-source.ts 直接接入 source.js +// 外部聚合音源(sources/ 目录脚本)不走这里,由 sdk/api-source.ts 直接接入 source.ts +// 注意:index.js 为 tsc 就地发射产物(不入库);改逻辑只改本文件,然后 pnpm build -import kw from './kw.js' -import kg from './kg.js' -import tx from './tx.js' -import wy from './wy.js' -import mg from './mg.js' -import bd from './bd.js' +import kw from './kw.ts' +import kg from './kg.ts' +import tx from './tx.ts' +import wy from './wy.ts' interface SongInfo { songmid?: string @@ -31,6 +30,4 @@ export default { kg: kg as ApiModule, tx: tx as ApiModule, wy: wy as ApiModule, - mg: mg as ApiModule, - bd: bd as ApiModule, } diff --git a/src/apis/kg.js b/sdk/apis/kg.ts similarity index 92% rename from src/apis/kg.js rename to sdk/apis/kg.ts index 1af13da..584e32f 100644 --- a/src/apis/kg.js +++ b/sdk/apis/kg.ts @@ -6,9 +6,9 @@ // promise resolve → { type, url } import crypto from 'node:crypto' -import { httpFetch } from '../common/request.js' -import { getKgAuth } from '../auth/store.js' -import { androidSign, getDevice } from '../auth/kg.js' +import { httpFetch } from '../common/request.ts' +import { getKgAuth } from '../auth/store.ts' +import { androidSign, getDevice } from '../auth/kg.ts' const md5 = s => crypto.createHash('md5').update(s).digest('hex') const SALT_KEY = '57ae12eb6890223e355ccfcb74edf70d' @@ -18,7 +18,7 @@ const CLIENTVER = 11430 const qualityMap = { '128k': '128', '320k': '320', flac: 'flac', flac24bit: 'high' } export const getMusicUrl = (songInfo, type) => { - const requestObj = { cancelHttp() {}, promise: null } + const requestObj = { cancelHttp() {}, promise: null as any } const hash = (songInfo._types?.[type]?.hash || songInfo.hash || '').toLowerCase() if (!hash) { requestObj.promise = Promise.reject(new Error('缺少 hash')) @@ -32,7 +32,7 @@ export const getMusicUrl = (songInfo, type) => { const auth = getKgAuth() const userid = Number(auth?.userid || 0) - const params = { + const params: Record = { dfid: dev.dfid, mid: dev.mid, uuid: '-', appid: APPID, clientver: CLIENTVER, clienttime: Math.floor(Date.now() / 1000), album_id: Number(songInfo.albumId ?? 0), diff --git a/src/apis/kw.js b/sdk/apis/kw.ts similarity index 71% rename from src/apis/kw.js rename to sdk/apis/kw.ts index ad76e3e..563206f 100644 --- a/src/apis/kw.js +++ b/sdk/apis/kw.ts @@ -2,14 +2,22 @@ // 统一接口约定:apis('kw').getMusicUrl(songInfo, type) → { promise, cancelHttp } // promise resolve → { type, url } // -// 取流方案: -// 首选 antiserver.kuwo.cn/anti.s(convert_url3 老接口,无需 token,128k/320k 有效) -// flac 及以上音质该接口返回 refuse,回退 www.kuwo.cn/api/v1/www/music/playUrl -// (需要 csrf token,token 失效时自动重新获取重试一次) +// 取流方案(按序回退): +// 1. antiserver.kuwo.cn/anti.s(convert_url3 老接口,无需 token,128k/320k 有效, +// 未登录时部分资源只返回试听片段) +// 2. www.kuwo.cn/api/v1/www/music/playUrl(需 csrf token,token 失效自动重取重试一次) +// +// 2026-09-19 实测结论(勿轻易改链): +// - mobi.kuwo.cn/mobi.s(DES 加密 query)已进防盗链蜜罐: +// 任意 rid(含不存在的)都返回 200 与 MP3 地址,但内容是《防盗链打击音频》假文件, +// 且响应无法与真实响应区分,故不接入默认链路;DES 实现可参考 UnblockNeteaseMusic 上游 kwDES。 +// - playUrl 对新 token(Hm_Iuvt_* cookie)返回 "The request is illegal!"(要求签名), +// 首页也不再下发 kw_token,getToken 会失败——通道名存实亡,仅作最后回退。 +// - flac 及以上音质各匿名通道均要求 VIP 登录态;接入酷我登录 token 后可重估 mobi.s。 -import { httpFetch } from '../common/request.js' +import { httpFetch } from '../common/request.ts' -// 音质映射:128k → 128kmp3, 320k → 320kmp3, flac → flac(antiserver 用 mp3/flac 格式) +// 音质映射:128k → 128kmp3, 320k → 320kmp3, flac → flac(antiserver/mobi.s 用 mp3/flac 格式) const qualityMap = { '128k': { br: '128kmp3', format: 'mp3' }, '320k': { br: '320kmp3', format: 'mp3' }, @@ -18,7 +26,7 @@ const qualityMap = { } let token = '' -let tokenPromise = null +let tokenPromise: Promise | null = null /** * 获取 kw csrf token(访问首页拿 cookie) @@ -31,7 +39,7 @@ const getToken = (retryNum = 0) => { timeout: 15000, }).promise.then(({ headers }) => { tokenPromise = null - const match = (headers['set-cookie'] || []).join(';').match(/kw_token=(\w+)/) + const match = ((headers['set-cookie'] as string[]) || []).join(';').match(/kw_token=(\w+)/) if (!match) throw new Error('获取token失败') token = match[1] return token @@ -94,7 +102,7 @@ const getUrlByPlayUrl = (songmid, br, retryNum = 0) => { } export const getMusicUrl = (songInfo, type) => { - const requestObj = { cancelHttp() {}, promise: null } + const requestObj = { cancelHttp() {}, promise: null as any } const { br, format } = qualityMap[type] || qualityMap['128k'] const rid = `MUSIC_${songInfo.songmid}` diff --git a/src/apis/source-ivm.js b/sdk/apis/source-ivm.ts similarity index 98% rename from src/apis/source-ivm.js rename to sdk/apis/source-ivm.ts index 3c5a191..5df9fc7 100644 --- a/src/apis/source-ivm.js +++ b/sdk/apis/source-ivm.ts @@ -15,7 +15,7 @@ // // 依赖:isolated-vm(未安装时工厂函数抛出明确错误) // 用法: -// import { createIvmSource } from './source-ivm.js' +// import { createIvmSource } from './source-ivm.ts' // const source = await createIvmSource({ scriptPath }) // scriptPath 必填(sources/ 扫描逻辑见 source.js) // const source = await createIvmSource({ scriptPath, http }) // http 可注入 mock 做离线测试 // await source.kw.getMusicUrl({ songmid: '123' }, '320k').promise @@ -29,12 +29,12 @@ import fs from 'node:fs' import crypto from 'node:crypto' import zlib from 'node:zlib' -import { httpFetch } from '../common/request.js' +import { httpFetch } from '../common/request.ts' // 从脚本头部的 SERVER_SCRIPT_CONFIG 提取 apiUrl 声明,作为 HTTP 白名单缺省值。 // 兼容裸键(apiUrl:)与 JSON 风格("apiUrl":),并处理 \/ 转义 const extractAllowedOrigins = (script) => { - const origins = [] + const origins: string[] = [] const re = /["']?apiUrl["']?\s*:\s*["'`]([^"'`\s]+)["'`]/g let m while ((m = re.exec(script))) { @@ -365,7 +365,7 @@ export const createIvmSource = async({ allowedOrigins, httpFilter, allowAllHttp = false, -} = {}) => { +}: Record = {}) => { let ivm try { ivm = (await import('isolated-vm')).default @@ -398,7 +398,7 @@ export const createIvmSource = async({ const jail = context.global // ---- Node 侧状态 ---- - const state = { + const state: Record = { inited: null, updateAlerts: [], onRegistered: null, @@ -425,7 +425,7 @@ export const createIvmSource = async({ try { fnRef.apply(undefined, args) } catch (err) { - log(`isolate 调用失败: ${err?.message}`) + log(`isolate 调用失败: ${(err as any)?.message}`) } } @@ -494,7 +494,7 @@ export const createIvmSource = async({ } log(`→ source.request ${method} ${brief(url, 120)}`) const startedAt = Date.now() - const options = { + const options: Record = { method: opts.method || 'get', timeout: typeof opts.timeout === 'number' && opts.timeout > 0 ? opts.timeout : 15000, } @@ -544,8 +544,8 @@ export const createIvmSource = async({ await context.eval(script, { async: true, timeout: 15000 }) // ---- 对外 dispatch ---- - const dispatch = (payload) => { - if (disposed) return Promise.reject(new Error('isolate 已销毁')) + const dispatch = (payload: any): { id: number; promise: Promise } => { + if (disposed) return Promise.reject(new Error('isolate 已销毁')) as any const id = ++dispatchSeq return { id, @@ -561,7 +561,7 @@ export const createIvmSource = async({ } const getMusicUrlFor = (source) => (songInfo, type) => { - const requestObj = { cancelHttp() {}, promise: null } + const requestObj = { cancelHttp() {}, promise: null as any } const id = songInfo?.hash || songInfo?.songmid || '?' const dispatched = dispatch({ source, action: 'musicUrl', info: { type, musicInfo: songInfo } }) const startedAt = Date.now() @@ -592,7 +592,7 @@ export const createIvmSource = async({ } // ---- 等待脚本 inited ---- - const inited = await new Promise((resolve, reject) => { + const inited = await new Promise((resolve, reject) => { if (state.inited) return resolve(state.inited) const waiter = { resolve, reject } state.initWaiters.push(waiter) @@ -609,7 +609,7 @@ export const createIvmSource = async({ // 按脚本上报的 sources 生成分平台接口(过滤 local 这类服务器侧无意义条目仍保留键,但 local 无 qualitys) const reportedSources = inited?.sources && typeof inited.sources === 'object' ? Object.keys(inited.sources) : [] - const sources = reportedSources.length ? reportedSources : ['kw', 'kg', 'tx', 'wy', 'mg'] + const sources = reportedSources.length ? reportedSources : ['kw', 'kg', 'tx', 'wy'] const apis = {} for (const s of sources) apis[s] = { getMusicUrl: getMusicUrlFor(s) } diff --git a/src/apis/source.js b/sdk/apis/source.ts similarity index 85% rename from src/apis/source.js rename to sdk/apis/source.ts index 453b931..31b2334 100644 --- a/src/apis/source.js +++ b/sdk/apis/source.ts @@ -1,5 +1,5 @@ // 外部聚合音源统一入口(isolated-vm 懒加载封装) -// 内部委托 source-ivm.js 在硬隔离沙箱中执行音源脚本。 +// 内部委托 source-ivm.ts 在硬隔离沙箱中执行音源脚本。 // 首次调用 getMusicUrl 时才加载并初始化隔离环境,后续复用同一实例。 // 统一接口约定:source.kw.getMusicUrl(songInfo, type) → { promise, cancelHttp },resolve → { type, url } // @@ -10,9 +10,9 @@ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' -import { createIvmSource } from './source-ivm.js' +import { createIvmSource } from './source-ivm.ts' -export const SOURCES = ['kw', 'kg', 'tx', 'wy', 'mg'] +export const SOURCES = ['kw', 'kg', 'tx', 'wy'] const SOURCES_DIR = fileURLToPath(new URL('../../sources/', import.meta.url)) @@ -32,7 +32,7 @@ const resolveScriptPath = () => { return path.join(SOURCES_DIR, files[0]) } -let sharedPromise = null +let sharedPromise: Promise | null = null const getShared = () => { if (!sharedPromise) { @@ -51,8 +51,8 @@ const getShared = () => { const apiFor = (source) => ({ getMusicUrl(songInfo, type) { - const requestObj = { cancelHttp() {}, promise: null } - let inner = null + const requestObj = { cancelHttp() {}, promise: null as any } + let inner: any = null requestObj.cancelHttp = () => inner?.cancelHttp() requestObj.promise = (async () => { const api = await getShared() @@ -64,10 +64,10 @@ const apiFor = (source) => ({ }, }) -// 兼容导出:musicSdk/api-source.js 以 createApi(source) 方式接入聚合音源 +// 兼容导出:sdk/api-source.ts 以 createApi(source) 方式接入聚合音源 export const createApi = apiFor -const sourceApi = {} +const sourceApi: Record = {} for (const s of SOURCES) sourceApi[s] = apiFor(s) // 预热:提前触发脚本加载与初始化(可选调用) diff --git a/sdk/apis/tx.ts b/sdk/apis/tx.ts new file mode 100644 index 0000000..fd13dc9 --- /dev/null +++ b/sdk/apis/tx.ts @@ -0,0 +1,87 @@ +// QQ音乐 URL 获取(直连) +// 接口: https://u.y.qq.com/cgi-bin/musicu.fcg +// 方法: vkey.GetVkeyServer.CgiGetVkey +// 取流方案: +// 按请求音质组 filename 链一次请求(M500{media_mid}.mp3 / M800{media_mid}.mp3 / F000{media_mid}.flac), +// 响应 midurlinfo 与 filename 一一对应,取第一个有 purl 的(即最高可用音质); +// 始终附带 C400{songmid}.m4a 保底(匿名可取的 128k)。 +// 登录 cookie(uin + qm_keyst,见 /login/tx)可解锁 320k/无损。 +// 统一接口约定:apis('tx').getMusicUrl(songInfo, type) → { promise, cancelHttp } +// promise resolve → { type, url }(type 为实际命中的音质,可能低于请求值) + +import { httpFetch } from '../common/request.ts' +import { getTxAuth } from '../auth/store.ts' + +// 音质 → [文件前缀, 扩展名](flac24bit 暂走 flac 通道,命中即返回) +const prefixMap = { + '128k': ['M500', 'mp3'], + '320k': ['M800', 'mp3'], + flac: ['F000', 'flac'], + flac24bit: ['F000', 'flac'], +} +// 降级链:高音质无版权/VIP 受限时逐级回退 +const chainMap = { + '128k': ['128k'], + '320k': ['320k', '128k'], + flac: ['flac', '320k', '128k'], + flac24bit: ['flac', '320k', '128k'], +} + +export const getMusicUrl = (songInfo, type) => { + const requestObj = { cancelHttp() {}, promise: null as any } + const mediaMid = songInfo.strMediaMid || songInfo.songmid + const entries: any[] = [] + for (const t of chainMap[type] || chainMap['128k']) { + const [prefix, ext] = prefixMap[t] + entries.push({ filename: `${prefix}${mediaMid}.${ext}`, type: t }) + } + entries.push({ filename: `C400${songInfo.songmid}.m4a`, type: '128k' }) + + const auth = getTxAuth() + const uin = auth?.uin || '0' + const data = { + comm: { ct: '19', cv: '1859', uin }, + req: { + module: 'vkey.GetVkeyServer', + method: 'CgiGetVkey', + param: { + guid: '7342827269', + songmid: [songInfo.songmid], + filename: entries.map(e => e.filename), + songtype: [0], + uin, + loginflag: 1, + platform: '20', + needNewCode: 1, + }, + }, + } + + requestObj.promise = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'post', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + Referer: 'https://y.qq.com/', + ...(auth ? { cookie: auth.cookie } : {}), + }, + body: data, + timeout: 15000, + }).promise.then(({ statusCode, body }) => { + if (statusCode != 200 || body.code != 0 || body.req?.code != 0) { + return Promise.reject(new Error('获取失败')) + } + const data = body.req.data + const sip = data.sip?.[0] || 'http://aqqmusic.tc.qq.com/' + const infos = data.midurlinfo || [] + const idx = infos.findIndex(info => info.purl) + if (idx >= 0) { + return { type: entries[idx]?.type || '128k', url: `${sip}${infos[idx].purl}` } + } + // 全部受限(VIP 歌匿名)时回退试听地址(服务器返回的 128k m4a 试听文件) + if (data.testfile2g) return { type: '128k', url: `${sip}${data.testfile2g}` } + return Promise.reject(new Error('获取失败')) + }) + return requestObj +} + +export default { getMusicUrl } diff --git a/src/apis/wy.js b/sdk/apis/wy.ts similarity index 81% rename from src/apis/wy.js rename to sdk/apis/wy.ts index 3e6f1e1..cff3022 100644 --- a/src/apis/wy.js +++ b/sdk/apis/wy.ts @@ -4,9 +4,10 @@ // 统一接口约定:apis('wy').getMusicUrl(songInfo, type) → { promise, cancelHttp } // promise resolve → { type, url } -import { httpFetch } from '../common/request.js' -import { eapi } from '../../musicSdk/wy/utils/crypto.ts' -import { getWyCookie } from '../auth/store.js' +import { httpFetch } from '../common/request.ts' +// crypto.js 为 tsc 就地发射产物(源 sdk/wy/utils/crypto.ts);统一走 .js,避免同进程出现 .ts/.js 双实例 +import { eapi } from '../wy/utils/crypto.ts' +import { getWyCookie } from '../auth/store.ts' const levelMap = { '128k': 'standard', @@ -16,7 +17,7 @@ const levelMap = { } export const getMusicUrl = (songInfo, type) => { - const requestObj = { cancelHttp() {}, promise: null } + const requestObj = { cancelHttp() {}, promise: null as any } const data = { ids: `[${songInfo.songmid}]`, level: levelMap[type] || 'standard', diff --git a/src/auth/kg.js b/sdk/auth/kg.ts similarity index 96% rename from src/auth/kg.js rename to sdk/auth/kg.ts index dafefe9..6e55c84 100644 --- a/src/auth/kg.js +++ b/sdk/auth/kg.ts @@ -3,8 +3,8 @@ // token 可从酷狗 App 抓包获取(login.token) import crypto from 'node:crypto' -import { httpFetch } from '../common/request.js' -import { getCredentials, setCredentials } from './store.js' +import { httpFetch } from '../common/request.ts' +import { getCredentials, setCredentials } from './store.ts' const md5 = s => crypto.createHash('md5').update(s).digest('hex') const SALT_KEY = '57ae12eb6890223e355ccfcb74edf70d' @@ -79,7 +79,7 @@ export const verifyToken = async({ token, userid }) => { }) if (body.status !== 1 || !body.data) { const err = new Error(body.error ?? 'token 无效或已过期') - err.status = 401 + ;(err as any).status = 401 throw err } return { diff --git a/src/auth/store.ts b/sdk/auth/store.ts similarity index 77% rename from src/auth/store.ts rename to sdk/auth/store.ts index 1b2208e..691e4da 100644 --- a/src/auth/store.ts +++ b/sdk/auth/store.ts @@ -1,6 +1,6 @@ // 登录态存储 —— 各平台凭据持久化(data/auth.json,不入库) -// 结构:{ wy: {...}, kg: {...}, _kg_device: {...} } -// 注意:本文件经 tsc 就地发射 store.js 供纯 JS 模块引用(与 src/apis/index.ts 同约定) +// 结构:{ wy: {...}, kg: {...}, tx: {...}, _kg_device: {...} } +// 注意:store.js 为 tsc 就地发射产物(不入库);改逻辑只改本文件,然后 pnpm build import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' @@ -32,6 +32,11 @@ export interface KgDevice { updatedAt?: string } +export interface TxCredentials { + cookie: string + updatedAt?: string +} + let cache: Record = {} let loaded = false @@ -75,3 +80,11 @@ export const getKgAuth = (): { token: string; userid: number | string } | null = const cred = getCredentials('kg') as KgCredentials | null return cred?.token ? { token: cred.token, userid: cred.userid || 0 } : null } + +/** tx(QQ) 登录态:cookie 与其中的 uin(未登录返回 null) */ +export const getTxAuth = (): { cookie: string; uin: string } | null => { + const cred = getCredentials('tx') as TxCredentials | null + if (!cred?.cookie) return null + const uin = cred.cookie.match(/uin=(\d+)/)?.[1] ?? '0' + return { cookie: cred.cookie, uin } +} diff --git a/src/auth/wy.js b/sdk/auth/wy.ts similarity index 95% rename from src/auth/wy.js rename to sdk/auth/wy.ts index bfd418d..96bff78 100644 --- a/src/auth/wy.js +++ b/sdk/auth/wy.ts @@ -2,9 +2,9 @@ // 参考实现:NeteaseCloudMusicApi(login_qr_key / login_qr_check / login_cellphone / login_status) import crypto from 'node:crypto' -import { httpFetch } from '../common/request.js' -import { weapi } from '../../musicSdk/wy/utils/crypto.ts' -import { getWyCookie, setCredentials } from './store.js' +import { httpFetch } from '../common/request.ts' +import { weapi } from '../wy/utils/crypto.ts' +import { getWyCookie, setCredentials } from './store.ts' const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' @@ -106,7 +106,7 @@ export const cellphoneLogin = async({ phone, password, captcha, countrycode = '8 const { body, setCookie } = await requestWeapi('/api/w/login/cellphone', data) if (body.code !== 200) { const err = new Error(body.message ?? body.msg ?? '登录失败') - err.status = 401 + ;(err as any).status = 401 throw err } const profile = await applyLogin(setCookie) diff --git a/src/common/ipcNames.js b/sdk/common/ipcNames.ts similarity index 100% rename from src/common/ipcNames.js rename to sdk/common/ipcNames.ts diff --git a/src/common/lyricDecode.js b/sdk/common/lyricDecode.ts similarity index 93% rename from src/common/lyricDecode.js rename to sdk/common/lyricDecode.ts index 39f467d..242a886 100644 --- a/src/common/lyricDecode.js +++ b/sdk/common/lyricDecode.ts @@ -9,7 +9,7 @@ import iconv from 'iconv-lite' // ==================== 酷我 ==================== -const handleInflate = data => new Promise((resolve, reject) => { +const handleInflate = (data: Buffer): Promise => new Promise((resolve, reject) => { zlib.inflate(data, (err, result) => { if (err) return reject(err) resolve(result) @@ -53,7 +53,7 @@ export const handleKwDecodeLyric = async({ lrcBase64, isGetLyricx }) => { // ==================== QQ音乐(qrc) ==================== -import { decodeQrc } from './qrcDecode.js' +import { decodeQrc } from './qrcDecode.ts' /** * QQ音乐歌词解码 diff --git a/src/common/lyricUtils/kg.js b/sdk/common/lyricUtils/kg.ts similarity index 88% rename from src/common/lyricUtils/kg.js rename to sdk/common/lyricUtils/kg.ts index fb3e279..57d1d9f 100644 --- a/src/common/lyricUtils/kg.js +++ b/sdk/common/lyricUtils/kg.ts @@ -1,10 +1,10 @@ // 酷狗 krc 歌词解码 import zlib from 'node:zlib' -import { decodeName } from '../utils.js' +import { decodeName } from '../utils.ts' // 基于社区通用的 krc 解析思路 -const enc_key = Buffer.from([0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69], 'binary') +const enc_key = Buffer.from([0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69]) const decodeLyric = str => new Promise((resolve, reject) => { if (!str.length) return @@ -44,12 +44,12 @@ const parseLyric = str => { let i = 0 let yrc = str.replace(/\[((\d+),\d+)\].*/g, str => { let result = str.match(/\[((\d+),\d+)\].*/) - let time = parseInt(result[2]) + let time: any = parseInt(result[2]) let ms = time % 1000 time /= 1000 - let m = parseInt(time / 60).toString().padStart(2, '0') + let m = Math.trunc(time / 60).toString().padStart(2, '0') time %= 60 - let s = parseInt(time).toString().padStart(2, '0') + let s = Math.trunc(time).toString().padStart(2, '0') time = `${m}:${s}.${ms}` if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}` if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}` diff --git a/src/common/message.js b/sdk/common/message.ts similarity index 100% rename from src/common/message.js rename to sdk/common/message.ts diff --git a/src/common/qrcDecode.js b/sdk/common/qrcDecode.ts similarity index 99% rename from src/common/qrcDecode.js rename to sdk/common/qrcDecode.ts index ae5e817..8534bb0 100644 --- a/src/common/qrcDecode.js +++ b/sdk/common/qrcDecode.ts @@ -218,7 +218,7 @@ const QRC_KEY = Buffer.from([ 0x58, 0x43, 0x21, 0x40, 0x21, 0x40, 0x23, 0x29, 0x28, 0x4e, 0x48, 0x4c, ]) -const handleInflate = (data) => new Promise((resolve, reject) => { +const handleInflate = (data: Buffer): Promise => new Promise((resolve, reject) => { // Z_SYNC_FLUSH:容忍尾部不完整的 zlib 流(对应原实现忽略 Z_BUF_ERROR 的行为) inflate(data, { finishFlush: constants.Z_SYNC_FLUSH }, (err, result) => { if (err) reject(err) diff --git a/src/common/rendererIpc.js b/sdk/common/rendererIpc.ts similarity index 94% rename from src/common/rendererIpc.js rename to sdk/common/rendererIpc.ts index 7172012..61caac4 100644 --- a/src/common/rendererIpc.js +++ b/sdk/common/rendererIpc.ts @@ -3,7 +3,7 @@ // handle_kw_decode_lyric → 酷我歌词解码(见 lyricDecode.js) // handle_tx_decode_lyric → QQ音乐歌词解码(见 lyricDecode.js) -import { handleKwDecodeLyric, handleTxDecodeLyric } from './lyricDecode.js' +import { handleKwDecodeLyric, handleTxDecodeLyric } from './lyricDecode.ts' const localHandlers = { [WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric]: handleKwDecodeLyric, @@ -28,6 +28,6 @@ export const rendererOff = () => {} export const rendererOffAll = () => {} export const rendererSendSync = () => {} -import { WIN_MAIN_RENDERER_EVENT_NAME } from './ipcNames.js' +import { WIN_MAIN_RENDERER_EVENT_NAME } from './ipcNames.ts' export default { rendererInvoke, rendererSend, rendererOn, rendererOnce, rendererOff, rendererOffAll, rendererSendSync } diff --git a/src/common/request.js b/sdk/common/request.ts similarity index 74% rename from src/common/request.js rename to sdk/common/request.ts index b44e31a..71c4d30 100644 --- a/src/common/request.js +++ b/sdk/common/request.ts @@ -12,14 +12,43 @@ import http from 'node:http' import https from 'node:https' import zlib from 'node:zlib' import { URL } from 'node:url' +import { requestMsg } from './message.ts' const MAX_BUFFER_SIZE = 64 * 1024 * 1024 // 64MB +export interface HttpOptions { + method?: string + headers?: Record + body?: any + form?: Record + timeout?: number + lookup?: any + family?: number + [key: string]: any +} + +export interface HttpFetchResponse { + statusCode: number + statusMessage?: string + headers: Record + body: any + raw: Buffer +} + +export interface HttpRequestObject { + promise: Promise + cancelHttp(): void + isCancelled: boolean + requestObj?: any + cancelFn?: ((err: any) => void) | null + [key: string]: any +} + const defaultHeaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36', } -const parseBody = (raw) => { +const parseBody = (raw: Buffer): any => { if (!raw || !raw.length) return null const text = raw.toString('utf8') // 与原版一致:无论 content-type 都尝试 JSON 解析,失败则返回原始文本 @@ -33,9 +62,10 @@ const parseBody = (raw) => { /** * 发起请求(返回带 promise 与 cancelHttp 的对象) */ -const buildHttpPromise = (url, options = {}) => { +const buildHttpPromise = (url: string, options: HttpOptions = {}): HttpRequestObject => { const method = (options.method || 'GET').toUpperCase() - const obj = { + const obj: HttpRequestObject = { + promise: null as any, isCancelled: false, cancelHttp: () => { if (!obj.requestObj) { @@ -44,7 +74,8 @@ const buildHttpPromise = (url, options = {}) => { } obj.requestObj.destroy(new Error('Canceled')) obj.requestObj = null - obj.promise = obj.cancelHttp = null + // 取消后置空是运行时事实,但公开类型保持非空(消费方总是先取 promise 再取消) + ;(obj as any).promise = (obj as any).cancelHttp = null obj.cancelFn?.(new Error('Canceled')) obj.cancelFn = null }, @@ -52,7 +83,7 @@ const buildHttpPromise = (url, options = {}) => { obj.promise = new Promise((resolve, reject) => { obj.cancelFn = reject - let parsedUrl + let parsedUrl: URL try { parsedUrl = new URL(url) } catch (err) { @@ -64,7 +95,7 @@ const buildHttpPromise = (url, options = {}) => { const client = isHttps ? https : http // 数据体构造:body(对象/字符串/Buffer)与 form(对象 → urlencoded) - let body = null + let body: string | any = null let isForm = false if (options.form) { const formBody = new URLSearchParams() @@ -79,13 +110,13 @@ const buildHttpPromise = (url, options = {}) => { body = typeof options.body === 'object' && !Buffer.isBuffer(options.body) ? JSON.stringify(options.body) : options.body } - const headers = { ...defaultHeaders, ...(options.headers || {}) } + const headers: Record = { ...defaultHeaders, ...(options.headers || {}) } if (body && !headers['content-type'] && !headers['Content-Type']) { headers['content-type'] = isForm ? 'application/x-www-form-urlencoded' : 'application/json' } if (body) headers['content-length'] = Buffer.byteLength(body) - const requestOptions = { + const requestOptions: Record = { method, headers, family: options.family || undefined, @@ -95,16 +126,16 @@ const buildHttpPromise = (url, options = {}) => { const req = client.request(parsedUrl, requestOptions, res => { obj.requestObj = null obj.cancelFn = null - const statusCode = res.statusCode + const statusCode = res.statusCode ?? 0 - let stream = res + let stream: any = res switch (res.headers['content-encoding']) { case 'gzip': stream = res.pipe(zlib.createGunzip()); break case 'deflate': stream = res.pipe(zlib.createInflate()); break case 'br': stream = res.pipe(zlib.createBrotliDecompress()); break } - const chunks = [] + const chunks: Buffer[] = [] let size = 0 stream.on('data', chunk => { size += chunk.length @@ -129,13 +160,14 @@ const buildHttpPromise = (url, options = {}) => { obj.requestObj = req req.on('error', err => { - if (obj.isCancelled || err.canceled) err.canceled = true - reject(err) + const e = err as any + if (obj.isCancelled || e.canceled) e.canceled = true + reject(e) }) if (options.timeout) { req.setTimeout(options.timeout, () => { - const err = new Error('Request timeout') + const err = new Error('Request timeout') as any err.timeout = true req.destroy(err) }) @@ -150,9 +182,9 @@ const buildHttpPromise = (url, options = {}) => { /** * 请求超时/断网错误统一处理(与原版行为一致) */ -export const httpFetch = (url, options = { method: 'get' }) => { +export const httpFetch = (url: string, options: HttpOptions = { method: 'get' }): HttpRequestObject => { const requestObj = buildHttpPromise(url, options) - requestObj.promise = requestObj.promise.catch(err => { + requestObj.promise = requestObj.promise.catch((err: any) => { if (err.canceled) return Promise.reject(err) switch (err.code) { case 'ETIMEDOUT': @@ -169,6 +201,4 @@ export const httpFetch = (url, options = { method: 'get' }) => { return requestObj } -import { requestMsg } from './message.js' - export default { httpFetch } diff --git a/src/common/utils.js b/sdk/common/utils.ts similarity index 100% rename from src/common/utils.js rename to sdk/common/utils.ts diff --git a/musicSdk/index.ts b/sdk/index.ts similarity index 95% rename from musicSdk/index.ts rename to sdk/index.ts index 8d952fa..ccf9c6f 100644 --- a/musicSdk/index.ts +++ b/sdk/index.ts @@ -2,8 +2,6 @@ import kw from './kw/index.ts' import kg from './kg/index.ts' import tx from './tx/index.ts' import wy from './wy/index.ts' -import mg from './mg/index.ts' -import bd from './bd/index.ts' import { supportQuality } from './api-source.ts' import type { PlatformModule, SearchResult, SongInfo } from './types.ts' import { @@ -13,7 +11,7 @@ import { dateFormat2, formatPlayCount, decodeName, -} from '../src/index.js' +} from './common/utils.ts' // 通用工具函数(SDK 内 `import { ... } from '../index.ts'` 的解析目标) export { sizeFormate, formatPlayTime, dateFormat, dateFormat2, formatPlayCount, decodeName } @@ -24,9 +22,6 @@ const SUPPORTED_SOURCES: Array<{ id: string; name: string; module: PlatformModul { id: 'kg', name: '酷狗音乐', module: kg }, { id: 'tx', name: 'QQ音乐', module: tx }, { id: 'wy', name: '网易音乐', module: wy }, - { id: 'mg', name: '咪咕音乐', module: mg }, - // bd 取流接口可用但暂不列入平台列表(直连路由仍可访问 /url/bd/:id) - // { id: 'bd', name: '百度音乐', module: bd }, ] const SOURCE_MODULES = Object.fromEntries(SUPPORTED_SOURCES.map((s) => [s.id, s.module])) diff --git a/musicSdk/kg/album.ts b/sdk/kg/album.ts similarity index 100% rename from musicSdk/kg/album.ts rename to sdk/kg/album.ts diff --git a/musicSdk/kg/comment.ts b/sdk/kg/comment.ts similarity index 98% rename from musicSdk/kg/comment.ts rename to sdk/kg/comment.ts index d80f712..15297c4 100644 --- a/musicSdk/kg/comment.ts +++ b/sdk/kg/comment.ts @@ -1,7 +1,7 @@ import { httpFetch } from '../request.ts' import { decodeName, dateFormat2 } from '../index.ts' import { signatureParams } from './util.ts' -// import { getMusicInfoRaw } from './musicInfo.js' +// import { getMusicInfoRaw } from './musicInfo.ts' import type { HttpRequestObject } from '../types.ts' export default { diff --git a/musicSdk/kg/hotSearch.ts b/sdk/kg/hotSearch.ts similarity index 100% rename from musicSdk/kg/hotSearch.ts rename to sdk/kg/hotSearch.ts diff --git a/musicSdk/kg/index.ts b/sdk/kg/index.ts similarity index 95% rename from musicSdk/kg/index.ts rename to sdk/kg/index.ts index b53bac5..132ae89 100644 --- a/musicSdk/kg/index.ts +++ b/sdk/kg/index.ts @@ -8,7 +8,7 @@ import hotSearch from './hotSearch.ts' import comment from './comment.ts' import singer from './singer.ts' import album from './album.ts' -// import tipSearch from './tipSearch.js' +// import tipSearch from './tipSearch.ts' const kg = { // tipSearch, diff --git a/musicSdk/kg/leaderboard.ts b/sdk/kg/leaderboard.ts similarity index 100% rename from musicSdk/kg/leaderboard.ts rename to sdk/kg/leaderboard.ts diff --git a/musicSdk/kg/lyric.ts b/sdk/kg/lyric.ts similarity index 98% rename from musicSdk/kg/lyric.ts rename to sdk/kg/lyric.ts index ba61cb8..2f2ee1e 100644 --- a/musicSdk/kg/lyric.ts +++ b/sdk/kg/lyric.ts @@ -1,5 +1,5 @@ import { httpFetch } from '../request.ts' -import { decodeKrc } from '../../src/common/lyricUtils/kg.js' +import { decodeKrc } from '../common/lyricUtils/kg.ts' export default { getIntv(interval) { diff --git a/musicSdk/kg/musicInfo.ts b/sdk/kg/musicInfo.ts similarity index 100% rename from musicSdk/kg/musicInfo.ts rename to sdk/kg/musicInfo.ts diff --git a/musicSdk/kg/musicSearch.ts b/sdk/kg/musicSearch.ts similarity index 100% rename from musicSdk/kg/musicSearch.ts rename to sdk/kg/musicSearch.ts diff --git a/musicSdk/kg/pic.ts b/sdk/kg/pic.ts similarity index 100% rename from musicSdk/kg/pic.ts rename to sdk/kg/pic.ts diff --git a/musicSdk/kg/singer.ts b/sdk/kg/singer.ts similarity index 100% rename from musicSdk/kg/singer.ts rename to sdk/kg/singer.ts diff --git a/musicSdk/kg/songList.ts b/sdk/kg/songList.ts similarity index 100% rename from musicSdk/kg/songList.ts rename to sdk/kg/songList.ts diff --git a/musicSdk/kg/tipSearch.ts b/sdk/kg/tipSearch.ts similarity index 100% rename from musicSdk/kg/tipSearch.ts rename to sdk/kg/tipSearch.ts diff --git a/musicSdk/kg/util.ts b/sdk/kg/util.ts similarity index 100% rename from musicSdk/kg/util.ts rename to sdk/kg/util.ts diff --git a/musicSdk/kw/album.ts b/sdk/kw/album.ts similarity index 100% rename from musicSdk/kw/album.ts rename to sdk/kw/album.ts diff --git a/musicSdk/kw/comment.ts b/sdk/kw/comment.ts similarity index 100% rename from musicSdk/kw/comment.ts rename to sdk/kw/comment.ts diff --git a/musicSdk/kw/hotSearch.ts b/sdk/kw/hotSearch.ts similarity index 100% rename from musicSdk/kw/hotSearch.ts rename to sdk/kw/hotSearch.ts diff --git a/musicSdk/kw/index.ts b/sdk/kw/index.ts similarity index 100% rename from musicSdk/kw/index.ts rename to sdk/kw/index.ts diff --git a/musicSdk/kw/leaderboard.ts b/sdk/kw/leaderboard.ts similarity index 100% rename from musicSdk/kw/leaderboard.ts rename to sdk/kw/leaderboard.ts diff --git a/musicSdk/kw/lyric.ts b/sdk/kw/lyric.ts similarity index 100% rename from musicSdk/kw/lyric.ts rename to sdk/kw/lyric.ts diff --git a/musicSdk/kw/musicSearch.ts b/sdk/kw/musicSearch.ts similarity index 98% rename from musicSdk/kw/musicSearch.ts rename to sdk/kw/musicSearch.ts index 139b130..ee938e8 100644 --- a/musicSdk/kw/musicSearch.ts +++ b/sdk/kw/musicSearch.ts @@ -2,7 +2,7 @@ import { httpFetch } from '../request.ts' import { formatPlayTime, decodeName } from '../index.ts' -// import { debug } from '../../utils/env.js' +// import { debug } from '../../utils/env.ts' import { formatSinger } from './util.ts' import type { Quality, QualityInfo, SongInfo, SongType } from '../types.ts' diff --git a/musicSdk/kw/pic.ts b/sdk/kw/pic.ts similarity index 100% rename from musicSdk/kw/pic.ts rename to sdk/kw/pic.ts diff --git a/musicSdk/kw/songList.ts b/sdk/kw/songList.ts similarity index 100% rename from musicSdk/kw/songList.ts rename to sdk/kw/songList.ts diff --git a/musicSdk/kw/tipSearch.ts b/sdk/kw/tipSearch.ts similarity index 100% rename from musicSdk/kw/tipSearch.ts rename to sdk/kw/tipSearch.ts diff --git a/musicSdk/kw/util.ts b/sdk/kw/util.ts similarity index 98% rename from musicSdk/kw/util.ts rename to sdk/kw/util.ts index 482559a..d50065b 100644 --- a/musicSdk/kw/util.ts +++ b/sdk/kw/util.ts @@ -1,6 +1,6 @@ // import { httpGet, httpFetch } from '../request.ts' -import { WIN_MAIN_RENDERER_EVENT_NAME } from '../../src/common/ipcNames.js' -import { rendererInvoke } from '../../src/common/rendererIpc.js' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '../common/ipcNames.ts' +import { rendererInvoke } from '../common/rendererIpc.ts' import { createCipheriv, createDecipheriv } from 'crypto' import { toMD5 } from '../utils.ts' diff --git a/sdk/request.ts b/sdk/request.ts new file mode 100644 index 0000000..469c397 --- /dev/null +++ b/sdk/request.ts @@ -0,0 +1,5 @@ +// SDK 根请求模块 —— 唯一实现在 common/request.ts,此处仅转发 +// sdk 内 `import { ... } from '../request.ts'` 的解析目标 + +export { httpFetch } from './common/request.ts' +export type { HttpOptions, HttpRequestObject, HttpFetchResponse } from './common/request.ts' diff --git a/musicSdk/tx/album.ts b/sdk/tx/album.ts similarity index 100% rename from musicSdk/tx/album.ts rename to sdk/tx/album.ts diff --git a/musicSdk/tx/comment.ts b/sdk/tx/comment.ts similarity index 100% rename from musicSdk/tx/comment.ts rename to sdk/tx/comment.ts diff --git a/musicSdk/tx/hotSearch.ts b/sdk/tx/hotSearch.ts similarity index 100% rename from musicSdk/tx/hotSearch.ts rename to sdk/tx/hotSearch.ts diff --git a/musicSdk/tx/index.ts b/sdk/tx/index.ts similarity index 100% rename from musicSdk/tx/index.ts rename to sdk/tx/index.ts diff --git a/musicSdk/tx/leaderboard.ts b/sdk/tx/leaderboard.ts similarity index 100% rename from musicSdk/tx/leaderboard.ts rename to sdk/tx/leaderboard.ts diff --git a/musicSdk/tx/lyric.ts b/sdk/tx/lyric.ts similarity index 96% rename from musicSdk/tx/lyric.ts rename to sdk/tx/lyric.ts index 1367935..8d4f2f9 100644 --- a/musicSdk/tx/lyric.ts +++ b/sdk/tx/lyric.ts @@ -1,12 +1,12 @@ import type { HttpRequestObject } from '../types.ts' import { httpFetch } from '../request.ts' import getMusicInfo from './musicInfo.ts' -import { rendererInvoke } from '../../src/common/rendererIpc.ts' -import { WIN_MAIN_RENDERER_EVENT_NAME } from '../../src/common/ipcNames.ts' +import { rendererInvoke } from '../common/rendererIpc.ts' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '../common/ipcNames.ts' const songIdMap = new Map() const promises = new Map() -export const decodeLyric = (lrc, tlrc, rlrc) => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, { lrc, tlrc, rlrc }) +export const decodeLyric = (lrc: any, tlrc?: any, rlrc?: any): Promise => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, { lrc, tlrc, rlrc }) const parseTools = { diff --git a/musicSdk/tx/musicInfo.ts b/sdk/tx/musicInfo.ts similarity index 100% rename from musicSdk/tx/musicInfo.ts rename to sdk/tx/musicInfo.ts diff --git a/musicSdk/tx/musicSearch.ts b/sdk/tx/musicSearch.ts similarity index 100% rename from musicSdk/tx/musicSearch.ts rename to sdk/tx/musicSearch.ts diff --git a/musicSdk/tx/singer.ts b/sdk/tx/singer.ts similarity index 100% rename from musicSdk/tx/singer.ts rename to sdk/tx/singer.ts diff --git a/musicSdk/tx/songList.ts b/sdk/tx/songList.ts similarity index 100% rename from musicSdk/tx/songList.ts rename to sdk/tx/songList.ts diff --git a/musicSdk/tx/tipSearch.ts b/sdk/tx/tipSearch.ts similarity index 100% rename from musicSdk/tx/tipSearch.ts rename to sdk/tx/tipSearch.ts diff --git a/musicSdk/tx/utils/crypto.ts b/sdk/tx/utils/crypto.ts similarity index 100% rename from musicSdk/tx/utils/crypto.ts rename to sdk/tx/utils/crypto.ts diff --git a/musicSdk/tx/utils/index.ts b/sdk/tx/utils/index.ts similarity index 100% rename from musicSdk/tx/utils/index.ts rename to sdk/tx/utils/index.ts diff --git a/musicSdk/types.ts b/sdk/types.ts similarity index 95% rename from musicSdk/types.ts rename to sdk/types.ts index ac956c4..a4121a1 100644 --- a/musicSdk/types.ts +++ b/sdk/types.ts @@ -2,7 +2,7 @@ // 所有平台模块与根模块的类型锚点。类型只描述实际返回的数据形状,不改变运行时行为。 /** 音乐平台标识 */ -export type Source = 'kw' | 'kg' | 'tx' | 'wy' | 'mg' | 'bd' +export type Source = 'kw' | 'kg' | 'tx' | 'wy' /** 音质标识(wav 仅部分接口声明使用) */ export type Quality = '128k' | '320k' | 'flac' | 'flac24bit' | 'wav' @@ -39,7 +39,7 @@ export interface SongInfo { [key: string]: unknown } -/** httpFetch 响应(与 src/common/request.js 契约一致) */ +/** httpFetch 响应(与 common/request.ts 契约一致) */ export interface HttpResponse { statusCode: number statusMessage?: string @@ -54,7 +54,7 @@ export interface HttpRequestObject { cancelHttp(): void } -/** httpFetch 选项(src/common/request.js 子集) */ +/** httpFetch 选项(common/request.ts 子集) */ export interface HttpOptions { method?: string headers?: Record diff --git a/musicSdk/utils.ts b/sdk/utils.ts similarity index 96% rename from musicSdk/utils.ts rename to sdk/utils.ts index 31cc0d3..4e8744c 100644 --- a/musicSdk/utils.ts +++ b/sdk/utils.ts @@ -1,6 +1,6 @@ import crypto from 'crypto' import dns from 'dns' -import { decodeName } from '../src/common/utils.js' +import { decodeName } from './common/utils.ts' export const toMD5 = (str: string): string => crypto.createHash('md5').update(str).digest('hex') diff --git a/musicSdk/wy/comment.ts b/sdk/wy/comment.ts similarity index 100% rename from musicSdk/wy/comment.ts rename to sdk/wy/comment.ts diff --git a/musicSdk/wy/hotSearch.ts b/sdk/wy/hotSearch.ts similarity index 100% rename from musicSdk/wy/hotSearch.ts rename to sdk/wy/hotSearch.ts diff --git a/musicSdk/wy/index.ts b/sdk/wy/index.ts similarity index 95% rename from musicSdk/wy/index.ts rename to sdk/wy/index.ts index f208edf..74eeab5 100644 --- a/musicSdk/wy/index.ts +++ b/sdk/wy/index.ts @@ -7,7 +7,7 @@ import songList from './songList.ts' import hotSearch from './hotSearch.ts' import comment from './comment.ts' import singer from './singer.ts' -// import tipSearch from './tipSearch.js' +// import tipSearch from './tipSearch.ts' const wy = { // tipSearch, diff --git a/musicSdk/wy/leaderboard.ts b/sdk/wy/leaderboard.ts similarity index 100% rename from musicSdk/wy/leaderboard.ts rename to sdk/wy/leaderboard.ts diff --git a/musicSdk/wy/lyric.ts b/sdk/wy/lyric.ts similarity index 99% rename from musicSdk/wy/lyric.ts rename to sdk/wy/lyric.ts index 0ac29cb..1007a6f 100644 --- a/musicSdk/wy/lyric.ts +++ b/sdk/wy/lyric.ts @@ -1,7 +1,7 @@ import { httpFetch } from '../request.ts' import { eapi } from './utils/crypto.ts' import type { LyricInfo, HttpRequestObject } from '../types.ts' -// import { decodeName } from '../...js' +// import { decodeName } from '../...ts' // const parseLyric = (str, lrc) => { // if (!str) return '' diff --git a/musicSdk/wy/musicDetail.ts b/sdk/wy/musicDetail.ts similarity index 100% rename from musicSdk/wy/musicDetail.ts rename to sdk/wy/musicDetail.ts diff --git a/musicSdk/wy/musicInfo.ts b/sdk/wy/musicInfo.ts similarity index 100% rename from musicSdk/wy/musicInfo.ts rename to sdk/wy/musicInfo.ts diff --git a/musicSdk/wy/musicSearch.ts b/sdk/wy/musicSearch.ts similarity index 95% rename from musicSdk/wy/musicSearch.ts rename to sdk/wy/musicSearch.ts index 8e9ca8a..7664d5a 100644 --- a/musicSdk/wy/musicSearch.ts +++ b/sdk/wy/musicSearch.ts @@ -1,7 +1,7 @@ -// import { httpFetch } from '../request.js' -// import { weapi } from './utils/crypto.js' +// import { httpFetch } from '../request.ts' +// import { weapi } from './utils/crypto.ts' import { sizeFormate, formatPlayTime } from '../index.ts' -// import musicDetailApi from './musicDetail.js' +// import musicDetailApi from './musicDetail.ts' import { eapiRequest } from './utils/index.ts' export default { diff --git a/musicSdk/wy/singer.ts b/sdk/wy/singer.ts similarity index 100% rename from musicSdk/wy/singer.ts rename to sdk/wy/singer.ts diff --git a/musicSdk/wy/songList.ts b/sdk/wy/songList.ts similarity index 100% rename from musicSdk/wy/songList.ts rename to sdk/wy/songList.ts diff --git a/musicSdk/wy/tipSearch.ts b/sdk/wy/tipSearch.ts similarity index 100% rename from musicSdk/wy/tipSearch.ts rename to sdk/wy/tipSearch.ts diff --git a/musicSdk/wy/utils/crypto.ts b/sdk/wy/utils/crypto.ts similarity index 100% rename from musicSdk/wy/utils/crypto.ts rename to sdk/wy/utils/crypto.ts diff --git a/musicSdk/wy/utils/index.ts b/sdk/wy/utils/index.ts similarity index 94% rename from musicSdk/wy/utils/index.ts rename to sdk/wy/utils/index.ts index ec91b07..30b0f32 100644 --- a/musicSdk/wy/utils/index.ts +++ b/sdk/wy/utils/index.ts @@ -1,6 +1,6 @@ import { httpFetch } from '../../request.ts' import { eapi } from './crypto.ts' -import { getWyCookie } from '../../../src/auth/store.ts' +import { getWyCookie } from '../../auth/store.ts' import type { HttpRequestObject } from '../../types.ts' export const eapiRequest = (url: string, data: object): HttpRequestObject => { diff --git a/src/apis/bd.js b/src/apis/bd.js deleted file mode 100644 index e823190..0000000 --- a/src/apis/bd.js +++ /dev/null @@ -1,37 +0,0 @@ -// 百度音乐 URL 获取(直连) -// 接口: https://musicapi.qianqian.com/v1/restserver/ting?method=baidu.ting.song.getSongLink -// 返回: result.songinfo.song_link -// 统一接口约定:apis('bd').getMusicUrl(songInfo, type) → { promise, cancelHttp } -// promise resolve → { type, url } - -import { httpFetch } from '../common/request.js' - -const getSongLink = (songmid, retryNum = 0) => { - const url = `https://musicapi.qianqian.com/v1/restserver/ting?method=baidu.ting.song.getSongLink&format=json&from=bmpc&version=1.0.0&version_d=11.1.6.0&songid=${songmid}&type=1&res=1&s_protocol=1&aac=2&project=tpass` - return httpFetch(url, { - method: 'get', - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - }, - timeout: 15000, - }).promise.then(({ statusCode, body }) => { - if (statusCode != 200 || body.error_code != 22000 || !body.result?.songinfo) { - if (retryNum < 2) return getSongLink(songmid, ++retryNum) - return Promise.reject(new Error('获取失败')) - } - return body.result.songinfo - }) -} - -export const getMusicUrl = (songInfo, type) => { - const requestObj = { cancelHttp() {}, promise: null } - requestObj.promise = getSongLink(songInfo.songmid).then(info => { - // song_link 为试听地址(128k),按音质选择字段 - const url = info.song_link || info.file_link - if (!url) return Promise.reject(new Error('获取失败')) - return { type, url } - }) - return requestObj -} - -export default { getMusicUrl } diff --git a/src/apis/index.js b/src/apis/index.js deleted file mode 100644 index 1e99386..0000000 --- a/src/apis/index.js +++ /dev/null @@ -1,17 +0,0 @@ -// 各平台 URL 直连接口汇总 -// 服务器环境固定使用内置直连接口(apiSource.value === 'internal') -// 外部聚合音源(sources/ 目录脚本)不走这里,由 musicSdk/api-source.ts 直接接入 source.js -import kw from './kw.js'; -import kg from './kg.js'; -import tx from './tx.js'; -import wy from './wy.js'; -import mg from './mg.js'; -import bd from './bd.js'; -export default { - kw: kw, - kg: kg, - tx: tx, - wy: wy, - mg: mg, - bd: bd, -}; diff --git a/src/apis/mg.js b/src/apis/mg.js deleted file mode 100644 index 580f430..0000000 --- a/src/apis/mg.js +++ /dev/null @@ -1,84 +0,0 @@ -// 咪咕音乐 URL 获取(直连) -// 流程: resourceinfo.do(拿 contentId,需签名)→ listenSong.do(302 跳转到 CDN 音频地址) -// 统一接口约定:apis('mg').getMusicUrl(songInfo, type) → { promise, cancelHttp } -// promise resolve → { type, url } - -import { httpFetch } from '../common/request.js' -import { toMD5 } from '../common/utils.js' - -const rateMap = { - '128k': 'PQ', - '320k': 'HQ', - flac: 'SQ', - flac24bit: 'ZQ24', -} - -const deviceId = '963B7AA0D21511ED807EE5846EC87D20' -const signatureMd5 = '6cdc72a439cef99a3418d2a78aa28c73' - -const createSignature = (time, str) => { - const sign = toMD5(`${str}${signatureMd5}yyapp2d16148780a1dcc7408e06336b98cfd50${deviceId}${time}`) - return { sign, deviceId } -} - -/** - * 通过 songId 获取歌曲内容信息(contentId) - */ -const getResourceInfo = songmid => { - const time = Date.now().toString() - const signData = createSignature(time, songmid) - return httpFetch(`http://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceId=${songmid}&resourceType=2`, { - method: 'post', - headers: { - 'User-Agent': 'Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36', - timestamp: time, - sign: signData.sign, - deviceId: signData.deviceId, - appId: 'yyapp2', - channel: '0146921', - }, - timeout: 15000, - }).promise.then(({ statusCode, body }) => { - if (statusCode != 200 || body.code != '000000' || !body.resource?.length) { - return Promise.reject(new Error('获取失败')) - } - return body.resource[0] - }) -} - -/** - * 获取播放地址(302 跳转) - * @param {string} contentId - * @param {string} copyrightId - * @param {string} rateType PQ / HQ / SQ - */ -const getSongUrl = (contentId, copyrightId, rateType, retryNum = 0) => { - if (retryNum > 2) return Promise.reject(new Error('获取失败')) - const url = `https://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/sub/listenSong.do?contentId=${contentId}©rightId=${copyrightId}&resourceType=2&rateType=${rateType}&channel=0146921&toneFlag=${rateType}&netType=00&version=5.0.1` - return httpFetch(url, { - method: 'get', - headers: { - 'User-Agent': 'Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36', - Referer: 'http://music.migu.cn/', - }, - timeout: 15000, - }).promise.then(({ statusCode, headers }) => { - if (statusCode == 302 && headers.location) return headers.location - if (statusCode == 302 && !headers.location) { - // 手动跟随 - return Promise.reject(new Error('获取失败')) - } - return getSongUrl(contentId, copyrightId, rateType, ++retryNum) - }) -} - -export const getMusicUrl = (songInfo, type) => { - const requestObj = { cancelHttp() {}, promise: null } - const rateType = rateMap[type] || 'PQ' - requestObj.promise = getResourceInfo(songInfo.songmid || songInfo.copyrightId) - .then(resource => getSongUrl(resource.contentId, resource.copyrightId, rateType)) - .then(url => ({ type, url })) - return requestObj -} - -export default { getMusicUrl } diff --git a/src/apis/tx.js b/src/apis/tx.js deleted file mode 100644 index e4a4476..0000000 --- a/src/apis/tx.js +++ /dev/null @@ -1,55 +0,0 @@ -// QQ音乐 URL 获取(直连) -// 接口: https://u.y.qq.com/cgi-bin/musicu.fcg -// 方法: vkey.GetVkeyServer.CgiGetVkey -// 统一接口约定:apis('tx').getMusicUrl(songInfo, type) → { promise, cancelHttp } -// promise resolve → { type, url } - -import { httpFetch } from '../common/request.js' - -const buildData = songmid => ({ - comm: { - ct: '19', - cv: '1859', - uin: '0', - }, - req: { - module: 'vkey.GetVkeyServer', - method: 'CgiGetVkey', - param: { - guid: '7342827269', - songmid: [songmid], - songtype: [0], - uin: '0', - loginflag: 1, - platform: '20', - needNewCode: 1, - }, - }, -}) - -export const getMusicUrl = (songInfo, type) => { - const requestObj = { cancelHttp() {}, promise: null } - requestObj.promise = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { - method: 'post', - headers: { - 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', - Referer: 'https://y.qq.com/', - }, - body: buildData(songInfo.songmid), - timeout: 15000, - }).promise.then(({ statusCode, body }) => { - if (statusCode != 200 || body.code != 0 || body.req?.code != 0) { - return Promise.reject(new Error('获取失败')) - } - const data = body.req.data - const sip = data.sip?.[0] || 'http://aqqmusic.tc.qq.com/' - // 格式: C400{media_mid}.m4a?guid=&vkey=&uin=&fromtag= - const params = data.testfile2g?.split('?')[1] || '' - const url = `${sip}C400${songInfo.songmid}.m4a?${params}` - if (!params) return Promise.reject(new Error('获取失败')) - return { type, url } - }) - return requestObj -} - -export default { getMusicUrl } diff --git a/src/auth/store.js b/src/auth/store.js deleted file mode 100644 index ef630b0..0000000 --- a/src/auth/store.js +++ /dev/null @@ -1,45 +0,0 @@ -// 登录态存储 —— 各平台凭据持久化(data/auth.json,不入库) -// 结构:{ wy: {...}, kg: {...}, _kg_device: {...} } -// 注意:本文件经 tsc 就地发射 store.js 供纯 JS 模块引用(与 src/apis/index.ts 同约定) -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -const DATA_FILE = join(dirname(fileURLToPath(import.meta.url)), '../../data/auth.json'); -let cache = {}; -let loaded = false; -const load = () => { - if (loaded) - return cache; - try { - cache = existsSync(DATA_FILE) ? JSON.parse(readFileSync(DATA_FILE, 'utf8')) : {}; - } - catch { - cache = {}; - } - loaded = true; - return cache; -}; -const save = (data) => { - cache = data; - loaded = true; - mkdirSync(dirname(DATA_FILE), { recursive: true }); - writeFileSync(DATA_FILE, JSON.stringify(data, null, 2)); -}; -export const getCredentials = (platform) => load()[platform] ?? null; -export const setCredentials = (platform, credentials) => { - const data = load(); - data[platform] = { ...data[platform], ...credentials, updatedAt: new Date().toISOString() }; - save(data); -}; -export const clearCredentials = (platform) => { - const data = load(); - delete data[platform]; - save(data); -}; -/** wy 登录 cookie 字符串(未登录返回空串) */ -export const getWyCookie = () => getCredentials('wy')?.cookie || ''; -/** kg 登录态(未登录返回 null) */ -export const getKgAuth = () => { - const cred = getCredentials('kg'); - return cred?.token ? { token: cred.token, userid: cred.userid || 0 } : null; -}; diff --git a/src/http/api-error.js b/src/http/api-error.js deleted file mode 100644 index c40d2f7..0000000 --- a/src/http/api-error.js +++ /dev/null @@ -1,22 +0,0 @@ -// API 业务错误 —— 路由处理中抛出,由服务器统一转换为对应 HTTP 状态码的 JSON 响应 - -export class ApiError extends Error { - /** - * @param {number} status HTTP 状态码(同时作为响应体中的 code) - * @param {string} msg 错误描述 - * @param {unknown} [detail] 可选的结构化错误详情 - */ - constructor(status, msg, detail) { - super(msg) - this.name = 'ApiError' - this.status = status - this.code = status - this.detail = detail - } -} - -/** 资源不存在(未知路由 / 不支持的平台) */ -export const notFound = (msg, detail) => new ApiError(404, msg, detail) - -/** 上游平台请求失败 */ -export const upstream = (msg, detail) => new ApiError(502, msg, detail) diff --git a/src/http/api-error.ts b/src/http/api-error.ts new file mode 100644 index 0000000..53a03ad --- /dev/null +++ b/src/http/api-error.ts @@ -0,0 +1,24 @@ +// API 业务错误 —— 路由处理中抛出,由服务器统一转换为对应 HTTP 状态码的 JSON 响应 + +export class ApiError extends Error { + /** HTTP 状态码(同时作为响应体中的 code) */ + status: number + /** 与 status 同值,历史别名 */ + code: number + /** 可选的结构化错误详情 */ + detail?: unknown + + constructor(status: number, msg: string, detail?: unknown) { + super(msg) + this.name = 'ApiError' + this.status = status + this.code = status + this.detail = detail + } +} + +/** 资源不存在(未知路由 / 不支持的平台) */ +export const notFound = (msg: string, detail?: unknown) => new ApiError(404, msg, detail) + +/** 上游平台请求失败 */ +export const upstream = (msg: string, detail?: unknown) => new ApiError(502, msg, detail) diff --git a/src/index.js b/src/index.js deleted file mode 100644 index ac178a6..0000000 --- a/src/index.js +++ /dev/null @@ -1,11 +0,0 @@ -// SDK 根模块 —— musicSdk 内 `import xxx from '../../index.js'` 的解析目标 -// 聚合导出通用工具函数 - -export { - sizeFormate, - formatPlayTime, - dateFormat, - dateFormat2, - formatPlayCount, - decodeName, -} from './common/utils.js' diff --git a/src/openapi/components.js b/src/openapi/components.ts similarity index 99% rename from src/openapi/components.js rename to src/openapi/components.ts index a1dc398..6eb8bb3 100644 --- a/src/openapi/components.js +++ b/src/openapi/components.ts @@ -33,7 +33,7 @@ export const COMPONENT_SCHEMAS = { properties: { songmid: { type: 'string', description: '平台歌曲 id(kg 平台为 hash)' }, hash: { type: 'string', description: 'kg 平台的歌曲 hash' }, - copyrightId: { type: 'string', description: 'mg 平台的版权 id' }, + copyrightId: { type: 'string', description: '平台版权 id(原 mg 平台使用)' }, albumId: { type: 'string' }, name: { type: 'string', description: '歌名' }, singer: { type: 'string', description: '歌手' }, diff --git a/src/openapi/ui.js b/src/openapi/ui.ts similarity index 100% rename from src/openapi/ui.js rename to src/openapi/ui.ts diff --git a/src/renderer/store.js b/src/renderer/store.js deleted file mode 100644 index 6a7efe0..0000000 --- a/src/renderer/store.js +++ /dev/null @@ -1,15 +0,0 @@ -// 替代原渲染器的 @renderer/store 模块(保留导入别名所需的接口形态) -// 服务器环境默认使用内置直连接口(apiSource.value === 'internal'), -// 设置 SOURCE_ENABLED 环境变量(任意非 0/false 值)即启用外部聚合音源。 - -const enabled = process.env.SOURCE_ENABLED - -export const apiSource = { - value: enabled && enabled !== '0' && enabled !== 'false' ? 'source' : 'internal', -} - -export const userApi = { - apis: {}, -} - -export default { apiSource, userApi } diff --git a/src/routes-login.js b/src/routes-login.ts similarity index 72% rename from src/routes-login.js rename to src/routes-login.ts index 6095633..23f15e2 100644 --- a/src/routes-login.js +++ b/src/routes-login.ts @@ -1,9 +1,9 @@ // 登录路由 —— wy 扫码/手机号登录,kg token 绑定,凭据持久化于 data/auth.json // 注意:成功响应返回裸数据对象,全局 preSerialization 钩子会统一包成 { code: 0, data } -import * as wyAuth from './auth/wy.js' -import * as kgAuth from './auth/kg.js' -import { clearCredentials, getCredentials } from './auth/store.js' +import * as wyAuth from '../sdk/auth/wy.ts' +import * as kgAuth from '../sdk/auth/kg.ts' +import { clearCredentials, getCredentials, getTxAuth, setCredentials } from '../sdk/auth/store.ts' const fail = (reply, err) => { const status = err.status ?? 502 @@ -102,19 +102,53 @@ export const loginRoutes = async(fastify) => { return { logged: false } }) + // ---------- QQ音乐 ---------- + + fastify.post('/login/tx', { + schema: { + summary: '绑定 QQ 音乐 cookie', + description: 'cookie 从已登录的 y.qq.com 浏览器请求头复制(需含 uin 与 qm_keyst 字段),用于解锁 tx 取流的 320k/无损音质。', + tags: ['登录'], + body: { + type: 'object', + properties: { + cookie: { type: 'string' }, + }, + required: ['cookie'], + }, + }, + }, async(request, reply) => { + const cookie = request.body.cookie + const uin = cookie.match(/uin=(\d+)/)?.[1] + if (!uin || !/qm_keyst=\S+/.test(cookie)) { + return reply.code(422).send({ code: 422, msg: 'cookie 需包含 uin 与 qm_keyst 字段' }) + } + setCredentials('tx', { cookie }) + return { logged: true, uin } + }) + + fastify.delete('/login/tx', { + schema: { summary: '退出 QQ 音乐登录', tags: ['登录'] }, + }, async() => { + clearCredentials('tx') + return { logged: false } + }) + // ---------- 通用 ---------- fastify.get('/login/status', { schema: { summary: '查看登录状态', - description: 'wy 为实时校验(未登录或 cookie 失效返回 logged: false),kg 为已存凭据。', + description: 'wy 为实时校验(未登录或 cookie 失效返回 logged: false),kg/tx 为已存凭据。', tags: ['登录'], }, }, async() => { const [wy, kg] = await Promise.all([wyAuth.status().catch(() => null), Promise.resolve(kgAuth.status())]) + const tx = getTxAuth() return { wy: wy ? { logged: true, ...wy } : { logged: false }, kg: kg ? { logged: true, ...kg } : { logged: false }, + tx: tx ? { logged: true, uin: tx.uin } : { logged: false }, } }) } diff --git a/src/routes.js b/src/routes.ts similarity index 93% rename from src/routes.js rename to src/routes.ts index 96b89fc..bc0b883 100644 --- a/src/routes.js +++ b/src/routes.ts @@ -3,18 +3,18 @@ // 处理函数返回普通对象 → 由服务器的 preSerialization 钩子包成 { code: 0, data } // 抛出 ApiError → 由全局 setErrorHandler 转为对应 HTTP 状态码 -import { notFound, upstream } from './http/api-error.js' +import { notFound, upstream } from './http/api-error.ts' const QUALITIES = ['128k', '320k', 'flac', 'flac24bit'] /** 统一解包 SDK 返回值:{ promise, cancelHttp } 结构或裸 Promise 兼容 */ -const unwrap = (result) => ( +const unwrap = (result: any): any => ( result && typeof result.promise !== 'undefined' && typeof result.cancelHttp === 'function' ? result.promise : result ) -const envelope = (dataRef) => ({ +const envelope = (dataRef?: string) => ({ type: 'object', properties: { code: { type: 'integer', description: '0 表示成功', example: 0 }, @@ -24,7 +24,7 @@ const envelope = (dataRef) => ({ }) /** 组装路由的 schema.response:200 信封 + 通用错误码文档 */ -const responses = (dataRef, { notFoundDoc = true, upstreamDoc = true } = {}) => ({ +const responses = (dataRef: any, { notFoundDoc = true, upstreamDoc = true }: { notFoundDoc?: boolean; upstreamDoc?: boolean } = {}) => ({ '200': envelope(dataRef), '422': { $ref: 'ErrorEnvelope#' }, ...(notFoundDoc ? { '404': { $ref: 'ErrorEnvelope#' } } : {}), @@ -43,10 +43,10 @@ const pLimit = { type: 'integer', default: 20, minimum: 1, maximum: 100, descrip const pQuality = { type: 'string', enum: QUALITIES, default: '128k', description: '音质' } const pId = { type: 'string', description: '平台歌曲 id', example: '28423286' } -const qs = (properties, required = []) => ({ type: 'object', properties, required }) -const params = (properties, required) => qs(properties, required ?? Object.keys(properties)) +const qs = (properties: any, required: string[] = []) => ({ type: 'object', properties, required }) +const params = (properties: any, required?: string[]) => qs(properties, required ?? Object.keys(properties)) -const getSource = (musicSdk, name, capability) => { +const getSource = (musicSdk: any, name: string, capability?: string) => { const m = musicSdk[name] if (!m || (capability && !m[capability])) throw notFound(`不支持的平台: ${name}`) return m @@ -56,7 +56,7 @@ const getSource = (musicSdk, name, capability) => { * 注册全部业务路由 * @param {import('fastify').FastifyInstance} fastify */ -export const musicRoutes = async(fastify) => { +export const musicRoutes = async(fastify: any) => { const musicSdk = fastify.musicSdk // ---------- 搜索 ---------- @@ -86,7 +86,7 @@ export const musicRoutes = async(fastify) => { getSource(musicSdk, source, 'musicSearch') try { return { source, list: await doSearch(source), page, limit } - } catch (err) { + } catch (err: any) { throw upstream(err.message) } } @@ -119,7 +119,7 @@ export const musicRoutes = async(fastify) => { return { list: await musicSdk.findMusic({ name, singer, albumName, interval, source: excludeSource }), } - } catch (err) { + } catch (err: any) { throw upstream(err.message) } }) @@ -134,7 +134,7 @@ export const musicRoutes = async(fastify) => { let result try { result = unwrap(m.getMusicUrl(songInfo, quality)) - } catch (err) { + } catch (err: any) { throw upstream(err.message) } return result.then(res => { @@ -148,7 +148,7 @@ export const musicRoutes = async(fastify) => { fastify.get('/url/:source/:id', { schema: { summary: '获取播放地址', - description: 'id 为平台歌曲 id:kw/tx/wy/mg 用 songmid,kg 用 hash。', + description: 'id 为平台歌曲 id:kw/tx/wy 用 songmid,kg 用 hash。', tags: ['歌曲资源'], params: params({ source: pSource('url'), @@ -166,7 +166,7 @@ export const musicRoutes = async(fastify) => { let result try { result = unwrap(m.getLyric({ songmid: id, hash: id, copyrightId: id, name: name || '', interval: interval || '' })) - } catch (err) { + } catch (err: any) { throw upstream(err.message) } return result.then(res => (res || { lyric: '', tlyric: '' })).catch(err => { @@ -194,7 +194,7 @@ export const musicRoutes = async(fastify) => { let result try { result = unwrap(m.getPic({ songmid: id, hash: id, copyrightId: id, albumId: albumId || '' })) - } catch (err) { + } catch (err: any) { throw upstream(err.message) } return result.then(url => ({ url })).catch(err => { @@ -346,7 +346,7 @@ export const musicRoutes = async(fastify) => { const { source, id } = request.params const { page, limit } = request.query const album = getSource(musicSdk, source, 'album').album - // 各平台适配器签名不同:kg/mg 为 getAlbumDetail(id, page, limit), + // 各平台适配器签名不同:kg 为 getAlbumDetail(id, page, limit), // kw 为 getAlbumListDetail(id, page)——第三参数是 retryNum,不能透传 limit if (album.getAlbumDetail) return album.getAlbumDetail(id, page, limit).catch(err => { throw upstream(err.message) }) return album.getAlbumListDetail(id, page).catch(err => { throw upstream(err.message) }) @@ -378,7 +378,7 @@ export const musicRoutes = async(fastify) => { const m = musicSdk[id] || {} const qualities = [...new Set( Object.values(musicSdk.supportQuality || {}) - .map(q => q[id]) + .map((q: any) => q[id]) .filter(Boolean) .flat(), )] diff --git a/src/server.js b/src/server.ts similarity index 90% rename from src/server.js rename to src/server.ts index 0530993..fa89f2a 100644 --- a/src/server.js +++ b/src/server.ts @@ -13,7 +13,7 @@ // 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 /album/:source/:id 专辑详情+曲目(kg/kw) // GET /sources /health GET /docs /redoc /openapi.json // // 状态码语义:200 成功;422 参数校验失败;404 路由或平台不存在;405 方法不允许;502 上游平台失败;500 内部错误 @@ -26,19 +26,19 @@ import swaggerUi from '@fastify/swagger-ui' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' -import musicSdk from '../musicSdk/index.js' -import { ApiError } from './http/api-error.js' -import { musicRoutes } from './routes.js' -import { loginRoutes } from './routes-login.js' -import { COMPONENT_SCHEMAS } from './openapi/components.js' -import { redocHtml } from './openapi/ui.js' +import musicSdk from '../sdk/index.ts' +import { ApiError } from './http/api-error.ts' +import { musicRoutes } from './routes.ts' +import { loginRoutes } from './routes-login.ts' +import { COMPONENT_SCHEMAS } from './openapi/components.ts' +import { redocHtml } from './openapi/ui.ts' // 默认端口 16688:本机 TCP 动态端口范围是 1024~15000(出站连接会随机占用低端端口),避开该区间 const PORT = process.env.PORT || 16688 const DOCS_PREFIX = '/docs' const API_TITLE = 'MusicServer 聚合音乐 API' const API_DESCRIPTION = '多平台音乐聚合接口:搜索、播放地址、歌词、封面、榜单、歌单。' + - '支持 kw(酷我)/ kg(酷狗)/ tx(QQ音乐)/ wy(网易云)/ mg(咪咕)平台。' + '支持 kw(酷我)/ kg(酷狗)/ tx(QQ音乐)/ wy(网易云)平台。' const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8')) @@ -57,7 +57,7 @@ for (const [name, schema] of Object.entries(COMPONENT_SCHEMAS)) { // ---- 全局中间行为 ---- // 收集全部路由模式(含 :param 参数段),供 not-found 处理器做 405 判定 -const routePatterns = [] +const routePatterns: { method: any; regex: RegExp }[] = [] fastify.addHook('onRoute', (routeOptions) => { if (!routeOptions.url || !routeOptions.method) return // :param 段 → [^/]+;通配符 * → .*(如 @fastify/cors 注册的 catch-all 路由);其余按字面量转义 @@ -84,7 +84,7 @@ fastify.addHook('onResponse', async(request, reply) => { // 必须豁免:Swagger UI 强制从该地址加载规范,包了信封顶层就没有 openapi 字段,页面直接报错 fastify.addHook('preSerialization', async(request, reply, payload) => { if (reply.statusCode !== 200) return payload - if (request.routeOptions.config?.raw) return payload + if ((request.routeOptions.config as any)?.raw) return payload if (request.routeOptions.url === `${DOCS_PREFIX}/json`) return payload if (payload === null || typeof payload === 'object') return { code: 0, data: payload } return payload @@ -92,9 +92,9 @@ fastify.addHook('preSerialization', async(request, reply, payload) => { // ---- 错误处理 ---- -fastify.setErrorHandler((err, request, reply) => { +fastify.setErrorHandler((err: any, request, reply) => { if (err instanceof ApiError) { - const payload = { code: err.status, msg: err.message } + const payload: Record = { code: err.status, msg: err.message } if (err.detail !== undefined) payload.detail = err.detail return reply.code(err.status).send(payload) } @@ -129,7 +129,7 @@ fastify.setNotFoundHandler((request, reply) => { fastify.register(swagger, { // 组件按 addSchema 的 $id 命名(默认 def-1/def-2 不可读) refResolver: { - buildLocalReference: (json, _baseUri, _fragment, i) => json.$id || `def-${i}`, + buildLocalReference: (json: any, _baseUri: any, _fragment: any, i: number) => json.$id || `def-${i}`, }, openapi: { openapi: '3.1.0', @@ -185,7 +185,7 @@ fastify.register(musicRoutes) fastify.register(loginRoutes) export const startServer = async(port = PORT) => { - await fastify.listen({ port, host: '0.0.0.0' }) + await fastify.listen({ port: Number(port), host: '0.0.0.0' }) console.log(`音乐服务器已启动: http://localhost:${port}`) console.log(`接口文档: http://localhost:${port}/docs (Swagger UI)`) console.log(`ReDoc 文档: http://localhost:${port}/redoc`) @@ -205,6 +205,6 @@ process.on('SIGTERM', () => shutdown('SIGTERM')) process.on('SIGINT', () => shutdown('SIGINT')) // 直接运行时启动 -if (process.argv[1] && (process.argv[1].endsWith('server.js') || process.argv[1].endsWith('src\\server.js'))) { +if (process.argv[1] && (process.argv[1].endsWith('server.ts') || process.argv[1].endsWith('src\\server.ts'))) { startServer() } diff --git a/src/types/modules.d.ts b/src/types/modules.d.ts deleted file mode 100644 index e404bf9..0000000 --- a/src/types/modules.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -// 为项目中尚未迁移到 TypeScript 的 JavaScript 模块提供最小类型声明, -// 让 source-loader / apis 等 TypeScript 文件可以正常导入。 - -declare module '../common/request.js' { - export interface HttpFetchResponse { - statusCode: number - statusMessage?: string - headers: Record - body: any - } - - export interface HttpRequestObject { - promise: Promise - cancelHttp(): void - } - - export function httpFetch(url: string, options?: Record): HttpRequestObject -} - -declare module '../../src/auth/store.js' { - export function getCredentials(platform: string): any - export function setCredentials(platform: string, credentials: object): void - export function clearCredentials(platform: string): void - export function getWyCookie(): string - export function getKgAuth(): { token: string; userid: number | string } | null -} - -declare module './kw.js' { - const mod: { getMusicUrl(songInfo: any, type: string): any; default: any } - export = mod -} - -declare module './kg.js' { - const mod: { getMusicUrl(songInfo: any, type: string): any; default: any } - export = mod -} - -declare module './tx.js' { - const mod: { getMusicUrl(songInfo: any, type: string): any; default: any } - export = mod -} - -declare module './wy.js' { - const mod: { getMusicUrl(songInfo: any, type: string): any; default: any } - export = mod -} - -declare module './mg.js' { - const mod: { getMusicUrl(songInfo: any, type: string): any; default: any } - export = mod -} - -declare module './bd.js' { - const mod: { getMusicUrl(songInfo: any, type: string): any; default: any } - export = mod -} diff --git a/src/smoke-test.js b/test/smoke-test.ts similarity index 94% rename from src/smoke-test.js rename to test/smoke-test.ts index 09770e8..8ba4c56 100644 --- a/src/smoke-test.js +++ b/test/smoke-test.ts @@ -1,5 +1,5 @@ // 冒烟测试 —— 验证服务器各接口与各平台功能 -// 运行:node src/smoke-test.js +// 运行:pnpm test(Node 22.18+ 直接以类型剥离方式运行 .ts) // // 覆盖:搜索(各平台 + 聚合)、URL(kw/wy/tx)、歌词、封面、榜单详情、热搜、歌单、 // 跨平台匹配 /find、歌手/专辑、路径参数风格路由、标准 HTTP 语义(404/405/422/302)、 @@ -11,9 +11,9 @@ import { spawn } from 'node:child_process' // 本地默认 16688(避开 1024~15000 的动态端口冲突区),可用 PORT/BASE_URL 覆盖 const PORT = process.env.PORT || 16688 const BASE = process.env.BASE_URL || `http://localhost:${PORT}` -const results = [] +const results: { name: string; ok: boolean; detail?: any }[] = [] -const get = (path) => new Promise((resolve, reject) => { +const get = (path: string): Promise => new Promise((resolve, reject) => { http.get(`${BASE}${path}`, res => { let data = '' res.on('data', c => { data += c }) @@ -24,7 +24,7 @@ const get = (path) => new Promise((resolve, reject) => { }) // 原始响应(不解析 JSON,用于 HTML 页与状态码断言) -const getRaw = (path) => new Promise((resolve, reject) => { +const getRaw = (path: string): Promise => new Promise((resolve, reject) => { http.get(`${BASE}${path}`, res => { let data = '' res.on('data', c => { data += c }) @@ -32,7 +32,7 @@ const getRaw = (path) => new Promise((resolve, reject) => { }).on('error', reject) }) -const postRaw = (path) => new Promise((resolve, reject) => { +const postRaw = (path: string): Promise => new Promise((resolve, reject) => { const req = http.request(`${BASE}${path}`, { method: 'POST' }, res => { let data = '' res.on('data', c => { data += c }) @@ -43,7 +43,7 @@ const postRaw = (path) => new Promise((resolve, reject) => { }) // JSON POST(登录等写入接口) -const postJson = (path, body) => new Promise((resolve, reject) => { +const postJson = (path: string, body: any): Promise => new Promise((resolve, reject) => { const payload = JSON.stringify(body) const req = http.request(`${BASE}${path}`, { method: 'POST', @@ -61,12 +61,12 @@ const postJson = (path, body) => new Promise((resolve, reject) => { req.end() }) -const test = async(name, fn) => { +const test = async(name: string, fn: () => Promise) => { try { const result = await fn() results.push({ name, ok: true, detail: result }) console.log(`✅ ${name}`) - } catch (err) { + } catch (err: any) { results.push({ name, ok: false, detail: err.message }) console.log(`❌ ${name}: ${err.message}`) } @@ -89,7 +89,7 @@ const isServerRunning = () => new Promise(resolve => { * 启动本地服务器并返回关闭句柄 */ const startLocalServer = () => new Promise((resolve, reject) => { - const proc = spawn(process.execPath, ['src/server.js'], { + const proc = spawn(process.execPath, ['src/server.ts'], { cwd: process.cwd(), env: { ...process.env, PORT: String(PORT) }, stdio: ['ignore', 'pipe', 'pipe'], @@ -117,7 +117,7 @@ const startLocalServer = () => new Promise((resolve, reject) => { setTimeout(check, 300) }) -let localServer = null +let localServer: any = null const running = await isServerRunning() if (!running) { console.log('本地服务器未运行,正在自动启动...') @@ -128,7 +128,7 @@ if (!running) { } // 搜索 -for (const source of ['kw', 'kg', 'tx', 'wy', 'mg']) { +for (const source of ['kw', 'kg', 'tx', 'wy']) { await test(`搜索 ${source}`, async() => { const r = await get(`/search?source=${source}&name=${encodeURIComponent('稻香')}&limit=2`) if (r.code != 0) throw new Error(r.msg) @@ -183,7 +183,7 @@ await test('封面 kw', async() => { }) // 榜单 & 热搜 -for (const source of ['kw', 'kg', 'tx', 'wy', 'mg']) { +for (const source of ['kw', 'kg', 'tx', 'wy']) { await test(`榜单 ${source}`, async() => { const r = await get(`/boards/${source}`) if (r.code != 0) throw new Error(r.msg) @@ -297,13 +297,6 @@ await test('专辑详情 /album/tx', async() => { 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}`) diff --git a/tsconfig.json b/tsconfig.json index f20b926..d659aad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,14 +13,15 @@ "types": ["node"], // noImplicitAny 暂缓:SDK 侧隐式 any 数量过大,待整体补类型时开启 "noImplicitAny": false, + "noEmit": true, "noEmitOnError": true, "allowImportingTsExtensions": true, "rewriteRelativeImportExtensions": true }, "include": [ "src/**/*.ts", - "src/types/**/*.d.ts", - "musicSdk/**/*.ts" + "sdk/**/*.ts", + "test/**/*.ts" ], "exclude": [ "node_modules",