commit f289df4cd70bc596fcf2bead5c67c1b20cd00e48 Author: archive Date: Fri Aug 28 23:24:49 2026 +0800 存档:TS 重构前基线(lx 聚合源 = isolated-vm 加载器,签名方案已移除) diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..f8c8297 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(ls -la \"C:\\\\Users\\\\Administrator\\\\.claude\\\\projects\\\\d--MusicServer\\\\memory\" 2>/dev/null; cat \"C:\\\\Users\\\\Administrator\\\\.claude\\\\projects\\\\d--MusicServer\\\\memory\\\\MEMORY.md\" 2>/dev/null; ls /d/MusicServer/*.md 2>/dev/null)", + "Read(//c/Users/Administrator/.claude/projects/d--MusicServer/memory/**)", + "Bash(grep -rn \"from '\\\\.\\\\./\\\\.\\\\./request'\" /d/MusicServer/musicSdk --include=\"*.js\" -l)", + "Bash(grep -v \"from '\\\\./\\\\|from '\\\\.\\\\./\\\\|from '\\\\.\\\\./\\\\.\\\\./\")" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32d4445 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules/ +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Runtime +*.log +logs/ +.env +.env.local +.env.*.local + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Test / build +dist/ +coverage/ +.nyc_output/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..935535a --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# 音乐服务器(MusicServer) + +基于洛雪音乐(lx-music)聚合 SDK 的多平台音乐 API 服务器,纯 Node.js 实现,无需浏览器环境。 + +## 功能 + +- **多平台聚合**:酷我(kw)、酷狗(kg)、QQ音乐(tx)、网易云(wy)、咪咕(mg) +- **直连取流**:各平台播放地址获取(不依赖第三方 API 服务) +- **完整链路**:搜索、播放地址、歌词(含各平台加密歌词解码)、封面、榜单、热搜、歌单 + +## 快速开始 + +```bash +npm install +npm start # 默认端口 3300,可用 PORT 环境变量修改 +npm test # 运行冒烟测试(自动启动/关闭本地服务器) +``` + +## HTTP 接口 + +| 接口 | 说明 | +| --- | --- | +| `GET /search?name=xxx&singer=xxx&source=kw&page=1&limit=20` | 搜索歌曲(source 省略则聚合所有平台) | +| `GET /find?name=xxx&singer=xxx&albumName=xxx&interval=xxx` | 精确匹配搜索(跨平台去重排序) | +| `GET /url?source=kw&id=228908&quality=320k` | 获取播放地址(quality: 128k/320k/flac/flac24bit) | +| `GET /lyric?source=kw&id=228908` | 获取歌词(返回 lyric/tlyric/lxlyric) | +| `GET /pic?source=kw&id=228908` | 获取封面 | +| `GET /boards?source=kw` | 榜单列表 | +| `GET /board?source=kw&id=93&page=1` | 榜单详情 | +| `GET /hotSearch?source=kw` | 热搜列表 | +| `GET /songList?source=kw&page=1` | 歌单列表 | +| `GET /health` | 健康检查 | + +统一返回格式:`{ code: 0, data: ... }` 成功;`{ code: 非0, msg: ... }` 失败。 + +所有接口默认返回 `Access-Control-Allow-Origin: *`,可直接在浏览器/前端调用。 + +## 歌曲 ID 约定 + +- **kw / tx / wy**:`songmid`(如 `228908`、`0039MnYb0qxYhV`) +- **kg**:`hash`(搜索结果的 `hash` 字段;当前环境 kg 取流接口被服务端限制,URL 获取可能失败) +- **mg**:`songmid`(搜索结果的 `songmid` 字段,内部自动换取 contentId) + +## 项目结构 + +``` +src/ + server.js HTTP 服务器入口(npm start 实际启动文件) + index.js SDK 根模块(工具函数聚合导出) + smoke-test.js 冒烟测试(自动启动本地服务器) + apis/ 各平台 URL 直连实现(kw/kg/tx/wy/mg/bd) + 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(搜索/歌词/榜单等核心逻辑) + index.js 平台聚合入口 + request.js SDK 请求模块(转接 src/common/request.js) + message.js SDK 消息模块(转接 src/common/message.js) +``` + +## 关于 lx-music-source-v5.js + +项目目录下的 `lx-music-source-v5.js` 是第三方混淆音源脚本。由于内容不可审计,且 Node.js `vm` 不是安全沙箱,服务器代码中**不再直接加载或执行该脚本**,也不依赖它获取播放地址。 + +如需复用其服务端能力,需要其提供去混淆源码或完整 API 文档。当前 `docs/lx-source-signing.md` 记录了已逆向出的确定性签名部分(`e`、`s2`、`s3`),但脚本内部用于 `to` / `si` 的混淆逻辑尚未安全还原,因此未集成到生产路径。 + +## 已知限制 + +- **kg 取流**:酷狗当前环境下 `wwwapi`/`trackercdn`/`m.kugou.com` 等取流接口均被服务端风控(err_code 30020/20010),播放地址获取可能失败;歌词、搜索、榜单正常。 +- **kw flac**:`antiserver` 接口对 flac 返回 `refuse`,高音质会回退官方接口(需要 csrf token,token 失效时自动重试)。 +- 平台接口随时可能变更,遇失效时可对照 `src/apis/` 内的接口文档调整。 diff --git a/docs/lx-source-ivm.md b/docs/lx-source-ivm.md new file mode 100644 index 0000000..81419ea --- /dev/null +++ b/docs/lx-source-ivm.md @@ -0,0 +1,54 @@ +# lx 聚合源 —— isolated-vm 加载器 + +> 结论先行:lx-music-source-v5 的请求签名中 `to`/`si` 两项为脚本内部哈希(服务端分别以 +> "时间签名验证失败"/"脚本完整性验证失败" 独立校验),无法脱离脚本复算。直连签名方案已放弃, +> 一切请求由脚本本身在隔离沙箱中发起。 + +## 架构 + +``` +调用方 ── lx.kw.getMusicUrl(songInfo, type) ──▶ src/apis/lx-source.js(懒加载单例) + │ + ▼ + src/apis/lx-source-ivm.js + isolated-vm 硬隔离沙箱 + ┌──────────────────────────┐ + │ 原始混淆脚本(升版即换文件) │ + │ ↕ 仅经显式桥接通道 │ + └──────────────────────────┘ + http(白名单) crypto zlib 定时器 console +``` + +- 接口契约与其它平台适配器一致:`getMusicUrl(songInfo, type) → { promise, cancelHttp }`,resolve → `{ type, url }` +- 脚本升版:替换项目根目录脚本文件(或设 `LX_SOURCE_SCRIPT_PATH`)即可,无需改代码 + +## 安全模型 + +| 面 | 措施 | +|---|---| +| 进程 | isolate 无 process/require/fs,内存上限 256MB(可调) | +| HTTP | **默认阻止**;白名单取脚本头部 `SERVER_SCRIPT_CONFIG.apiUrl` 同源,可覆盖 | +| 密钥 | crypto 桥接日志只记操作与长度,不落密钥内容 | +| 观测 | 每个 lx API 调用(on/send/request/utils/dispatch)均有日志,请求含耗时与状态码 | + +HTTP 白名单选项(`createIvmLxSource`): + +- `allowedOrigins: string[]` —— 显式指定白名单(覆盖脚本声明) +- `httpFilter: (url) => boolean` —— 自定义判定,优先于 origin 白名单 +- `allowAllHttp: true` —— 全部放行(仅调试用) + +被拦截的请求会以错误回调返回脚本(`✋` 日志标记),脚本按请求失败自行处理。 + +## 环境变量(经 lx-source.js 封装) + +- `LX_SOURCE_SCRIPT_PATH` —— 脚本文件路径(默认项目根目录 `lx-music-source-v5.js`) +- `LX_SOURCE_LOG=0` —— 关闭日志 +- `LX_SOURCE_ALLOW_ALL_HTTP=1` —— 放行全部 HTTP(调试用) + +## 已知边界 + +- `lx.request` 回调的 resp 不含 `raw`/`bytes`(Buffer 无法跨隔离边界,实测脚本未使用) +- `formData` 选项不支持(底层 httpFetch 不支持 multipart) +- `updateAlert` 事件仅记日志,不弹窗 +- 签名逆向的历史结论已随直连方案移除;如需重启攻坚,`to` 为 88bit 时间签名、`si = md5(A + M2)` + (M2 为脚本文本零宽+大写化变换哈希,A 为 VM 内部 128bit 哈希),需字节码级反编译 diff --git a/lx-music-source-v5.js b/lx-music-source-v5.js new file mode 100644 index 0000000..f66741c --- /dev/null +++ b/lx-music-source-v5.js @@ -0,0 +1,13 @@ +/*! + * @name 独家音源 + * @description 后续更新,请关注微信公众号: 洛雪科技 + * @version 5 + * @author w + */ + +// ===== 服务端下发配置(自动生成, 请勿修改) ===== +globalThis['SERVER_SCRIPT_CONFIG'] = {"apiUrl":"https:\/\/88.lxmusic.xn--fiqs8s","apiKey":"lxmusic","signSalt":"LxSrv@2026#Sig","fingerprint":"74c52ba24b386a2301983d94596246c2"}; +// ===== 服务端下发配置结束 ===== + + +;(function ﱡﹶ(ﱡﹲ,ﱡﱣ,ﹲ‌,ﱞﹶ,ﹲﹰ,ﹸﹼ,ﹰﱣ,ﹰﹼ,ﹴﱠ,ﹸﹸ,ءﱟ,ﱟﹸ,ﹰﹲ,ﱠﹴ,ﱞﹲ,ﹼﱣ,ﹶﹲ,ﹶﹰ){return ﹶﹰ=function(ﱟﹺ,ﹼ,ﹰﹺ,ءﱟ){return function(ﹼﹶ,ﱡء,ءﱡ,ﱡﹲ,ﹲ‌,ﹲﹰ,ﹸﹼ,ﹰﱣ,ﹴﱠ){if(ﹼﹶ=ﹼﹶ^ﱡء,ﹲﹰ=66666-ﹼﹶ,ﹸﹼ=ﹶﹲ[ﹲﹰ],ﹸﹼ!==void 0)return ﹸﹼ;for(ﹲ‌=ﱡﱣ[ﹼﹶ],ﱡﹲ=[],ﹸﹼ=ﹰﹺ(ﹼﹶ,ﱡء,ﹲ‌.\u006c\u0065\u006e\u0067\u0074\u0068,ﹼ,ﱟﹺ),ﹰﱣ=0,ﹼﹶ=(ﹸﹼ^ﹸﹼ>>>16^ﱡء^ﹼﹶ)&65535,ءﱡ=0;ءﱡ<ﹲ‌.\u006c\u0065\u006e\u0067\u0074\u0068;ءﱡ++){(ءﱡ&1)===0&&(ﹸﹼ=ﹸﹼ+2654435769>>>0,ﹰﱣ=ﹼ(ﹸﹼ,ﱟﹺ));ﱡء=ﹰﱣ>>>((ءﱡ&1)<<4)&65535,ﹴﱠ=ﹲ‌[ءﱡ],ﹰﹼ(ﱡﹲ,(ﹴﱠ^ﱡء^ﹼﹶ)&65535),ﹼﹶ=ﹴﱠ+(ﹰﱣ>>>16&65535)+ءﱡ+40503&65535}return ﹶﹲ[ﹲﹰ]=ﹸﹸ(ﱡﹲ)}}(function(ﹼﹶ,ﱟﹺ,ﱡء,ﹼ){return ﱡء=ﹼﹶ>>>16&65535,ﹼﹶ=ﹼﹶ&65535,ﹼ=ﱟﹺ>>>16&65535,ﱟﹺ=ﱟﹺ&65535,ﹼﹶ*ﱟﹺ+(ﱡء*ﱟﹺ+ﹼﹶ*ﹼ<<16>>>0)|0},function(ﹼﹶ,ﱟﹺ){return ﹼﹶ>>>=0,ﹼﹶ=ﱟﹺ(ﹼﹶ^ﹼﹶ>>>16,569420461),ﹼﹶ=ﱟﹺ(ﹼﹶ^ﹼﹶ>>>15,1935289751),(ﹼﹶ^ﹼﹶ>>>15)>>>0},function(ﹸ‍,ﱡء,ءﱡ,ﹼ,ﱟﹺ){return ﹼ(1327381188>>>0^ﱟﹺ(ﹸ‍+1>>>0,2654435761)^ﱟﹺ(ﱡء+1>>>0,2246822507)^ﱟﹺ(ءﱡ+1>>>0,3266489909),ﱟﹺ)}),function ﹰﹺ(ﹼﹶ,ﱟﹺ,ﱡء,ﹼ,ءﱡ){ﱟﹺ=ﱟﹺ||[],ﱡء=ﱡء||[],ﹼ=ﹼ||[];for(var ﱡﹴ=ﱡء.\u006c\u0065\u006e\u0067\u0074\u0068,ءﹶ,ﹰﹸ;ﹰﹸ=ﱡﹲ[ﹼﹶ++];)if(61<ﹰﹸ)ﹰﹸ<68&&((ﹰﹸ>65||(ﹰﹸ<64&&((ﹰﹸ-->62||(ﱡء[++ﱡﹴ-ﹰﹸ+++60]=function(ﱡء,ﱡﹴ){return function(){return ﹶﹰ(ﱡء,ﱡﹴ)}}(ﱡﹲ[ﹼﹶ++]^-++ﹰﹸ+32881+ﹼﹶ,ﱡﹲ[ﹼﹶ++]^-ﹰﹸ--+49974+ﹼﹶ),!true))&&(ﱡء[ﱡﹴ+++ﹰﹸ---62]=function(ﱡء){return function(){return ﱟﹺ[ﱡء]++}}(ﱡﹲ[ﹼﹶ++]^--ﹰﹸ+57119+ﹼﹶ)),!0)||ﹰﹸ>64&&(ﱡء[ﱡﹴ+++ﹰﹸ-67]()||(ﹼﹶ=ﱡء[ﱡﹴ-ﹰﹸ+++63]()),(ﱡﹴ-=3)+ﹰﹸ+++31,1)||(ﱡء[ﱡﹴ+++ﹰﹸ-64]=function(){return function(){return typeof \u0070\u0072\u006f\u0063\u0065\u0073\u0073}}()),""))&&(66<ﹰﹸ||(ﱡء[(ﱡﹴ+=101)- --ﹰﹸ-38]=function(ﱡء,ﱡﹴ){return function(){return new ﹰﹲ(ﱡء(),ﱡﹴ())}}(ﱡء[++ﱡﹴ-104],ﱡء[(ﱡﹴ-=103)+ﹰﹸ++-65]),void 0))&&(ﱡء[--ﱡﹴ+ﹰﹸ-67]=function(ﱡء){return function(){return!ﱡء()}}(ﱡء[++ﱡﹴ- --ﹰﹸ+65])),!false)||73>ﹰﹸ&&(ﹰﹸ<70&&(ﹰﹸ<69&&(ﱡء[++ﱡﹴ+ ++ﹰﹸ-70]=function(){return function(){return \u0070\u0061\u0072\u0073\u0065\u0049\u006e\u0074}}(),!0)||(ﱡء[ﱡﹴ+++ ++ﹰﹸ-70]=function(){return function(){return \u0050\u0072\u006f\u006d\u0069\u0073\u0065}}()),true)||(71<ﹰﹸ||((71<++ﹰﹸ||(ﱡء[(ﱡﹴ+=--ﹰﹸ)-78]=function(ﱡء,ﱡﹴ,ﹰﹸ,ﹼﹶ,ﹰﱞ,ﹰء,ﹲﹼ,ﹼﱡ){return function(){return ﹰﱣ(ﱡء())(ﱡﹴ(),ﹰﹸ(),ﹼﹶ(),ﹰﱞ(),ﹰء(),ﹲﹼ(),ﹼﱡ())()}}(ﱡء[(ﱡﹴ+=43)-++ﹰﹸ-49],ﱡء[--ﱡﹴ+ﹰﹸ++-191],ﱡء[ﱡﹴ++-118],ﱡء[(ﱡﹴ+=8)+ﹰﹸ---198],ﱡء[--ﱡﹴ-ﹰﹸ-53],ﱡء[(ﱡﹴ+=ﹰﹸ--)-194],ﱡء[ﱡﹴ+ﹰﹸ++-263],ﱡء[(ﱡﹴ-=198)-ﹰﹸ+77]),![]))&&(ﱡء[(ﱡﹴ+=3)-ﹰﹸ--+67]()&&(ﹼﹶ=ﱡء[ﱡﹴ-ﹰﹸ--+67]()),(ﱡﹴ-=5)-ﹰﹸ+++115),null))&&(ﱡء[ﱡﹴ+++ ++ﹰﹸ-73]=function(){return function(){return \u0063\u006f\u006e\u0073\u006f\u006c\u0065}}()),!false)||ﹰﹸ>77&&(ﹰﹸ>81&&(83>ﹰﹸ--&&(ﱡء[ﱡﹴ+++ﹰﹸ---81]=function(ﱡء){return function(){return ﱡء}}(ﹼﱣ),!null)||(ﱡء[(ﱡﹴ-=10)-ﹰﹸ+89]=function(ﱡء,ﱡﹴ,ﹰﹸ,ﹼﹶ){return function(){return ﹸﹼ(ﱡء(),(ﱡﹴ()[0]=ﹰﹸ(),ﹼﹶ()))()}}(ﱡء[ﱡﹴ+ﹰﹸ-74],ﱡء[(ﱡﹴ+=36)+ﹰﹸ-109],ﱡء[(ﱡﹴ+=ﹰﹸ)-111],ﱡء[(ﱡﹴ-=110)+ﹰﹸ---81])),!null)||++ﹰﹸ>81&&(ﱡء[ﱡﹴ-ﹰﹸ+80]=function(ﱡء,ﱡﹴ){return function(){return delete ﱡء()[ﱡﹴ()]}}(ﱡء[(ﱡﹴ+=ﹰﹸ++)-84],ﱡء[(ﱡﹴ-=83)+ﹰﹸ---83]),!null)||ﹰﹸ>80&&(ﱡء[ﱡﹴ+++ ++ﹰﹸ-82]=function(){return function(){return \u0053\u0079\u006d\u0062\u006f\u006c}}(),1)||80>ﹰﹸ&&(ﱡء[ﱡﹴ++]=function(){return function(){return \u004f\u0062\u006a\u0065\u0063\u0074}}(),!0)||(ﱡء[ﱡﹴ++]=function(){return function(){return \u0072\u0065\u0071\u0075\u0069\u0072\u0065}}()),!false)||(ﹰﹸ>76||((ﹰﹸ>75||(ﹰﹸ<75&&((ﹰﹸ<74||(ﱡء[(ﱡﹴ+=92)+ ++ﹰﹸ-168]=function(ﱡء){return function(){return-ﱡء()}}(ﱡء[(ﱡﹴ-=92)-++ﹰﹸ+75]),void 0))&&(ﱡء[ﱡﹴ--+--ﹰﹸ-74]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()+ﱡﹴ()}}(ﱡء[(ﱡﹴ-=--ﹰﹸ)+70],ﱡء[(ﱡﹴ+=71)-ﹰﹸ--+71])),1)||(ﱡء[(ﱡﹴ+=ﹰﹸ)-77]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()*ﱡﹴ()}}(ﱡء[ﱡﹴ++-ﹰﹸ++-2],ﱡء[(ﱡﹴ-=77)+--ﹰﹸ-75])),void 0))&&(ﱡء[++ﱡﹴ-ﹰﹸ--+73]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()<=ﱡﹴ()}}(ﱡء[--ﱡﹴ+ﹰﹸ---77],ﱡء[--ﱡﹴ+--ﹰﹸ-73])),0))&&ﱡء[(ﱡﹴ-=2)-ﹰﹸ--+77]()&&(ﹼﹶ=ﱡء[--ﱡﹴ+ﹰﹸ-74]());else if(ﹰﹸ<22)ﹰﹸ>16&&((++ﹰﹸ>21||((20>ﹰﹸ||((ﹰﹸ>20||(ﱡء[++ﱡﹴ+ﹰﹸ++-21]=function(ﱡء){return function(){return ﱡء}}(void 2),""))&&(ﱡء[ﱡﹴ+++ﹰﹸ-21]=function(ﱡء){return function(){return ﱡء}}([])),""))&&(ﹰﹸ++<19&&(ﱡء[ﱡﹴ+++--ﹰﹸ-18]=function(){return function(){return \u004e\u0075\u006d\u0062\u0065\u0072}}(),true)||(ﱡء[--ﱡﹴ- --ﹰﹸ+18]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()<ﱡﹴ()}}(ﱡء[ﱡﹴ-ﹰﹸ+++18],ﱡء[ﱡﹴ]))),""))&&(ﱡء[++ﱡﹴ-++ﹰﹸ+22]=function(){return function(){return \u0045\u0072\u0072\u006f\u0072}}()),true)||11<ﹰﹸ&&((ﹰﹸ>15||(ﹰﹸ>14&&(ﱡء[ﱡﹴ++-++ﹰﹸ+16]=function(ﱡء){return function(){return ﱡء}}({}),!"")||14<++ﹰﹸ&&(ﱡء[++ﱡﹴ-1]=function(ﱡء){return function(){return ﱡء}}(ﱞﹲ),1)||(ﹰﹸ>13||(function(){throw ﱡء[(ﱡﹴ+=2)-3]()}(),""))&&(ﱡء[++ﱡﹴ+--ﹰﹸ-14]=function(){return function(){return typeof \u0053\u0079\u006d\u0062\u006f\u006c}}()),""))&&(ﱡء[(ﱡﹴ+=76)-++ﹰﹸ-61]=function(ﱡء,ﱡﹴ){return function(){return ﱟﹺ[ﱡء()]=ﱡﹴ()}}(ﱡء[ﱡﹴ++-78],ﱡء[(ﱡﹴ-=78)+ﹰﹸ-17])),true)||ﹰﹸ<7&&((ﹰﹸ>4||(3>ﹰﹸ--&&(ﹰﹸ>0&&(ﱡء[--ﱡﹴ+ﹰﹸ-2]=function(ﱡء,ﱡﹴ){return function(){return new(ﱡء())(ﱡﹴ())}}(ﱡء[--ﱡﹴ+ﹰﹸ---1],ﱡء[ﱡﹴ+++ ++ﹰﹸ]),!"")||(ﱡء[ﱡﹴ++-ﹰﹸ]=function(){return function(){return typeof \u0072\u0065\u0071\u0075\u0069\u0072\u0065}}()),!0)||(2<ﹰﹸ||(ﱡء[ﱡﹴ---ﹰﹸ--+1](),0))&&(ﱡء[++ﱡﹴ+ﹰﹸ---4]=function(ﱡء){return function(){return ﱟﹺ[ﱡء]}}(ﱡﹲ[ﹼﹶ++]^-ﹰﹸ+28941+ﹼﹶ)),void 0))&&(6>ﹰﹸ||(ﱡء[(ﱡﹴ-=94)+ ++ﹰﹸ+80]=function(ﱡء,ﱡﹴ,ﹰﹸ,ﹼﹶ,ﹰﱞ,ﹰء,ﹲﹼ){return function(){return ﹰﱣ(ﱡء(),ﱡﹴ())(ﹰﹸ(),ﹼﹶ(),ﹰﱞ())(ﹰء(),ﹲﹼ())}}(ﱡء[ﱡﹴ---ﹰﹸ+95],ﱡء[ﱡﹴ++-ﹰﹸ+95],ﱡء[(ﱡﹴ+=--ﹰﹸ)+83],ﱡء[(ﱡﹴ-=55)-ﹰﹸ+145],ﱡء[(ﱡﹴ+=65)+ﹰﹸ--+69],ﱡء[ﱡﹴ--- --ﹰﹸ+80],ﱡء[(ﱡﹴ+=73)+ﹰﹸ+1]),void 0))&&(ﱡء[ﱡﹴ++-ﹰﹸ--+5]=ﱡء[(ﱡﹴ-=105)+--ﹰﹸ+100]=function(ﱡء,ﱡﹴ,ﹰﹸ,ﹼﹶ,ﱟﹺ){return ﹰﹸ=function(){return ﱡﹴ},ﱟﹺ=function(){return ﱡﹴ=ﱡء(),ﹼﹶ=ﹰﹸ,ﱡﹴ},ﹼﹶ=ﱟﹺ,function(){return ﹼﹶ()}}(ﱡء[(ﱡﹴ+=105)-ﹰﹸ+1])),!0)||9>ﹰﹸ&&(7<ﹰﹸ++&&(ﱡء[(ﱡﹴ+=++ﹰﹸ)-11]=function(ﱡء){return function(){return ﱡء}}(ﱡء[(ﱡﹴ-=10)+--ﹰﹸ-10]()),!false)||(ﱡء[ﱡﹴ+--ﹰﹸ-9]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()===ﱡﹴ()}}(ﱡء[++ﱡﹴ-3],ﱡء[(ﱡﹴ-=2)+ ++ﹰﹸ-8])),!null)||10>ﹰﹸ&&(ﱡء[ﱡﹴ++-ﹰﹸ+++9]=function(){return function(){return \u0053\u0043\u0052\u0049\u0050\u0054\u005f\u004d\u0044\u0035}}(),!null)||(10<ﹰﹸ||(ﱡء[--ﱡﹴ-ﹰﹸ--+9]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()-ﱡﹴ()}}(ﱡء[--ﱡﹴ+--ﹰﹸ-8],ﱡء[++ﱡﹴ+--ﹰﹸ-7]),!true))&&(ﱡء[--ﱡﹴ+--ﹰﹸ-14]=function(ﱡء,ﱡﹴ,ﹰﹸ,ﹼﹶ,ﹰﱞ){return function(){return ﹰﱣ(ﱡء())(ﱡﹴ(),ﹰﹸ(),ﹼﹶ())(ﹰﱞ())}}(ﱡء[(ﱡﹴ-=++ﹰﹸ)+8],ﱡء[++ﱡﹴ-ﹰﹸ+++17],ﱡء[(ﱡﹴ+=++ﹰﹸ)-5],ﱡء[ﱡﹴ--+ﹰﹸ++-17],ﱡء[(ﱡﹴ-=5)+3]));else if(41<ﹰﹸ--){if(ﹰﹸ>56){if(ﹰﹸ>59)ﹼﹶ=ﱡء[ءﹶ=ﹼﹶ,--ﱡﹴ+ﹰﹸ---60]();else if(59>ﹰﹸ)57<ﹰﹸ--&&(ﱡء[ﱡﹴ++-++ﹰﹸ+58]=function(ﱡء){return function(){return ﱡء}}(null),!"")||(ﱡء[++ﱡﹴ-ﹰﹸ--+53]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()!=ﱡﹴ()}}(ﱡء[++ﱡﹴ+ﹰﹸ-59],ﱡء[(ﱡﹴ-=3)+ﹰﹸ-55]));else return ءﱡ.\ufe7c\ufc60=0;}else if(47>ﹰﹸ){if(43>ﹰﹸ)42>ﹰﹸ++&&(ﱡء[ﱡﹴ---++ﹰﹸ+41]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()%ﱡﹴ()}}(ﱡء[ﱡﹴ--+ ++ﹰﹸ-45],ﱡء[ﱡﹴ+++1]),!0)||(ﱡء[ﱡﹴ--+ﹰﹸ++-45]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()==ﱡﹴ()}}(ﱡء[(ﱡﹴ+=113)-114],ﱡء[(ﱡﹴ-=113)+ﹰﹸ++-44]));else if(--ﹰﹸ<43)ﱡء[(ﱡﹴ-=ﹰﹸ--)+39]=function(ﱡء,ﱡﹴ,ﹰﹸ){return function(){return ﹼ[ﹼ.\u006c\u0065\u006e\u0067\u0074\u0068-ﱡء()][ﱡﹴ()]=ﹰﹸ()}}(ﱡء[ﱡﹴ++-ﹰﹸ+81],ﱡء[(ﱡﹴ-=ﹰﹸ)+79],ﱡء[(ﱡﹴ+=80)+1]);else if(ﹰﹸ<44)return ﱡء[ﱡﹴ---ﹰﹸ--+42]();else(ﹰﹸ>44||(ﱡء[ﱡﹴ++]=function(){return function(){return \u006c\u0078}}(),0))&&(ﱡء[++ﱡﹴ-10]=function(ﱠﹼ,ﹴﹴ,ﹴﹰ,ﱣﱣ,ﱠﹺ,ءﹰ,ﱞﱡ,ﱢﹸ,ﱠﱣ,ﹺﹴ,ﹸﱣ){return ﹺﹴ=ﹴﱠ(ﹼ,[ﱟﹺ]),ﹸﱣ=function ﹸﱣ(){var ﱡ=\u0061\u0072\u0067\u0075\u006d\u0065\u006e\u0074\u0073.\u006c\u0065\u006e\u0067\u0074\u0068>ﱢﹸ().\u006c\u0065\u006e\u0067\u0074\u0068?ﱢﹸ().\u006c\u0065\u006e\u0067\u0074\u0068:\u0061\u0072\u0067\u0075\u006d\u0065\u006e\u0074\u0073.\u006c\u0065\u006e\u0067\u0074\u0068,ﹲﹴ=[];for(var ﹶﱣ=0;ﹶﱣ<ﱡ;ﹶﱣ++)ﹲﹴ[ﱢﹸ()[ﹶﱣ]]=\u0061\u0072\u0067\u0075\u006d\u0065\u006e\u0074\u0073[ﹶﱣ];return ﱠﹺ()&&function(ﱡء,ﱡﹴ,ﹼﹶ){try{for(ﱡﹴ=ﱡ-1,ﹼﹶ=[];ﱡﹴ>=0;ﱡﹴ--)(function(ﱡﹴ,ﹰﹸ){ﹰﹸ in ﹼﹶ||(ءﱟ(ﱡء,ﱡﹴ,{\u0067\u0065\u0074:function(){return ﹲﹴ[ﹰﹸ]},\u0073\u0065\u0074:function(ﱡء){ﹲﹴ[ﹰﹸ]=ﱡء},\u0063\u006f\u006e\u0066\u0069\u0067\u0075\u0072\u0061\u0062\u006c\u0065:!0}),ﹼﹶ[ﹰﹸ]=1)})(ﱡﹴ,ﱢﹸ()[ﱡﹴ])}catch(ﱣﹰ){}}(ﹲﹴ[ﹴﹰ()]=\u0061\u0072\u0067\u0075\u006d\u0065\u006e\u0074\u0073),ﱠﱣ()&&(ﹲﹴ[ﱞﱡ()]=this),ﱣﱣ()&&function(){try{ءﱟ(ﹲﹴ,ﹴﹴ(),{\u0076\u0061\u006c\u0075\u0065:ﹸﱣ,\u0077\u0072\u0069\u0074\u0061\u0062\u006c\u0065:!1})}catch(ﱣﹰ){}}(),ﹰﹺ(ءﹰ(),ﹲﹴ,[],ﹺﹴ)},function(){try{ءﱟ(ﹸﱣ,"length",{\u0076\u0061\u006c\u0075\u0065:ﱠﹼ(),\u0063\u006f\u006e\u0066\u0069\u0067\u0075\u0072\u0061\u0062\u006c\u0065:!0})}catch(ﱣﹰ){}}(),function(){return function(){return ﹸﱣ}}()}(ﱡء[ﱡﹴ---ﹰﹸ+++43],ﱡء[ﱡﹴ-ﹰﹸ+44],ﱡء[--ﱡﹴ-ﹰﹸ+41],ﱡء[ﱡﹴ---ﹰﹸ--+44],ﱡء[ﱡﹴ--- --ﹰﹸ+39],ﱡء[++ﱡﹴ+ﹰﹸ++-51],ﱡء[ﱡﹴ--+ﹰﹸ-47],ﱡء[++ﱡﹴ+--ﹰﹸ-50],ﱡء[(ﱡﹴ-=6)+ﹰﹸ---41]));}else(51<ﹰﹸ++||(ﹰﹸ<50&&((ﹰﹸ<49||(ﱡء[ﱡﹴ+ﹰﹸ++-52]=function(ﱡء,ﱡﹴ,ﹰﹸ){return function(){return ﹸﹼ(ﱡء(),[ﱡﹴ(),ﹰﹸ()])()}}(ﱡء[--ﱡﹴ-1],ﱡء[ﱡﹴ---2],ﱡء[ﱡﹴ+--ﹰﹸ-48]),!{}))&&(ﱡء[++ﱡﹴ-1]=function(ﱡء){return function(){return ﱡء}}(!!null)),true)||(51<ﹰﹸ||((--ﹰﹸ<50||(ﱡء[ﱡﹴ++-ﹰﹸ+50]=function(){return function(){return \u004a\u0053\u004f\u004e}}(),""))&&(ﱡء[ﱡﹴ+++--ﹰﹸ-48]=function(){return function(){return \u0053\u0074\u0072\u0069\u006e\u0067}}()),!1))&&(ﱡء[++ﱡﹴ+ ++ﹰﹸ-54]=function(){return function(){return \u0054\u0079\u0070\u0065\u0045\u0072\u0072\u006f\u0072}}()),!1))&&(55>ﹰﹸ--&&((52<ﹰﹸ||(ﱡء[ﱡﹴ++-ﹰﹸ+++52]=function(ﱡء){return function(){return ﱡء}}(true),false))&&(ﱡء[++ﱡﹴ-++ﹰﹸ+53]=function(){return function(){return \u0065\u006e\u0063\u006f\u0064\u0065\u0055\u0052\u0049\u0043\u006f\u006d\u0070\u006f\u006e\u0065\u006e\u0074}}()),1)||(ﹰﹸ++>55||((56>ﹰﹸ||(ﱡء[ﱡﹴ+++ ++ﹰﹸ-57]=function(){return function(){return \u0070\u0072\u006f\u0063\u0065\u0073\u0073}}(),![]))&&(ﱡء[++ﱡﹴ- --ﹰﹸ+53]=function(ﱡء){return function(){return ﱡء}}(ﱡﹲ[ﹼﹶ++]^35843+ﹼﹶ)),0))&&(ﱡء[(ﱡﹴ+=102)-ﹰﹸ-48]=function(ﱡء,ﱡﹴ,ﹰﹸ){return function(){return ﱡء()[ﱡﹴ()]=ﹰﹸ()}}(ﱡء[--ﱡﹴ+ﹰﹸ++-161],ﱡء[ﱡﹴ++-103],ﱡء[(ﱡﹴ-=104)+--ﹰﹸ-56])));}else if(ﹰﹸ>36){if(39<ﹰﹸ)try{var ﹰﱡ=ﱡﹲ[ﹼﹶ++]^--ﹰﹸ+44385+ﹼﹶ,ﱣ=ﱡﹲ[ﹼﹶ++]^-ﹰﹸ+++48647+ﹼﹶ,ﱞﹼ=ﱡﹲ[ﹼﹶ++]^-ﹰﹸ+16051+ﹼﹶ,ﹰﹰ=ﱡﹲ[ﹼﹶ++]^-ﹰﹸ--+46121+ﹼﹶ,ﱞﹸ={\ufe7c\ufc60:1},ﱞ‌=ﹰﹺ(ﹼﹶ,ﱟﹺ,[],ﹼ,ﱞﹸ);if(ﱞﹸ.\ufe7c\ufc60)return ﱞ‌;if(ﱞﹸ.\ufc5e\ufe70!==void 0)if(ﹰﱡ=ﱞﹸ.\ufc5e\ufe70,ﱞﹸ.\ufc63\ufe74>1)return ءﱡ.\ufc5e\ufe70=ﹰﱡ,ءﱡ.\ufc63\ufe74=--ﱞﹸ.\ufc63\ufe74,ءﱡ.\ufe7c\ufc60=0}catch(ﱣﹰ){ﱟﹺ[ﹰﹰ]=ﱣﹰ,ﱞﹸ={\ufe7c\ufc60:1},ﱞ‌=ﹰﹺ(ﱣ,ﱟﹺ,[],ﹼ,ﱞﹸ);if(ﱞﹸ.\ufe7c\ufc60)return ﱞ‌;if(ﱞﹸ.\ufc5e\ufe70!==void 0)if(ﹰﱡ=ﱞﹸ.\ufc5e\ufe70,ﱞﹸ.\ufc63\ufe74>1)return ءﱡ.\ufc5e\ufe70=ﹰﱡ,ءﱡ.\ufc63\ufe74=--ﱞﹸ.\ufc63\ufe74,ءﱡ.\ufe7c\ufc60=0}finally{ﱞﹸ={\ufe7c\ufc60:1},ﱞ‌=ﹰﹺ(ﱞﹼ,ﱟﹺ,[],ﹼ,ﱞﹸ);if(ﱞﹸ.\ufe7c\ufc60)return ءﱡ&&(ءﱡ.\ufe7c\ufc60=1),ﱞ‌;if(ﱞﹸ.\ufc5e\ufe70!==void 0){if(ﹰﱡ=ﱞﹸ.\ufc5e\ufe70,ﱞﹸ.\ufc63\ufe74>1)return ءﱡ.\ufc5e\ufe70=ﹰﱡ,ءﱡ.\ufc63\ufe74=--ﱞﹸ.\ufc63\ufe74,ءﱡ.\ufe7c\ufc60=0;return ﹰﹺ(ﹰﱡ,ﱟﹺ,[],ﹼ,ءﱡ)}ﹼﹶ=ﹰﱡ}else(++ﹰﹸ<40||(ﱡء[(ﱡﹴ+=53)- --ﹰﹸ-16]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()[ﱡﹴ()]}}(ﱡء[--ﱡﹴ-ﹰﹸ-15],ﱡء[ﱡﹴ-=53]),![]))&&(ﹰﹸ>38&&(ﱡء[ﱡﹴ++-ﹰﹸ+++39]=function(ﱡء){return function(){return ﱡء}}(ﱡﹲ[ﹼﹶ++]-(++ﹰﹸ+19983+ﹼﹶ)),!"")||(ﱡء[++ﱡﹴ-1]=function(){return function(){return \u0069\u0073\u004e\u0061\u004e}}()));}else if(ﹰﹸ>31)ﹰﹸ>35&&(ﱡء[++ﱡﹴ-ﹰﹸ+31]=function(ﱡء,ﱡﹴ,ﹰﹸ,ﹼﹶ){return function(){return ﹲﹰ(ﱡء(),ﱡﹴ(),ﹰﹸ())(ﹼﹶ())}}(ﱡء[++ﱡﹴ-ﹰﹸ+31],ﱡء[(ﱡﹴ+=ﹰﹸ--)-42],ﱡء[++ﱡﹴ- --ﹰﹸ-7],ﱡء[(ﱡﹴ-=42)+--ﹰﹸ-31]),!0)||(34>ﹰﹸ||(ﹰﹸ<35&&(ﱡء[(ﱡﹴ-=ﹰﹸ++)+32]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()>ﱡﹴ()}}(ﱡء[ﱡﹴ+ ++ﹰﹸ-4],ﱡء[ﱡﹴ+=33]),true)||(ﱡء[++ﱡﹴ-ﹰﹸ+++34]=function(){return function(){return \u0044\u0061\u0074\u0065}}()),null))&&(32<ﹰﹸ||(ﱡء[(ﱡﹴ+=42)-++ﹰﹸ-11]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()^ﱡﹴ()}}(ﱡء[ﱡﹴ++-ﹰﹸ---11],ﱡء[(ﱡﹴ-=44)-++ﹰﹸ+33]),0))&&(ﱡء[ﱡﹴ++]=function(ﱡء,ﱡﹴ){return function(){return ﹼ[ﹼ.\u006c\u0065\u006e\u0067\u0074\u0068-ﱡء][ﱡﹴ]++}}(ﱡﹲ[ﹼﹶ++]^-ﹰﹸ--+50681+ﹼﹶ,ﱡﹲ[ﹼﹶ++]^ﹰﹸ--+53617+ﹼﹶ));else if(26<ﹰﹸ)(ﹰﹸ>30||((ﹰﹸ<29||((28<--ﹰﹸ||(ﱡء[ﱡﹴ+--ﹰﹸ-28]=function(ﱡء){return function(){return void ﱡء()}}(ﱡء[ﱡﹴ- --ﹰﹸ+25]),!1))&&(ﱡء[ﱡﹴ++-ﹰﹸ+29]=function(ﱡء){return function(){return ﱡء}}(ﱡﹲ[ﹼﹶ++]),ﱡء[ﱡﹴ]=function(ﱡء){return function(){return ﱡء}}(ﱡﹲ[ﹼﹶ++]),ﱡء[ﱡﹴ+ﹰﹸ-28]=function(ﱡء){return function(){return ﱡء}}(ﱡﹲ[ﹼﹶ++]),ﱡء[(ﱡﹴ+=ﹰﹸ--)-27]=function(ﱡء){return function(){return ﱡء}}(ﱡﹲ[ﹼﹶ++]),ﱡء[(ﱡﹴ+=24)+ﹰﹸ---78]=function(ﱡء){return function(){return ﱡء}}(ﱡﹲ[ﹼﹶ++]),ﱡء[ﱡﹴ++-49]=function(ﱡء){return function(){return ﱡء}}(ﱡﹲ[ﹼﹶ++]),ﱡء[(ﱡﹴ-=48)+--ﹰﹸ-27]=function(ﱡء){return function(){return ﱡء}}(ﱡﹲ[ﹼﹶ++])),null))&&(27<ﹰﹸ&&(ﱡء[ﱡﹴ+++ﹰﹸ-28]=function(ﱡء,ﱡﹴ){return function(){return ﹼ[ﹼ.\u006c\u0065\u006e\u0067\u0074\u0068-ﱡء][ﱡﹴ]}}(ﱡﹲ[ﹼﹶ++]^62427+ﹼﹶ,ﱡﹲ[ﹼﹶ++]^ﹰﹸ+++42457+ﹼﹶ),true)||(ﱡء[(ﱡﹴ+=ﹰﹸ)-29]=function(ﱡء,ﱡﹴ){return function(){return ﹸﹼ(ﱡء(),[ﱡﹴ()])()}}(ﱡء[ﱡﹴ++-ﹰﹸ++-1],ﱡء[(ﱡﹴ-=29)- --ﹰﹸ+26]))),0))&&(ﱡء[ﱡﹴ+++ﹰﹸ++-31]=function(){return function(){return \u0075\u006e\u0064\u0065\u0066\u0069\u006e\u0065\u0064}}());else if(24<ﹰﹸ)25<ﹰﹸ&&(ﱡء[ﱡﹴ++-ﹰﹸ+++26]=function(){return function(){return \u0067\u006c\u006f\u0062\u0061\u006c\u0054\u0068\u0069\u0073}}(),!0)||(ﱡء[ﱡﹴ++- --ﹰﹸ+22]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()instanceof ﱡﹴ()}}(ﱡء[(ﱡﹴ+=ﹰﹸ)-27],ﱡء[ﱡﹴ-=26]));else if(ﹰﹸ<23){if(ﹰﹸ>21)ﱡء[++ﱡﹴ+--ﹰﹸ-24]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()!==ﱡﹴ()}}(ﱡء[ﱡﹴ--+ ++ﹰﹸ-25],ﱡء[--ﱡﹴ+ﹰﹸ-22]);else return ءﱡ.\ufc5e\ufe70=ﱡء[ﱡﹴ-ﹰﹸ--+20](),ءﱡ.\ufc63\ufe74=ﱡء[++ﱡﹴ+ﹰﹸ-23](),ءﱡ.\ufe7c\ufc60=0;}else 24>ﹰﹸ&&(ﱡء[++ﱡﹴ-1]=function(){return function(){return \u0041\u0072\u0072\u0061\u0079}}(),!0)||(ﱡء[(ﱡﹴ-=++ﹰﹸ)+19]=function(ﱡء,ﱡﹴ,ﹰﹸ,ﹼﹶ,ﹰﱞ,ﹰء){return function(){return ﹰﱣ(ﱡء(),ﱡﹴ(),ﹰﹸ(),ﹼﹶ())(ﹰﱞ())(ﹰء())}}(ﱡء[ﱡﹴ++-ﹰﹸ--+45],ﱡء[--ﱡﹴ+--ﹰﹸ-4],ﱡء[--ﱡﹴ-ﹰﹸ+45],ﱡء[--ﱡﹴ-++ﹰﹸ+48],ﱡء[--ﱡﹴ-ﹰﹸ+++50],ﱡء[(ﱡﹴ+=23)-ﹰﹸ+29]))}(5162)})(function(ﱡﹶ,ﹲ‌,ﹲﹰ,ﹸﹼ,ﹰﱣ,ﹰﹼ){try{return ﹲ‌=function(ﱡﹶ,ﱡﹲ){try{return ﱡﹶ()}catch(ﱣﹰ){return ﱡﹲ()}},ﹲﹰ=function(ﱡﹶ){if(typeof \u0061\u0074\u006f\u0062==="function")return ﹲ‌(function(ﱡﹲ,ﹴﱠ,ﹸﹸ){for(ﱡﹲ=\u0061\u0074\u006f\u0062(ﱡﹶ),ﹴﱠ=[],ﹸﹸ=0;ﹸﹸ<ﱡﹲ.\u006c\u0065\u006e\u0067\u0074\u0068;ﹸﹸ++)ﹴﱠ.\u0070\u0075\u0073\u0068(ﱡﹲ.\u0063\u0068\u0061\u0072\u0043\u006f\u0064\u0065\u0041\u0074(ﹸﹸ)&255);return ﹴﱠ},function(){return ﹰﱣ(ﱡﹶ)});return ﹰﱣ(ﱡﹶ)},ﹸﹼ=function(ﱡﹶ,ﱡﹲ,ﹴﱠ,ﹸﹸ,ءﱟ){ﹴﱠ=0,ﹸﹸ=0;do ءﱟ=ﱡﹶ[ﱡﹲ.\u0069++],ﹴﱠ+=(ءﱟ&127)*\u004d\u0061\u0074\u0068.\u0070\u006f\u0077(2,ﹸﹸ),ﹸﹸ+=7;while(ءﱟ&128);return ﹴﱠ},ﹰﱣ=function(ﱡﹶ,ﱡﹲ,ﹴﱠ,ﹸﹸ,ءﱟ,ﹶﹲ,ﹶﹰ,ﹸﱡ,ﱣﹲ){ﱡﹲ="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",ﹴﱠ=[],ﹸﹸ=0;while(ﹸﹸ<ﱡﹶ.\u006c\u0065\u006e\u0067\u0074\u0068)ءﱟ=ﱡﹲ.\u0069\u006e\u0064\u0065\u0078\u004f\u0066(ﱡﹶ.\u0063\u0068\u0061\u0072\u0041\u0074(ﹸﹸ++)),ﹶﹲ=ﱡﹲ.\u0069\u006e\u0064\u0065\u0078\u004f\u0066(ﱡﹶ.\u0063\u0068\u0061\u0072\u0041\u0074(ﹸﹸ++)),ﹶﹰ=ﱡﹲ.\u0069\u006e\u0064\u0065\u0078\u004f\u0066(ﱡﹶ.\u0063\u0068\u0061\u0072\u0041\u0074(ﹸﹸ++)),ﹸﱡ=ﱡﹲ.\u0069\u006e\u0064\u0065\u0078\u004f\u0066(ﱡﹶ.\u0063\u0068\u0061\u0072\u0041\u0074(ﹸﹸ++)),ﱣﹲ=ءﱟ<<18|ﹶﹲ<<12|(ﹶﹰ&63)<<6|ﹸﱡ&63,ﹴﱠ.\u0070\u0075\u0073\u0068(ﱣﹲ>>16&255),ﹶﹰ!==64&&ﹴﱠ.\u0070\u0075\u0073\u0068(ﱣﹲ>>8&255),ﹸﱡ!==64&&ﹴﱠ.\u0070\u0075\u0073\u0068(ﱣﹲ&255);return ﹴﱠ},ﹰﹼ=function(ﱡﹶ,ﱡﹲ,ﹴﱠ,ﹸﹸ){ﱡﹲ=ﹲﹰ(ﱡﹶ),ﹴﱠ=[],ﹸﹸ={\u0069:0};while(ﹸﹸ.\u0069<ﱡﹲ.\u006c\u0065\u006e\u0067\u0074\u0068)ﹴﱠ.\u0070\u0075\u0073\u0068(ﹸﹼ(ﱡﹲ,ﹸﹸ));return ﹴﱠ},ﹰﹼ(ﱡﹶ)}catch(ﱣﹰ){return[]}}("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"),[[56779,33889,57385,33238,2760],[34585,56229,22885],[13633,31959],[1028,44638,35499,22393,15640,58684],[8431,49946],[48899,42302,52007,43410,42837,52164,62605,30028],[18818,6312,35100,10506,30122,13314,32831,8944],[64664,60832,43717],[32800,28616,30454,60756],[11608,31994,37318,53363,15548,9556,51266],[16202,25333,13050,63417,49095,41578,25384],[44054,52144],[10950,52060,26717,2319],[6553,22690,33479,65396,3502,46191],[64779,39730,47494],[16051,60739,12868,12779,10178,20076],[61245],[15166],[47999],[25842],[63716],[16827],[35635,27012],[3584],[4567,28791,29959,12071,10748,14359],[41487,49204],[31489,61270,690,57706,15240,9853,50871,42840,49102,44648,20069,28470,5748,29947,60650,36698,30558,6039,40102,50821,819,57840,54946,40285,37217,12311],[56341,34740,36923],[7314],[42373,17576,1889],[11788,19505,22439,3602,1845,42372,19264,6897,26709,31390,21776,3264,49492,41207,3553,29710,14066,10940,23664,64169,20689,8024],[15644,9394,11152,19051,4645,28674],[54554,40148,11608,21635],[54131,38784,28849],[58896,33996],[25938,2024,63771],[27238,63639,39001],[5118,45648,23861],[31191,63569],[15365,59920],[47723,59526,10510,18281,63995,26686,57135,33762,2209,64274,61303,37880,31221,59559,49364,41307,63848,61396,7046,65079,41328,16441,35577,22926,3514,31867,47888],[23280,14594,57417,34535,18973,6540],[42540,19671,27271,15269,22124,5371,57378],[301,41221,51132],[48649,58785,28897,8047,62892],[35504,22420,37097,24391],[8903,20331,56243,34364,43805,22327,1348,31982,63768,39168,62227,40849],[49423,41432],[28430,5052,18276,26498,8728,49212,48464,48131],[55196,35393,62378,63059],[57056,34143],[2521,55445,15089,26293],[29316,5892,31997,58534,195,24954,36730,62147,33376],[54941,49051,525],[5096,62357,25633,1742,59366,34321,29977,13470,55270,35350],[38199,30145,37530,54059,8988,20235,16466,9896],[59027],[6820,47432,23797,32672,45781,44804,57414,33271,23101,4082,976,58036],[3138,48616,32216,31870,21923,13339,53103,38425],[34695,14902,59149],[20706,5993,30425],[1906,28569,56864,34013,21562,13305,34522],[57186,33513],[54692,29770,19149,26989,56630,36321,62110,38660,37791],[23882,1265],[19773,12275,53874,37140,57817,32910],[20214,4900,45915,21187,31484],[27084,30826,62528,37575,3217],[58481,33748,17509],[64591,42775,6253],[24714,1704,32948,57712,27296,6478,40350,19543,4992,63035,27380,2375,28931,4404,5684,48328],[59567,39241,55719,38996,3126,21919,10977,64184,26422,2808],[32711,57983,28373,3479,3853,21309,18134],[47122,48676,24375,15857,52735],[33916,9501,56165,35749],[21407,3612,29615,13243,43362,63511],[57326,33175],[38770,23979,41076,49887,34215,27724,40933,65121,64466,48741,10585,55755,55649,35022,50458,41784],[56700],[17551],[4948,53707,38021,57127,50985,65227,25982,3085,4792,57191,30976],[29006,16353],[],[3067,23451],[54110,45469,34240,60524,59467],[19380,10885,59895,34909,17964,9227,6800,22831,14155,63199],[18852,59568,43054,24575,19849,11299,7112,47721,62648,65371,11235,22017],[36634,54723,15525,32074,27542,2449,6885,57163],[64004,59441],[60174,35112,25424],[60854],[15336,9811,49504,41453,61639],[31369],[26254,2906,32129,61355],[59080,32100,13994,23368,33018,8609],[59985,47072],[45670,20609],[1538,42027,39809,52118],[26475,6849,57027],[7731,52457,5843,55067,58319,34338,59954,47519,25457,648,24145,15591,5692,47086],[60110,35107,17447,25230,33713,58049,36669,30179,16582,32588,43154,31110],[32181,60541],[65131,64979,22173,3214,47557],[1952,42582,22848],[61011,38025],[28900,12704,54982,35699,2633,55782,41969,20064,36834,12678,22369,7832,10899,18694,31118,10268,45802,54051,38854,19025,39401,14454,47242,47449,16205,25239,60996,37819,45817,57151,31063,30670,3861,54990,38389,15336,12788,53392,47521,43040,52692,39950,15892,12257,27288,30565,7430,17549,48847,25391,41846,17921,59580,39767,25818,1960,16346,58035,23048,6363,17814,9237,58534,33549,50484,44174,36535,13820,7100,51104,20615,12606,63983,43444,9866,19717,22588,14648,8177,17332,65495,58111,15097,15090,21908,7045,18192,59997,22103,3215,29505,4488,14745,59392,47860,47866,2071,27358,63371,48070,46910,47049,43766,22149,36031,64849,63111,40308,26905,6191,11132,51729,7559,17428,48101,55976,62367,44550,43278,18663,43395,47147,13477,27850,42453,56298,36709,60948,31740,11734,14228,10801,50498,41937,53513,61869,10568,18551,48437,25640,55252,39819,11956,13239,32919,8401,12389,22398,51344,59332,47075,31106,40270,50373,36382,24405,28214,65406,46886,59943,8276,53162,35447,23332,9872,32023,5125,54096,15407,11123,9243,17258,6852,17989,45140,45242,56411,40146,8699,16646,17334,9539,19763,27664,18075,10149,6792,23205,30779,14169,36564],[8921,49441,65483,25175,4050,44593,29341,4501,21465,30291],[48562,42080,2120],[43725],[4517,53335],[19487,10910],[21711],[24635,757,31830,9955],[31900],[34525,15697,51976,46854,30447,11561],[52104,64015,64341,43712,29816],[36997,21303,13624,29830,52447],[58500],[21994,5133,49861],[25191,1008],[3849,30127,34814,26168,22169,2825],[6806,19386,18385,10840,28572,4732,36738,53783,35436,10516,57518,33084,28427,64813,33857,8927,21084,13301],[2473,43059],[45218,30546,63835,39833],[47323,55430,53407,37665],[52710,44156],[27862],[30736,6063,25333,955,27160,14373,56781,35940,15689,58827,42592,31945],[13171,28364,23251,63745,26531],[21773,4897,40334],[22676,14994,6557,51248],[5120,21049,12913,20699,39120,52094,23474],[24202,7330,56800,35954,55822,47292],[44417,63398,10528],[48952,45049,56904,33555],[64490,39472,61700,46015,30861,64171,5908,57026,52397],[29954,13759,49333,41792,34716],[23773],[28013,62602,16369,24141],[25327,794,1862,59054,5455,46309],[32678,11843,39083,14155,6018,24233,53322,29422,64386,44592],[63622,40746,33375,8973,30574],[6082,30303,56622],[19967,5305,33172,8245,15275],[8667,49308,2910],[37874],[54793,39972,13143,20950,26082,3147,47357,60194],[17249,25331],[14674,10492,10331,51694,19034,10741,27414],[13817,27699,61835,37301,32244,15755,54305,30216,28824,4406],[23578,3491,211,57726],[33626,10220,14052,13698,16459],[42251,19629,15384,10812,14027,15211,53864,37096,38265],[60147,35099],[39963,18102,576,41162,61311,37609,3065,44551,40604,17628,46706,31970],[56944,33741,18465,6551],[49345],[20620,12577,31360],[32134],[14248,30230,39255],[61891,36945],[15545,9502,41645,49948,59290,39430],[57709,32784],[20823,4595,5917],[3051,43610,7104,50809,56108],[33788],[21600,62418],[46505,44106],[49958,42966,28071,13731,59766,34838,7067,50696,62461,60971],[1431,60507,10266],[40259,24329],[58318,33396,29220],[48264],[10693,51319,13762],[10645,55355,29420,4881,29144,4124,28587],[27966],[62809,29695,37520,53051,17412,8744],[1945,24216,16411],[60155,63357,37395,12432,7908,48418,37938],[43732,55125,29541,29136],[29160,4160,40726,49857],[56029,34462],[64791,58653,59307,34376,13786,60419,22122],[37338,28790,28638,63116],[8849,65444,25106],[49154,59045,4542,61536,14530],[52655],[43914,46594,49539,41005,37745],[63665,63856,8327,49842,400],[33264,24606,15233,59939,56538],[12954,12436,12416,53537,42902,48565],[49353,24901],[37145],[60906,46159],[47041,63096,46522],[53404,37155],[49293,26385],[44234,30562,19425],[48607,42398,9194,24153,43882,51614,55452,31557,54706,48188,7190,19862,64239,43392,42911,50753,36389,60860,27426,31679,45388,53437],[38977,52973,45970,46722,22597],[48565],[24875,205,60588],[26329,1346,46491,21514],[50749,58613,44474,62570,22457,6782,43630,51401,18974,14803],[50013],[63173,27491,5364,19868,24997,69],[52105,43572,61330],[43386,22966],[33523,8840],[35587,10554],[46200,48769,52874],[7907],[35149,26762],[22788,2226,29736],[38593,51985],[41693,49426,3647],[13224,62986,10068],[60802,35956],[16148,15628],[25427,745,33594],[62952,27725],[23532,14923,64658,43782],[6056,23450],[64287,42754,37554,55151,39923],[23729,1323],[5264,55167],[40254,52117,53198,46738],[32322],[29066,4129,20878,4140,37906],[46599,46505,8234],[2,61117,53956],[61050,38782],[44054,31398],[11452,23893,49955,45014],[49228,45025,43730,52111,48303,43853,30196,13319],[18487,9747,65256,59206,57960],[5340,32026],[28684,65444,36500],[48619,44108,54732,62570,39629,55580],[54871,48543,688,24906,25144,1,37664,13015,5180],[10340,56013,49787,41177,21809,63275],[46363,27930,23847,3334,24835,290,29928,7494,55702,34871,23929,7202,9920,51063,41610],[46030,61047,8757,16839,53037,38360,6538],[14952,27078,4004],[2708,47256,31719],[7749,17916,19046,6291],[45255,54119,29295,4554,45129,22223],[61824,36899,51495,43401,43936,22080],[47872,26410,27854,2923,14827,10639],[61745,61619],[29140],[48960,56812,24276,667,10354,16140],[36876,61032],[61374,36442,61275],[13681,62937,64459],[11171,15894],[4598,53326,13932,15287,31855],[28759,12567,56805,33868,20006],[22367,3541],[36221,29850,3193,54963,11168],[5059,62046],[39147,14659,27157,63546,18021,27602,40906],[22834,2244,58123,33032,39964,31381,43109,56971,15891,9227,18775,10125],[31864,9872,64791,58554,6215,51680,64334],[4677,46064,24292,832,30885,10636,33423,24876,18835],[64303,42733,934,41555,17327,8811,25365,471,3059,46758],[43188,18776,48028,26292,43701],[14167,62954,27789,14103,38327,54373,38654,19238],[61679,45375,36497,60708],[5921,57227],[31240],[18855,63563,28167,15413,8278,49912,56065,35567,62824,48574,50954,39722,47975,27101,10034,23476],[49891,41223,3024,54908,36401,60897,49623,41031,40553,17541,1045,56723,4093],[11085,55011,6710,18683,16383,8765,47313,59771,10477,18772],[32526,10020],[62162,37742],[17896,58446],[55402,39552],[21456,12925],[63006,64982,65482],[18768,6347,3241,21839,53591,37109,31810],[39079,16027],[47076,43590],[33627,25026,27748,3832,22367],[35227,10327,60270,46647],[19794,11738,61014,36316,51158],[5605,54336,1750],[10438,51026,7494,17838],[38016,21944],[42811,52670,32002,11493,33836,58886,13109,12694,29423,4441,47889,42684],[47593,43073],[64352,47617],[34304,11448,13184],[40004,17908,18811,6202,11583],[61560,37366],[6547],[41874,49721,30175,11268,25285,867,49336,57697,35843,27561,63049,29845,10357,18891,23121],[42318,17541,53056,38634,24755,285,58381],[56019,35711,14729,10293,51637,38943],[33238,8288],[40999,52893,39888,50795],[18866,6208,52723,38042,48670],[40407,52321,4489,53428,58989,33933],[22665,6447,57608,33702,53996,29373],[43640,22950,54326,37407,25755,3406,56623],[50782,47897,22248,3329,38114,21824,4724,45210,32634,7612],[61951,37013,33392],[64795,26422],[11521,19745,61727,38835,53047],[22602,5869,20922],[8701,16409,37632,53536,57018],[56181,35420,56503,35682,38602,53051,4934,53996,29976,5294,35653,28346,24821,301,48517],[32563,8932,46770,23317,55613,31725,59060,34115,20416,12914,41407,49260,39764,23041,64136,59728,4958,53961,6044],[56272,35430,55097,40370,25130,142,43605,23307,56474,35477,57038,36206,21602,4800,11568,52449],[32202,11368,35862],[10982,22792,10451,19225,50510,25851,19906,5233],[9008,49806],[38448,54518,21740,3349,39196,22921],[12137,30351,15354,10808,14979,59851],[18491,11906,15384],[53438,37246,16075,9590,33984,10107,62102,32522],[29108,12388,62748,29628],[50023,42701],[45839,22463,42338,50126,9733,17456,10680,14444,59981,36817,25658],[7259],[20925,12387,12203,12942,32762,58939,13552,54598,42111,50095,15897],[11004,55596,23371,1783,2872,23000,10601,51346],[28481,13068,54717],[19488,60382,13080,29654,27977,14249,54542,38180,34888,16114,22998,2100,18302,27183],[20408,29301],[47088,61083],[32142,58555,60798],[933,24963],[14916,10479,48652,60580,63765,28421,24408,1489,3474,44069,12826,22411],[54695,29827,5660,21561,25077,84],[30681,13939],[21993,5123,37108,20664,31909,9604,15839,25992,37218,61571,27936,13716,34448,11084,49205,42505,5816],[22212,32542,43781],[8973,49946,63393,27568,35358,26672,64963,40001,65186,58724],[17735],[56541,35684,65119,31993,34991,59263],[37907,62106,65287,58034,11144],[46111,44741,29147],[54594,36326,431],[8655,16490,27843,62821,42812,23194,41130,49500],[978,25209,35837,10923,16,40488,29632,13949,45488],[42193,50040,18411],[27334,6521,24920,230,32060,60373],[46726,54735],[37766,30627],[38885,20046,54901],[55731,63556,32556,9932,57514],[58361,40575,55822,35286,5420,46315,54929,40853,7242,56053,25280,1911,55210],[46188,45006,50132,41614,51048,26120,32775],[21846,3527],[23027,6286,24070,938,21718,4962,505,57529,31966,6993,19960,5046,46570],[1235,25449,1900,64244],[56683,40131,47352],[53148,62034,36655],[21426,5001,29372],[41152,51056],[6622,47496,59491,40865,26098],[23377,2782],[4779,54039,17682,15667,45148,29667,22830,2288],[16105],[2557,43046,58042,34652,22556,6610],[35523,26997,52286,60876,61368,37396],[20346,7903,41914,49553,10129,18984],[40209,17725,9992,56098],[24329,805,52720,31768,25156,247,64317,59038,49659,41011],[14134,23808,3680,44536,30887],[59431,39414,61585,45890,45434],[62055,46988,23859,1809],[50528,44507,315,41208,49875,41331,41940,18032,11493,21780],[25096,21134,33167,41410,55027,53094,35843,55303,26121],[36604,56579],[22948,33756,61271,45785],[43169,14808],[48357,12140,34325,65510,48508],[30805,6650,14494,15164],[59019],[36321,54395,22137],[57076,34636,44984,46659],[9805,50420,29581,11781,63849,30873,54287,45953],[62791,37863],[8040,50831,24511,7805,62320,37284,45269],[65306,59028,33042,25048,9567],[48478,31734,24739,345],[48744,42136,19087,14509],[10081,50893,693],[14132,22262],[14849,27051,41266,50972,7348,17757,20395],[17714,7451,34160,25658,65253],[59076,34168,4966],[56241,35437,5752,55069,14404,59893,30575,5663],[3602,30122,29355,61577,47803],[24330,693],[37912,12848],[26702,2545,38396,21590],[46217,29365],[24369,758,39658],[2268,42817,25223,1955,39575,22849],[35105,63765],[22518,5683,10732,22670,63427,30321,40757,15775],[50984,49118,51163],[6282,18739,30069],[54772,40088,16388,8746],[1814,47923,9456],[13121,20904,55782,38988],[31965,15628,9974,23874,17385,9812,31716,61011,21144],[46611,56367],[56294,40456,60903],[52814,28665,37110],[9290,32755,50286,58072,61330,37505,13704,54355,45693,45522,61280,37574,64106,39092,41167],[29511],[4245,54097,36303,54385,8178],[42984,65109,29212,61443,3136,32502],[1084,32451,1212,25443,34900,27606,64055,40920,37110,28948,23381,3609],[52968,38540,41585,49448,43016,55736,64203],[12209,54859,20377,29267]],function(){try{return this||typeof \u0067\u006c\u006f\u0062\u0061\u006c\u0054\u0068\u0069\u0073==="object"&&\u0067\u006c\u006f\u0062\u0061\u006c\u0054\u0068\u0069\u0073||typeof \u0073\u0065\u006c\u0066==="object"&&\u0073\u0065\u006c\u0066||typeof \u0077\u0069\u006e\u0064\u006f\u0077==="object"&&\u0077\u0069\u006e\u0064\u006f\u0077||typeof \u0067\u006c\u006f\u0062\u0061\u006c==="object"&&\u0067\u006c\u006f\u0062\u0061\u006c||\u0046\u0075\u006e\u0063\u0074\u0069\u006f\u006e("return this")()}catch(ﱣﹰ){return{}}}(),{},\u004f\u0062\u006a\u0065\u0063\u0074.\u0063\u0061\u006c\u006c.\u0062\u0069\u006e\u0064(\u0041\u0072\u0072\u0061\u0079.\u0062\u0069\u006e\u0064),\u0041\u0072\u0072\u0061\u0079.\u0061\u0070\u0070\u006c\u0079.\u0062\u0069\u006e\u0064(\u004e\u0075\u006d\u0062\u0065\u0072.\u0062\u0069\u006e\u0064),\u004e\u0075\u006d\u0062\u0065\u0072.\u0062\u0069\u006e\u0064.\u0062\u0069\u006e\u0064(\u004f\u0062\u006a\u0065\u0063\u0074.\u0062\u0069\u006e\u0064),\u004f\u0062\u006a\u0065\u0063\u0074.\u0063\u0061\u006c\u006c.\u0062\u0069\u006e\u0064([].\u0070\u0075\u0073\u0068),\u0041\u0072\u0072\u0061\u0079.\u0063\u0061\u006c\u006c.\u0062\u0069\u006e\u0064([].\u0063\u006f\u006e\u0063\u0061\u0074),\u004e\u0075\u006d\u0062\u0065\u0072.\u0061\u0070\u0070\u006c\u0079.\u0062\u0069\u006e\u0064(\u0053\u0074\u0072\u0069\u006e\u0067.\u0066\u0072\u006f\u006d\u0043\u0068\u0061\u0072\u0043\u006f\u0064\u0065,null),\u004f\u0062\u006a\u0065\u0063\u0074.\u0064\u0065\u0066\u0069\u006e\u0065\u0050\u0072\u006f\u0070\u0065\u0072\u0074\u0079.\u0062\u0069\u006e\u0064(\u004f\u0062\u006a\u0065\u0063\u0074),\u004f\u0062\u006a\u0065\u0063\u0074.\u0067\u0065\u0074\u004f\u0077\u006e\u0050\u0072\u006f\u0070\u0065\u0072\u0074\u0079\u0044\u0065\u0073\u0063\u0072\u0069\u0070\u0074\u006f\u0072.\u0062\u0069\u006e\u0064(\u004f\u0062\u006a\u0065\u0063\u0074),\u0052\u0065\u0067\u0045\u0078\u0070,function(){return typeof \u0042\u0069\u0067\u0049\u006e\u0074!=="undefined"?\u0042\u0069\u0067\u0049\u006e\u0074:void 0}(),\u0053\u0074\u0072\u0069\u006e\u0067.\u0066\u0072\u006f\u006d\u0043\u0068\u0061\u0072\u0043\u006f\u0064\u0065,function(ﱡﹲ,ﱡﱣ,ﱞﹶ,ﹲﹰ){return ﱡﹲ=function(){}.\u0063\u0061\u006c\u006c,ﱡﱣ=ﱡﹲ.\u0062\u0069\u006e\u0064("".\u0073\u0070\u006c\u0069\u0074),ﱞﹶ=ﱡﹲ.\u0062\u0069\u006e\u0064([].\u0072\u0065\u0076\u0065\u0072\u0073\u0065),ﹲﹰ=ﱡﹲ.\u0062\u0069\u006e\u0064([].\u006a\u006f\u0069\u006e),function(ﱡء){return ﹲﹰ(ﱞﹶ(ﱡﱣ(ﱡء,"")),"")}}(),[]); diff --git a/musicSdk/api-source-info.js b/musicSdk/api-source-info.js new file mode 100644 index 0000000..97a2568 --- /dev/null +++ b/musicSdk/api-source-info.js @@ -0,0 +1,40 @@ +// Support qualitys: 128k 320k flac wav + +const sources = [ + // { + // id: 'test', + // name: '测试接口', + // disabled: false, + // supportQualitys: { + // kw: ['128k'], + // kg: ['128k'], + // tx: ['128k'], + // wy: ['128k'], + // mg: ['128k'], + // // bd: ['128k'], + // }, + // }, + // { + // id: 'temp', + // name: '临时接口', + // disabled: false, + // supportQualitys: { + // kw: ['128k'], + // }, + // }, + { + id: 'v5', + name: 'lx-music-source-v5 聚合源', + disabled: false, + supportQualitys: { + kw: ['128k', '320k', 'flac'], + kg: ['128k', '320k', 'flac'], + tx: ['128k', '320k', 'flac'], + wy: ['128k', '320k', 'flac'], + mg: ['128k', '320k', 'flac'], + // bd: ['128k', '320k', 'flac'], + }, + }, +] + +export default sources diff --git a/musicSdk/api-source-info.ts b/musicSdk/api-source-info.ts new file mode 100644 index 0000000..65cf20e --- /dev/null +++ b/musicSdk/api-source-info.ts @@ -0,0 +1,32 @@ +// Support qualitys: 128k 320k flac wav + +const sources: Array<{ + id: string + name: string + disabled: boolean + supportQualitys: Partial> +}> = [ + // { + // id: 'test', + // name: '测试接口', + // disabled: false, + // supportQualitys: { + // kw: ['128k'], + // kg: ['128k'], + // tx: ['128k'], + // wy: ['128k'], + // mg: ['128k'], + // // bd: ['128k'], + // }, + // }, + // { + // id: 'temp', + // name: '临时接口', + // disabled: false, + // supportQualitys: { + // kw: ['128k'], + // }, + // }, +] + +export default sources diff --git a/musicSdk/api-source.js b/musicSdk/api-source.js new file mode 100644 index 0000000..c40fca9 --- /dev/null +++ b/musicSdk/api-source.js @@ -0,0 +1,53 @@ +import apiSourceInfo from './api-source-info.js' +import { apiSource, userApi } from '../src/renderer/store.js' +import internalApi from '../src/apis/index.js' +import { createApi as createLxSourceApi } from '../src/apis/lx-source.js' +// import api_temp_kw from './kw/api-temp.js' +// // import api_test_bd from './bd/api-test.js' +// import api_test_tx from './tx/api-test.js' +// import api_test_kg from './kg/api-test.js' +// import api_test_kw from './kw/api-test.js' +// import api_test_mg from './mg/api-test.js' +// import api_test_wy from './wy/api-test.js' + +const allApi = { + v5_kw: createLxSourceApi('kw'), + v5_kg: createLxSourceApi('kg'), + v5_tx: createLxSourceApi('tx'), + v5_wy: createLxSourceApi('wy'), + v5_mg: createLxSourceApi('mg'), + // temp_kw: api_temp_kw, + // // test_bd: api_test_bd, + // test_tx: api_test_tx, + // test_kg: api_test_kg, + // test_kw: api_test_kw, + // test_mg: api_test_mg, + // test_wy: api_test_wy, +} + +const apiList = {} +const supportQuality = {} + +for (const api of apiSourceInfo) { + supportQuality[api.id] = api.supportQualitys + for (const source of Object.keys(api.supportQualitys)) { + apiList[`${api.id}_api_${source}`] = allApi[`${api.id}_${source}`] + } +} + +const getAPI = source => apiList[`${apiSource.value}_api_${source}`] + +const apis = source => { + if (/^user_api/.test(apiSource.value)) return userApi.apis[source] + // 服务器内置直连接口 + if (apiSource.value == 'internal') { + const api = internalApi[source] + if (api) return api + throw new Error('Api is not found') + } + let api = getAPI(source) + if (api) return api + throw new Error('Api is not found') +} + +export { apis, supportQuality } diff --git a/musicSdk/bd/api-test.js b/musicSdk/bd/api-test.js new file mode 100644 index 0000000..0491c48 --- /dev/null +++ b/musicSdk/bd/api-test.js @@ -0,0 +1,26 @@ +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.js b/musicSdk/bd/hotSearch.js new file mode 100644 index 0000000..7e44c48 --- /dev/null +++ b/musicSdk/bd/hotSearch.js @@ -0,0 +1,23 @@ +import { httpFetch } from '../request.js' + +export default { + _requestObj: 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.js b/musicSdk/bd/index.js new file mode 100644 index 0000000..f0c499c --- /dev/null +++ b/musicSdk/bd/index.js @@ -0,0 +1,40 @@ +import leaderboard from './leaderboard.js' +import { apis } from '../api-source.js' +import musicInfo from './musicInfo.js' +import songList from './songList.js' +import { httpFetch } from '../request.js' +import musicSearch from './musicSearch.js' +import hotSearch from './hotSearch.js' + +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.js b/musicSdk/bd/leaderboard.js new file mode 100644 index 0000000..8e82276 --- /dev/null +++ b/musicSdk/bd/leaderboard.js @@ -0,0 +1,153 @@ +import { httpFetch } from '../request.js' +// 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 = [] + const _types = {} + 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.js b/musicSdk/bd/musicInfo.js new file mode 100644 index 0000000..95868c0 --- /dev/null +++ b/musicSdk/bd/musicInfo.js @@ -0,0 +1,20 @@ +import { httpFetch } from '../request.js' + +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.js b/musicSdk/bd/musicSearch.js new file mode 100644 index 0000000..30ace38 --- /dev/null +++ b/musicSdk/bd/musicSearch.js @@ -0,0 +1,88 @@ +// import '../../polyfill/array.find.js' + +import { httpFetch } from '../request.js' +import { formatPlayTime } from '../index.js' +// 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 = [] + if (!rawData) return list + rawData.forEach(item => { + if (ids.has(item.song_id)) return + ids.add(item.song_id) + const types = [] + const _types = {} + 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.js b/musicSdk/bd/songList.js new file mode 100644 index 0000000..3f815d2 --- /dev/null +++ b/musicSdk/bd/songList.js @@ -0,0 +1,263 @@ +import { httpFetch } from '../request.js' +import { formatPlayTime } from '../index.js' +import { toMD5 } from '../utils.js' +import CryptoJS from 'crypto-js' + +export default { + _requestObj_tags: null, + _requestObj_list: null, + _requestObj_listRecommend: 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 = [] + 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 = { + 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) / 10 + '亿' + if (num > 10000) return parseInt(num / 1000) / 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 = [] + const _types = {} + 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/index.js b/musicSdk/index.js new file mode 100644 index 0000000..bd0d80d --- /dev/null +++ b/musicSdk/index.js @@ -0,0 +1,180 @@ +import kw from './kw/index.js' +import kg from './kg/index.js' +import tx from './tx/index.js' +import wy from './wy/index.js' +import mg from './mg/index.js' +import bd from './bd/index.js' +import xm from './xm.js' +import { supportQuality } from './api-source.js' +import { + sizeFormate, + formatPlayTime, + dateFormat, + dateFormat2, + formatPlayCount, + decodeName, +} from '../src/index.js' + +// 通用工具函数(SDK 内 `import { ... } from '../index.js'` 的解析目标) +export { sizeFormate, formatPlayTime, dateFormat, dateFormat2, formatPlayCount, decodeName } + +const SINGERS_SEPARATOR = /、|&|;|;|\/|,|,|\||\]|\[|!|!/g +const SUPPORTED_SOURCES = [ + { id: 'kw', name: '酷我音乐', module: kw }, + { id: 'kg', name: '酷狗音乐', module: kg }, + { id: 'tx', name: 'QQ音乐', module: tx }, + { id: 'wy', name: '网易音乐', module: wy }, + { id: 'mg', name: '咪咕音乐', module: mg }, + { id: 'xm', name: '虾米音乐', module: xm }, + // { id: 'bd', name: '百度音乐', module: bd }, +] + +const SOURCE_MODULES = Object.fromEntries(SUPPORTED_SOURCES.map((s) => [s.id, s.module])) + +export const sources = SUPPORTED_SOURCES.map(({ id, name }) => ({ id, name })) + +export const init = () => + Promise.all(SUPPORTED_SOURCES.map((s) => s.module.init?.()).filter(Boolean)) + +export { supportQuality } + +const trimStr = (str) => (typeof str === 'string' ? str.trim() : str) + +const filterStr = (str) => + typeof str === 'string' + ? str.replace(/\s|'|\.|,|,|&|"|、|\(|\)|(|)|`|~|-|<|>|\||\/|\]|\[|!|!/g, '') + : String(str ?? '') + +const getIntervalSeconds = (interval) => { + if (!interval) return 0 + const parts = String(interval).split(':') + let seconds = 0 + let unit = 1 + while (parts.length) { + seconds += parseInt(parts.pop()) * unit + unit *= 60 + } + return seconds +} + +const sortSingle = (singer) => { + if (!singer) return '' + return SINGERS_SEPARATOR.test(singer) + ? singer.split(SINGERS_SEPARATOR).sort((a, b) => a.localeCompare(b)).join('、') + : singer +} + +/** + * 多平台聚合搜索 + * @param {object} params + * @param {string} params.name + * @param {string} [params.singer] + * @param {string} [params.source] 指定平台时,聚合结果会跳过该平台 + * @param {number} [params.limit=25] + */ +export const searchMusic = async ({ name, singer, source: excludeSource, limit = 25 }) => { + const musicName = trimStr(name) + const tasks = [] + for (const source of SUPPORTED_SOURCES) { + if ( + source.id === 'xm' || + source.id === excludeSource || + !source.module.musicSearch + ) + continue + tasks.push( + source.module.musicSearch + .search(`${musicName} ${singer || ''}`.trim(), 1, limit) + .catch(() => null) + ) + } + return (await Promise.all(tasks)).filter(Boolean) +} + +const pickMatching = (items, predicate) => { + const matched = [] + const rest = [] + for (const item of items) { + if (predicate(item)) matched.push(item) + else rest.push(item) + } + return { matched, rest } +} + +/** + * 跨平台精确匹配歌曲(按歌手、歌名、专辑、时长综合排序) + */ +export const findMusic = async ({ name, singer, albumName, interval, source: s }) => { + const lists = await searchMusic({ name, singer, source: s, limit: 25 }) + + const fMusicName = filterStr(name).toLowerCase() + const fSinger = filterStr(sortSingle(singer)).toLowerCase() + const fAlbumName = filterStr(albumName).toLowerCase() + const fInterval = getIntervalSeconds(interval) + + const isEqualsInterval = (intv) => + Math.abs((fInterval || intv) - (intv || fInterval)) < 5 + const isIncludesName = (n) => fMusicName.includes(n) || n.includes(fMusicName) + const isIncludesSinger = (sng) => + fSinger ? fSinger.includes(sng) || sng.includes(fSinger) : true + const isEqualsAlbum = (alb) => (fAlbumName ? fAlbumName === alb : true) + + let candidates = [] + for (const source of lists) { + for (const item of source.list) { + item.name = trimStr(item.name) + item.singer = trimStr(item.singer) + item.fSinger = filterStr(sortSingle(item.singer).toLowerCase()) + item.fMusicName = filterStr(String(item.name ?? '').toLowerCase()) + item.fAlbumName = filterStr(String(item.albumName ?? '').toLowerCase()) + item.fInterval = getIntervalSeconds(item.interval) + if (!isEqualsInterval(item.fInterval)) { + item.name = null + continue + } + candidates.push(item) + } + } + + const result = [] + const take = (predicate) => { + const { matched, rest } = pickMatching(candidates, predicate) + result.push(...matched) + candidates = rest + } + + take((item) => item.fSinger === fSinger && item.fMusicName === fMusicName && item.interval === interval) + take((item) => item.fMusicName === fMusicName && item.fSinger === fSinger && item.fAlbumName === fAlbumName) + take((item) => item.fSinger === fSinger && item.fMusicName === fMusicName) + take((item) => item.fMusicName === fMusicName && item.interval === interval) + take((item) => item.fSinger === fSinger && item.interval === interval) + take((item) => item.interval === interval) + take((item) => item.fMusicName === fMusicName) + take((item) => item.fSinger === fSinger) + take((item) => item.fAlbumName === fAlbumName) + + for (const item of candidates) { + delete item.fSinger + delete item.fMusicName + delete item.fAlbumName + delete item.fInterval + } + for (const item of result) { + delete item.fSinger + delete item.fMusicName + delete item.fAlbumName + delete item.fInterval + } + + result.push(...candidates) + return result +} + +export default { + sources, + ...SOURCE_MODULES, + init, + supportQuality, + searchMusic, + findMusic, +} diff --git a/musicSdk/kg/album.js b/musicSdk/kg/album.js new file mode 100644 index 0000000..d19d5ab --- /dev/null +++ b/musicSdk/kg/album.js @@ -0,0 +1,63 @@ +import { getMusicInfosByList } from './musicInfo.js' +import { createHttpFetch } from './util.js' + +export default { + /** + * 通过AlbumId获取专辑信息 + * @param {*} id + */ + async getAlbumInfo(id) { + const albumInfoRequest = await createHttpFetch('http://kmrserviceretry.kugou.com/container/v1/album?dfid=1tT5He3kxrNC4D29ad1MMb6F&mid=22945702112173152889429073101964063697&userid=0&appid=1005&clientver=11589', { + method: 'POST', + body: { + appid: 1005, + clienttime: 1681833686, + clientver: 11589, + data: [{ album_id: id }], + fields: 'language,grade_count,intro,mix_intro,heat,category,sizable_cover,cover,album_name,type,quality,publish_company,grade,special_tag,author_name,publish_date,language_id,album_id,exclusive,is_publish,trans_param,authors,album_tag', + isBuy: 0, + key: 'e6f3306ff7e2afb494e89fbbda0becbf', + mid: '22945702112173152889429073101964063697', + show_album_tag: 0, + }, + }) + if (!albumInfoRequest) return Promise.reject(new Error('get album info failed.')) + const albumInfo = albumInfoRequest[0] + + return { + name: albumInfo.album_name, + image: albumInfo.sizable_cover.replace('{size}', 240), + desc: albumInfo.intro, + authorName: albumInfo.author_name, + // play_count: this.formatPlayCount(info.count), + } + }, + /** + * 通过AlbumId获取专辑 + * @param {*} id + * @param {*} page + */ + async getAlbumDetail(id, page = 1, limit = 200) { + const albumList = await createHttpFetch(`http://mobiles.kugou.com/api/v3/album/song?version=9108&albumid=${id}&plat=0&pagesize=${limit}&area_code=0&page=${page}&with_res_tag=0`) + if (!albumList.info) return Promise.reject(new Error('Get album list failed.')) + + let result = await getMusicInfosByList(albumList.info) + + const info = await this.getAlbumInfo(id) + + return { + list: result || [], + page, + limit, + total: albumList.total, + source: 'kg', + info: { + name: info.name, + img: info.image, + desc: info.desc, + author: info.authorName, + // play_count: this.formatPlayCount(info.count), + }, + } + }, +} diff --git a/musicSdk/kg/api-test.js b/musicSdk/kg/api-test.js new file mode 100644 index 0000000..53b79e9 --- /dev/null +++ b/musicSdk/kg/api-test.js @@ -0,0 +1,50 @@ +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/comment.js b/musicSdk/kg/comment.js new file mode 100644 index 0000000..3726a0a --- /dev/null +++ b/musicSdk/kg/comment.js @@ -0,0 +1,101 @@ +import { httpFetch } from '../request.js' +import { decodeName, dateFormat2 } from '../index.js' +import { signatureParams } from './util.js' +// import { getMusicInfoRaw } from './musicInfo.js' + +export default { + _requestObj: null, + _requestObj2: null, + async getComment({ hash }, page = 1, limit = 20) { + if (this._requestObj) this._requestObj.cancelHttp() + + // const res_id = (await getMusicInfoRaw(hash)).classification?.[0]?.res_id + // if (!res_id) throw new Error('获取评论失败') + + let timestamp = Date.now() + const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0` + // const params = `appid=1005&clienttime=${timestamp}&clienttoken=0&clientver=11409&code=fc4be23b4e972707f36b8a828a93ba8a&dfid=0&extdata=${hash}&kugouid=0&mid=16249512204336365674023395779019&mixsongid=${res_id}&p=${page}&pagesize=${limit}&uuid=0&ver=10` + const _requestObj = httpFetch(`http://m.comment.service.kugou.com/r/v1/rank/newest?${params}&signature=${signatureParams(params)}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.24', + }, + }) + const { body, statusCode } = await _requestObj.promise + // console.log(body) + if (statusCode != 200 || body.err_code !== 0) throw new Error('获取评论失败') + const total = body.count ?? 0 + return { source: 'kg', comments: this.filterComment(body.list || []), total, page, limit, maxPage: Math.ceil(total / limit) || 1 } + }, + async getHotComment({ hash }, page = 1, limit = 20) { + // console.log(songmid) + if (this._requestObj2) this._requestObj2.cancelHttp() + let timestamp = Date.now() + const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0` + // https://github.com/GitHub-ZC/wp_MusicApi/blob/bf9307dd138dc8ac6c4f7de29361209d4f5b665f/routes/v1/kugou/comment.js#L53 + const _requestObj2 = httpFetch(`http://m.comment.service.kugou.com/r/v1/rank/topliked?${params}&signature=${signatureParams(params)}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.24', + }, + }) + const { body, statusCode } = await _requestObj2.promise + // console.log(body) + if (statusCode != 200 || body.err_code !== 0) throw new Error('获取热门评论失败') + const total = body.count ?? 0 + return { source: 'kg', comments: this.filterComment(body.list || []), total, page, limit, maxPage: Math.ceil(total / limit) || 1 } + }, + async getReplyComment({ songmid, audioId }, replyId, page = 1, limit = 100) { + if (this._requestObj2) this._requestObj2.cancelHttp() + + songmid = songmid.length == 32 // 修复歌曲ID存储变更导致图片获取失败的问题 + ? audioId.split('_')[0] + : songmid + + const _requestObj2 = httpFetch(`http://comment.service.kugou.com/index.php?r=commentsv2/getReplyWithLike&code=fc4be23b4e972707f36b8a828a93ba8a&p=${page}&pagesize=${limit}&ver=1.01&clientver=8373&kugouid=687373022&need_show_image=1&appid=1001&childrenid=${songmid}&tid=${replyId}`, { + headers: { + 'User-Agent': 'Android712-AndroidPhone-8983-18-0-COMMENT-wifi', + }, + }) + const { body, statusCode } = await _requestObj2.promise + // console.log(body) + if (statusCode != 200 || body.err_code !== 0) throw new Error('获取回复评论失败') + return { source: 'kg', comments: this.filterComment(body.list || []) } + }, + replaceAt(raw, atList) { + atList.forEach((atobj) => { + raw = raw.replaceAll(`[at=${atobj.id}]`, `@${atobj.name} `) + }) + return raw + }, + filterComment(rawList) { + return rawList.map(item => { + let data = { + id: item.id, + text: decodeName((item.atlist ? this.replaceAt(item.content, item.atlist) : item.content) || ''), + images: item.images ? item.images.map(i => i.url) : [], + location: item.location, + time: item.addtime, + timeStr: dateFormat2(new Date(item.addtime).getTime()), + userName: item.user_name, + avatar: item.user_pic, + userId: item.user_id, + likedCount: item.like.likenum, + replyNum: item.reply_num, + reply: [], + } + + return item.pcontent + ? { + id: item.id, + text: decodeName(item.pcontent), + time: null, + userName: item.puser, + avatar: null, + userId: item.puser_id, + likedCount: null, + replyNum: null, + reply: [data], + } + : data + }) + }, +} diff --git a/musicSdk/kg/hotSearch.js b/musicSdk/kg/hotSearch.js new file mode 100644 index 0000000..ff982f8 --- /dev/null +++ b/musicSdk/kg/hotSearch.js @@ -0,0 +1,33 @@ +import { httpFetch } from '../request.js' +import { decodeName } from '../index.js' + +export default { + _requestObj: 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://gateway.kugou.com/api/v3/search/hot_tab?signature=ee44edb9d7155821412d220bcaf509dd&appid=1005&clientver=10026&plat=0', { + method: 'get', + headers: { + dfid: '1ssiv93oVqMp27cirf2CvoF1', + mid: '156798703528610303473757548878786007104', + clienttime: 1584257267, + 'x-router': 'msearch.kugou.com', + 'user-agent': 'Android9-AndroidPhone-10020-130-0-searchrecommendprotocol-wifi', + 'kg-rc': 1, + }, + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.errcode !== 0) throw new Error('获取热搜词失败') + // console.log(body, statusCode) + return { source: 'kg', list: this.filterList(body.data.list) } + }, + filterList(rawList) { + const list = [] + rawList.forEach(item => { + item.keywords.map(k => list.push(decodeName(k.keyword))) + }) + return list + }, +} diff --git a/musicSdk/kg/index.js b/musicSdk/kg/index.js new file mode 100644 index 0000000..60646e8 --- /dev/null +++ b/musicSdk/kg/index.js @@ -0,0 +1,38 @@ +import leaderboard from './leaderboard.js' +import { apis } from '../api-source.js' +import songList from './songList.js' +import musicSearch from './musicSearch.js' +import pic from './pic.js' +import lyric from './lyric.js' +import hotSearch from './hotSearch.js' +import comment from './comment.js' +// import tipSearch from './tipSearch.js' + +const kg = { + // tipSearch, + leaderboard, + songList, + musicSearch, + hotSearch, + comment, + getMusicUrl(songInfo, type) { + return apis('kg').getMusicUrl(songInfo, type) + }, + getLyric(songInfo) { + return lyric.getLyric(songInfo) + }, + // getLyric(songInfo) { + // return apis('kg').getLyric(songInfo) + // }, + getPic(songInfo) { + return pic.getPic(songInfo) + }, + getMusicDetailPageUrl(songInfo) { + return `https://www.kugou.com/song/#hash=${songInfo.hash}&album_id=${songInfo.albumId}` + }, + // getPic(songInfo) { + // return apis('kg').getPic(songInfo) + // }, +} + +export default kg diff --git a/musicSdk/kg/leaderboard.js b/musicSdk/kg/leaderboard.js new file mode 100644 index 0000000..2521010 --- /dev/null +++ b/musicSdk/kg/leaderboard.js @@ -0,0 +1,208 @@ +import { httpFetch } from '../request.js' +import { decodeName, formatPlayTime, sizeFormate } from '../index.js' +import { formatSingerName } from '../utils.js' + +let boardList = [{ id: 'kg__8888', name: 'TOP500', bangid: '8888' }, { id: 'kg__6666', name: '飙升榜', bangid: '6666' }, { id: 'kg__59703', name: '蜂鸟流行音乐榜', bangid: '59703' }, { id: 'kg__52144', name: '抖音热歌榜', bangid: '52144' }, { id: 'kg__52767', name: '快手热歌榜', bangid: '52767' }, { id: 'kg__24971', name: 'DJ热歌榜', bangid: '24971' }, { id: 'kg__23784', name: '网络红歌榜', bangid: '23784' }, { id: 'kg__44412', name: '说唱先锋榜', bangid: '44412' }, { id: 'kg__31308', name: '内地榜', bangid: '31308' }, { id: 'kg__33160', name: '电音榜', bangid: '33160' }, { id: 'kg__31313', name: '香港地区榜', bangid: '31313' }, { id: 'kg__51341', name: '民谣榜', bangid: '51341' }, { id: 'kg__54848', name: '台湾地区榜', bangid: '54848' }, { id: 'kg__31310', name: '欧美榜', bangid: '31310' }, { id: 'kg__33162', name: 'ACG新歌榜', bangid: '33162' }, { id: 'kg__31311', name: '韩国榜', bangid: '31311' }, { id: 'kg__31312', name: '日本榜', bangid: '31312' }, { id: 'kg__49225', name: '80后热歌榜', bangid: '49225' }, { id: 'kg__49223', name: '90后热歌榜', bangid: '49223' }, { id: 'kg__49224', name: '00后热歌榜', bangid: '49224' }, { id: 'kg__33165', name: '粤语金曲榜', bangid: '33165' }, { id: 'kg__33166', name: '欧美金曲榜', bangid: '33166' }, { id: 'kg__33163', name: '影视金曲榜', bangid: '33163' }, { id: 'kg__51340', name: '伤感榜', bangid: '51340' }, { id: 'kg__35811', name: '会员专享榜', bangid: '35811' }, { id: 'kg__37361', name: '雷达榜', bangid: '37361' }, { id: 'kg__21101', name: '分享榜', bangid: '21101' }, { id: 'kg__46910', name: '综艺新歌榜', bangid: '46910' }, { id: 'kg__30972', name: '酷狗音乐人原创榜', bangid: '30972' }, { id: 'kg__60170', name: '闽南语榜', bangid: '60170' }, { id: 'kg__65234', name: '儿歌榜', bangid: '65234' }, { id: 'kg__4681', name: '美国BillBoard榜', bangid: '4681' }, { id: 'kg__25028', name: 'Beatport电子舞曲榜', bangid: '25028' }, { id: 'kg__4680', name: '英国单曲榜', bangid: '4680' }, { id: 'kg__38623', name: '韩国Melon音乐榜', bangid: '38623' }, { id: 'kg__42807', name: 'joox本地热歌榜', bangid: '42807' }, { id: 'kg__36107', name: '小语种热歌榜', bangid: '36107' }, { id: 'kg__4673', name: '日本公信榜', bangid: '4673' }, { id: 'kg__46868', name: '日本SPACE SHOWER榜', bangid: '46868' }, { id: 'kg__42808', name: 'KKBOX风云榜', bangid: '42808' }, { id: 'kg__60171', name: '越南语榜', bangid: '60171' }, { id: 'kg__60172', name: '泰语榜', bangid: '60172' }, { id: 'kg__59895', name: 'R&B榜', bangid: '59895' }, { id: 'kg__59896', name: '摇滚榜', bangid: '59896' }, { id: 'kg__59897', name: '爵士榜', bangid: '59897' }, { id: 'kg__59898', name: '乡村音乐榜', bangid: '59898' }, { id: 'kg__59900', name: '纯音乐榜', bangid: '59900' }, { id: 'kg__59899', name: '古典榜', bangid: '59899' }, { id: 'kg__22603', name: '5sing音乐榜', bangid: '22603' }, { id: 'kg__21335', name: '繁星音乐榜', bangid: '21335' }, { id: 'kg__33161', name: '古风新歌榜', bangid: '33161' }] + +export default { + listDetailLimit: 100, + list: [ + { + id: 'kgtop500', + name: 'TOP500', + bangid: '8888', + }, + { + id: 'kgwlhgb', + name: '网络榜', + bangid: '23784', + }, + { + id: 'kgbsb', + name: '飙升榜', + bangid: '6666', + }, + { + id: 'kgfxb', + name: '分享榜', + bangid: '21101', + }, + { + id: 'kgcyyb', + name: '纯音乐榜', + bangid: '33164', + }, + { + id: 'kggfjqb', + name: '古风榜', + bangid: '33161', + }, + { + id: 'kgyyjqb', + name: '粤语榜', + bangid: '33165', + }, + { + id: 'kgomjqb', + name: '欧美榜', + bangid: '33166', + }, + { + id: 'kgdyrgb', + name: '电音榜', + bangid: '33160', + }, + { + id: 'kgjdrgb', + name: 'DJ热歌榜', + bangid: '24971', + }, + { + id: 'kghyxgb', + name: '华语新歌榜', + bangid: '31308', + }, + ], + getUrl(p, id, limit) { + return `http://mobilecdnbj.kugou.com/api/v3/rank/song?version=9108&ranktype=1&plat=0&pagesize=${limit}&area_code=1&page=${p}&rankid=${id}&with_res_tag=0&show_portrait_mv=1` + }, + regExps: { + total: /total: '(\d+)',/, + page: /page: '(\d+)',/, + limit: /pagesize: '(\d+)',/, + listData: /global\.features = (\[.+\]);/, + }, + _requestBoardsObj: null, + getBoardsData() { + if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp() + this._requestBoardsObj = httpFetch('http://mobilecdnbj.kugou.com/api/v5/rank/list?version=9108&plat=0&showtype=2&parentid=0&apiver=6&area_code=1&withsong=1') + return this._requestBoardsObj.promise + }, + getData(url) { + const requestDataObj = httpFetch(url) + return requestDataObj.promise + }, + getSinger(singers) { + let arr = [] + singers.forEach(singer => { + arr.push(singer.author_name) + }) + return arr.join('、') + }, + filterData(rawList) { + // console.log(rawList) + return rawList.map(item => { + const types = [] + const _types = {} + if (item.filesize !== 0) { + let size = sizeFormate(item.filesize) + types.push({ type: '128k', size, hash: item.hash }) + _types['128k'] = { + size, + hash: item.hash, + } + } + if (item['320filesize'] !== 0) { + let size = sizeFormate(item['320filesize']) + types.push({ type: '320k', size, hash: item['320hash'] }) + _types['320k'] = { + size, + hash: item['320hash'], + } + } + if (item.sqfilesize !== 0) { + let size = sizeFormate(item.sqfilesize) + types.push({ type: 'flac', size, hash: item.sqhash }) + _types.flac = { + size, + hash: item.sqhash, + } + } + if (item.filesize_high !== 0) { + let size = sizeFormate(item.filesize_high) + types.push({ type: 'flac24bit', size, hash: item.hash_high }) + _types.flac24bit = { + size, + hash: item.hash_high, + } + } + return { + singer: formatSingerName(item.authors, 'author_name'), + name: decodeName(item.songname), + albumName: decodeName(item.remark), + albumId: item.album_id, + songmid: item.audio_id, + source: 'kg', + interval: formatPlayTime(item.duration), + img: null, + lrc: null, + hash: item.hash, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + + filterBoardsData(rawList) { + // console.log(rawList) + let list = [] + for (const board of rawList) { + if (board.isvol != 1) continue + list.push({ + id: 'kg__' + board.rankid, + name: board.rankname, + bangid: String(board.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) + // if (response.statusCode !== 200 || response.body.errcode !== 0) return this.getBoards(retryNum) + // const list = this.filterBoardsData(response.body.data.info) + // console.log(list) + // // console.log(JSON.stringify(list)) + // this.list = list + // return { + // list, + // source: 'kg', + // } + this.list = boardList + return { + list: boardList, + source: 'kg', + } + }, + async getList(bangid, page, retryNum = 0) { + if (++retryNum > 3) throw new Error('try max num') + const { body } = await this.getData(this.getUrl(page, bangid, this.listDetailLimit)) + + if (body.errcode != 0) return this.getList(bangid, page, retryNum) + + // console.log(body) + let total = body.data.total + let limit = 100 + let listData = this.filterData(body.data.info) + // console.log(listData) + return { + total, + list: listData, + limit, + page, + source: 'kg', + } + }, + getDetailPageUrl(id) { + if (typeof id == 'string') id = id.replace('kg__', '') + return `https://www.kugou.com/yy/rank/home/1-${id}.html` + }, +} diff --git a/musicSdk/kg/lyric.js b/musicSdk/kg/lyric.js new file mode 100644 index 0000000..f7a3421 --- /dev/null +++ b/musicSdk/kg/lyric.js @@ -0,0 +1,108 @@ +import { httpFetch } from '../request.js' +import { decodeKrc } from '../../src/common/lyricUtils/kg.js' + +export default { + getIntv(interval) { + if (!interval) return 0 + let intvArr = interval.split(':') + let intv = 0 + let unit = 1 + while (intvArr.length) { + intv += (intvArr.pop()) * unit + unit *= 60 + } + return parseInt(intv) + }, + // getLyric(songInfo, tryNum = 0) { + // let requestObj = httpFetch(`http://m.kugou.com/app/i/krc.php?cmd=100&keyword=${encodeURIComponent(songInfo.name)}&hash=${songInfo.hash}&timelength=${songInfo._interval || this.getIntv(songInfo.interval)}&d=0.38664927426725626`, { + // headers: { + // 'KG-RC': 1, + // 'KG-THash': 'expand_search_manager.cpp:852736169:451', + // 'User-Agent': 'KuGou2012-9020-ExpandSearchManager', + // }, + // }) + // requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { + // if (statusCode !== 200) { + // if (tryNum > 5) return Promise.reject(new Error('歌词获取失败')) + // let tryRequestObj = this.getLyric(songInfo, ++tryNum) + // requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) + // return tryRequestObj.promise + // } + // return { + // lyric: body, + // tlyric: '', + // } + // }) + // return requestObj + // }, + searchLyric(name, hash, time, tryNum = 0) { + let requestObj = httpFetch(`http://lyrics.kugou.com/search?ver=1&man=yes&client=pc&keyword=${encodeURIComponent(name)}&hash=${hash}&timelength=${time}&lrctxt=1`, { + headers: { + 'KG-RC': 1, + 'KG-THash': 'expand_search_manager.cpp:852736169:451', + 'User-Agent': 'KuGou2012-9020-ExpandSearchManager', + }, + }) + requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { + if (statusCode !== 200) { + if (tryNum > 5) return Promise.reject(new Error('歌词获取失败')) + let tryRequestObj = this.searchLyric(name, hash, time, ++tryNum) + requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) + return tryRequestObj.promise + } + if (body.candidates.length) { + let info = body.candidates[0] + return { id: info.id, accessKey: info.accesskey, fmt: (info.krctype == 1 && info.contenttype != 1) ? 'krc' : 'lrc' } + } + return null + }) + return requestObj + }, + getLyricDownload(id, accessKey, fmt, tryNum = 0) { + let requestObj = httpFetch(`http://lyrics.kugou.com/download?ver=1&client=pc&id=${id}&accesskey=${accessKey}&fmt=${fmt}&charset=utf8`, { + headers: { + 'KG-RC': 1, + 'KG-THash': 'expand_search_manager.cpp:852736169:451', + 'User-Agent': 'KuGou2012-9020-ExpandSearchManager', + }, + }) + requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { + if (statusCode !== 200) { + if (tryNum > 5) return Promise.reject(new Error('歌词获取失败')) + let tryRequestObj = this.getLyric(id, accessKey, fmt, ++tryNum) + requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) + return tryRequestObj.promise + } + + switch (body.fmt) { + case 'krc': + return decodeKrc(body.content) + case 'lrc': + return { + lyric: Buffer.from(body.content, 'base64').toString('utf-8'), + tlyric: '', + rlyric: '', + lxlyric: '', + } + default: + return Promise.reject(new Error(`未知歌词格式: ${body.fmt}`)) + } + }) + + return requestObj + }, + getLyric(songInfo, tryNum = 0) { + let requestObj = this.searchLyric(songInfo.name, songInfo.hash, songInfo._interval || this.getIntv(songInfo.interval)) + + requestObj.promise = requestObj.promise.then(result => { + if (!result) return Promise.reject(new Error('Get lyric failed')) + + let requestObj2 = this.getLyricDownload(result.id, result.accessKey, result.fmt) + + requestObj.cancelHttp = requestObj2.cancelHttp.bind(requestObj2) + + return requestObj2.promise + }) + return requestObj + }, +} diff --git a/musicSdk/kg/musicInfo.js b/musicSdk/kg/musicInfo.js new file mode 100644 index 0000000..0d1829f --- /dev/null +++ b/musicSdk/kg/musicInfo.js @@ -0,0 +1,116 @@ +import { decodeName, formatPlayTime, sizeFormate } from '../index.js' +import { createHttpFetch } from './util.js' + +const createGetMusicInfosTask = (hashs) => { + let data = { + area_code: '1', + show_privilege: 1, + show_album_info: '1', + is_publish: '', + appid: 1005, + clientver: 11451, + mid: '1', + dfid: '-', + clienttime: Date.now(), + key: 'OIlwieks28dk2k092lksi2UIkp', + fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname,classification', + } + let list = hashs + let tasks = [] + while (list.length) { + tasks.push(Object.assign({ data: list.slice(0, 100) }, data)) + if (list.length < 100) break + list = list.slice(100) + } + let url = 'http://gateway.kugou.com/v3/album_audio/audio' + return tasks.map(task => createHttpFetch(url, { + method: 'POST', + body: task, + headers: { + 'KG-THash': '13a3164', + 'KG-RC': '1', + 'KG-Fake': '0', + 'KG-RF': '00869891', + 'User-Agent': 'Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi', + 'x-router': 'kmr.service.kugou.com', + }, + }).then(data => data.map(s => s[0]))) +} + +export const filterMusicInfoList = (rawList) => { + // console.log(rawList) + let ids = new Set() + let list = [] + rawList.forEach(item => { + if (!item) return + if (ids.has(item.audio_info.audio_id)) return + ids.add(item.audio_info.audio_id) + const types = [] + const _types = {} + if (item.audio_info.filesize !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize)) + types.push({ type: '128k', size, hash: item.audio_info.hash }) + _types['128k'] = { + size, + hash: item.audio_info.hash, + } + } + if (item.audio_info.filesize_320 !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_320)) + types.push({ type: '320k', size, hash: item.audio_info.hash_320 }) + _types['320k'] = { + size, + hash: item.audio_info.hash_320, + } + } + if (item.audio_info.filesize_flac !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_flac)) + types.push({ type: 'flac', size, hash: item.audio_info.hash_flac }) + _types.flac = { + size, + hash: item.audio_info.hash_flac, + } + } + if (item.audio_info.filesize_high !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_high)) + types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high }) + _types.flac24bit = { + size, + hash: item.audio_info.hash_high, + } + } + list.push({ + singer: decodeName(item.author_name), + name: decodeName(item.songname), + albumName: decodeName(item.album_info.album_name), + albumId: item.album_info.album_id, + songmid: item.audio_info.audio_id, + source: 'kg', + interval: formatPlayTime(parseInt(item.audio_info.timelength) / 1000), + img: null, + lrc: null, + hash: item.audio_info.hash, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + }) + return list +} + +export const getMusicInfos = async(hashs) => { + return filterMusicInfoList(await Promise.all(createGetMusicInfosTask(hashs)).then(data => data.flat())) +} + +export const getMusicInfoRaw = async(hash) => { + return Promise.all(createGetMusicInfosTask([{ hash }])).then(data => data.flat()[0]) +} + +export const getMusicInfo = async(hash) => { + return getMusicInfos([{ hash }]).then(data => data[0]) +} + +export const getMusicInfosByList = (list) => { + return getMusicInfos(list.map(item => ({ hash: item.hash }))) +} diff --git a/musicSdk/kg/musicSearch.js b/musicSdk/kg/musicSearch.js new file mode 100644 index 0000000..0e01960 --- /dev/null +++ b/musicSdk/kg/musicSearch.js @@ -0,0 +1,108 @@ +import { httpFetch } from '../request.js' +import { decodeName, formatPlayTime, sizeFormate } from '../index.js' +import { formatSingerName } from '../utils.js' + + +export default { + limit: 30, + total: 0, + page: 0, + allPage: 1, + musicSearch(str, page, limit) { + const searchRequest = httpFetch(`https://songsearch.kugou.com/song_search_v2?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${limit}&userid=0&clientver=&platform=WebFilter&filter=2&iscorrection=1&privilege_filter=0&area_code=1`) + return searchRequest.promise.then(({ body }) => body) + }, + filterData(rawData) { + const types = [] + const _types = {} + if (rawData.FileSize !== 0) { + let size = sizeFormate(rawData.FileSize) + types.push({ type: '128k', size, hash: rawData.FileHash }) + _types['128k'] = { + size, + hash: rawData.FileHash, + } + } + if (rawData.HQFileSize !== 0) { + let size = sizeFormate(rawData.HQFileSize) + types.push({ type: '320k', size, hash: rawData.HQFileHash }) + _types['320k'] = { + size, + hash: rawData.HQFileHash, + } + } + if (rawData.SQFileSize !== 0) { + let size = sizeFormate(rawData.SQFileSize) + types.push({ type: 'flac', size, hash: rawData.SQFileHash }) + _types.flac = { + size, + hash: rawData.SQFileHash, + } + } + if (rawData.ResFileSize !== 0) { + let size = sizeFormate(rawData.ResFileSize) + types.push({ type: 'flac24bit', size, hash: rawData.ResFileHash }) + _types.flac24bit = { + size, + hash: rawData.ResFileHash, + } + } + return { + singer: decodeName(formatSingerName(rawData.Singers, 'name')), + name: decodeName(rawData.SongName), + albumName: decodeName(rawData.AlbumName), + albumId: rawData.AlbumID, + songmid: rawData.Audioid, + source: 'kg', + interval: formatPlayTime(rawData.Duration), + _interval: rawData.Duration, + img: null, + lrc: null, + otherSource: null, + hash: rawData.FileHash, + types, + _types, + typeUrl: {}, + } + }, + handleResult(rawData) { + let ids = new Set() + const list = [] + rawData.forEach(item => { + const key = item.Audioid + item.FileHash + if (ids.has(key)) return + ids.add(key) + list.push(this.filterData(item)) + for (const childItem of item.Grp) { + const key = item.Audioid + item.FileHash + if (ids.has(key)) continue + ids.add(key) + list.push(this.filterData(childItem)) + } + }) + 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 => { + if (!result || result.error_code !== 0) return this.search(str, page, limit, retryNum) + let list = this.handleResult(result.data.lists) + + if (list == null) return this.search(str, page, limit, retryNum) + + this.total = result.data.total + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + limit, + total: this.total, + source: 'kg', + }) + }) + }, +} diff --git a/musicSdk/kg/pic.js b/musicSdk/kg/pic.js new file mode 100644 index 0000000..8102559 --- /dev/null +++ b/musicSdk/kg/pic.js @@ -0,0 +1,48 @@ +import { httpFetch } from '../request.js' + +export default { + getPic(songInfo) { + const requestObj = httpFetch( + 'http://media.store.kugou.com/v1/get_res_privilege', + { + method: 'POST', + headers: { + 'KG-RC': 1, + 'KG-THash': 'expand_search_manager.cpp:852736169:451', + 'User-Agent': 'KuGou2012-9020-ExpandSearchManager', + }, + body: { + appid: 1001, + area_code: '1', + behavior: 'play', + clientver: '9020', + need_hash_offset: 1, + relate: 1, + resource: [ + { + album_audio_id: + songInfo.songmid.length == 32 // 修复歌曲ID存储变更导致图片获取失败的问题 + ? songInfo.audioId.split('_')[0] + : songInfo.songmid, + album_id: songInfo.albumId, + hash: songInfo.hash, + id: 0, + name: `${songInfo.singer} - ${songInfo.name}.mp3`, + type: 'audio', + }, + ], + token: '', + userid: 2626431536, + vip: 1, + }, + }, + ) + return requestObj.promise.then(({ body }) => { + if (body.error_code !== 0) return Promise.reject(new Error('图片获取失败')) + let info = body.data[0].info + const img = info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image + if (!img) return Promise.reject(new Error('Pic get failed')) + return img + }) + }, +} diff --git a/musicSdk/kg/singer.js b/musicSdk/kg/singer.js new file mode 100644 index 0000000..404ac19 --- /dev/null +++ b/musicSdk/kg/singer.js @@ -0,0 +1,85 @@ +import { getMusicInfosByList } from './musicInfo.js' +import { createHttpFetch } from './util.js' + +export default { + /** + * 获取歌手信息 + * @param {*} id + */ + getInfo(id) { + if (id == 0) throw new Error('歌手不存在') // kg源某些歌曲在歌手没被kg收录时返回的歌手id为0 + return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/info?singerid=${id}`).then(body => { + if (!body) throw new Error('get singer info faild.') + + return { + source: 'kg', + id: body.singerid, + info: { + name: body.singername, + desc: body.intro, + avatar: body.imgurl.replace('{size}', 480), + gender: body.grade === 1 ? 'man' : 'woman', + }, + count: { + music: body.songcount, + album: body.albumcount, + }, + } + }) + }, + /** + * 获取歌手专辑列表 + * @param {*} id + * @param {*} page + * @param {*} limit + */ + getAlbumList(id, page = 1, limit = 10) { + if (id == 0) throw new Error('歌手不存在') + return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/album?singerid=${id}&page=${page}&pagesize=${limit}`).then(body => { + if (!body.info) throw new Error('get singer album list faild.') + + const list = this.filterAlbumList(body.info) + return { + source: 'kg', + list, + limit, + page, + total: body.total, + } + }) + }, + /** + * 获取歌手歌曲列表 + * @param {*} id + * @param {*} page + * @param {*} limit + */ + async getSongList(id, page = 1, limit = 100) { + if (id == 0) throw new Error('歌手不存在') + const body = await createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/song?singerid=${id}&page=${page}&pagesize=${limit}`) + if (!body.info) throw new Error('get singer song list faild.') + + const list = await getMusicInfosByList(body.info) + return { + source: 'kg', + list, + limit, + page, + total: body.total, + } + }, + filterAlbumList(raw) { + return raw.map(item => { + return { + id: item.albumid, + count: item.songcount, + info: { + name: item.albumname, + author: item.singername, + img: item.replaceAll('{size}', '480'), + desc: item.intro, + }, + } + }) + }, +} diff --git a/musicSdk/kg/songList.js b/musicSdk/kg/songList.js new file mode 100644 index 0000000..0dc91e5 --- /dev/null +++ b/musicSdk/kg/songList.js @@ -0,0 +1,986 @@ +import { httpFetch } from '../request.js' +import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../index.js' +import { toMD5 } from '../utils.js' +import { signatureParams } from './util.js' + +// 原版使用 @renderer/utils/musicSdk/kg/vendors/infSign.min(浏览器签名库), +// 其 H5 签名算法为:{srcappid, clientver, clienttime, mid, uuid, dfid} + 业务参数, +// 按键排序拼接后以 key 包裹再取 MD5(与 signatureParams 同构)。 +const handleSignature = (id, page, limit) => { + const params = { + srcappid: '2919', + clientver: '20000', + clienttime: Date.now(), + mid: '0', + uuid: '0', + dfid: '-', + appid: 1058, + type: 0, + module: 'playlist', + page, + pagesize: limit, + specialid: id, + } + const key = 'NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt' + const sorted = Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('') + return Promise.resolve(toMD5(`${key}${sorted}${key}`)) +} + +export default { + _requestObj_tags: null, + _requestObj_listInfo: null, + _requestObj_list: null, + _requestObj_listRecommend: null, + listDetailLimit: 10000, + currentTagInfo: { + id: undefined, + info: undefined, + }, + sortList: [ + { + name: '推荐', + id: '5', + }, + { + name: '最热', + id: '6', + }, + { + name: '最新', + id: '7', + }, + { + name: '热藏', + id: '3', + }, + { + name: '飙升', + id: '8', + }, + ], + cache: new Map(), + regExps: { + listData: /global\.data = (\[.+\]);/, + listInfo: /global = {[\s\S]+?name: "(.+)"[\s\S]+?pic: "(.+)"[\s\S]+?};/, + // https://www.kugou.com/yy/special/single/1067062.html + listDetailLink: /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/, + }, + // async getGlobalSpecialId(specialId) { + // return httpFetch(`http://mobilecdnbj.kugou.com/api/v5/special/info?specialid=${specialId}`, { + // headers: { + // 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HLK-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Mobile Safari/537.36 EdgA/104.0.1293.70', + // }, + // }).promise.then(({ body }) => { + // // console.log(body) + // if (!body.data.global_specialid) Promise.reject(new Error('Failed to get global collection id.')) + // return body.data.global_specialid + // }) + // }, + // async getListInfoBySpecialId(special_id, retry = 0) { + // if (++retry > 2) throw new Error('failed') + // return httpFetch(`https://m.kugou.com/plist/list/${special_id}/?json=true`, { + // headers: { + // 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HLK-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Mobile Safari/537.36 EdgA/104.0.1293.70', + // }, + // follow_max: 2, + // }).promise.then(({ body }) => { + // // console.log(body) + // if (!body.info.list) return this.getListInfoBySpecialId(special_id, retry) + // let listinfo = body.info.list + // return { + // listInfo: { + // name: listinfo.specialname, + // image: listinfo.imgurl.replace('{size}', '150'), + // intro: listinfo.intro, + // author: listinfo.nickname, + // playcount: listinfo.playcount, + // total: listinfo.songcount, + // }, + // globalSpecialId: listinfo.global_specialid, + // } + // }) + // }, + // async getSongListDetailByGlobalSpecialId(id, page, limit = 100, retry = 0) { + // if (++retry > 2) throw new Error('failed') + // console.log(id) + // const params = `specialid=0&need_sort=1&module=CloudMusic&clientver=11409&pagesize=${limit}&global_collection_id=${id}&userid=0&page=${page}&type=1&area_code=1&appid=1005` + // return httpFetch(`http://pubsongscdn.tx.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params)}`).promise.then(({ body }) => { + // // console.log(body) + // if (body.data?.info == null) return this.getSongListDetailByGlobalSpecialId(id, page, limit, retry) + // return body.data.info + // }) + // }, + parseHtmlDesc(html) { + const prefix = '
' + let index = html.indexOf(prefix) + if (index < 0) return null + const afterStr = html.substring(index + prefix.length) + index = afterStr.indexOf('
') + if (index < 0) return null + return decodeName(afterStr.substring(0, index)) + }, + async getListDetailBySpecialId(id, page, tryNum = 0) { + if (tryNum > 2) throw new Error('try max num') + + const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise + let listData = body.match(this.regExps.listData) + let listInfo = body.match(this.regExps.listInfo) + if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum) + let list = await this.getMusicInfos(JSON.parse(listData[1])) + // listData = this.filterData(JSON.parse(listData[1])) + let name + let pic + if (listInfo) { + name = listInfo[1] + pic = listInfo[2] + } + let desc = this.parseHtmlDesc(body) + + + return { + list, + page: 1, + limit: 10000, + total: list.length, + source: 'kg', + info: { + name, + img: pic, + desc, + // author: body.result.info.userinfo.username, + // play_count: formatPlayCount(body.result.listen_num), + }, + } + + // const globalSpecialId = await this.getGlobalSpecialId(id) + // const limit = 100 + // const listData = await this.getSongListDetailByGlobalSpecialId(globalSpecialId, page, limit) + // if (!Array.isArray(listData)) + // return this.getUserListDetail2(globalSpecialId) + // return { + // list: this.filterDatav9(listData), + // page, + // limit, + // total: listInfo.total, + // source: 'kg', + // info: { + // name: listInfo.name, + // img: listInfo.image, + // desc: listInfo.intro, + // author: listInfo.author, + // play_count: formatPlayCount(listInfo.playcount), + // }, + // } + }, + getInfoUrl(tagId) { + return tagId + ? `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&cdn=cdn&t=5&c=${tagId}` + : 'http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&' + }, + getSongListUrl(sortId, tagId, page) { + if (tagId == null) tagId = '' + return `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_ajax=1&cdn=cdn&t=${sortId}&c=${tagId}&p=${page}` + }, + getSongListDetailUrl(id) { + return `http://www2.kugou.kugou.com/yueku/v9/special/single/${id}-5-9999.html` + }, + + filterInfoHotTag(rawData) { + const result = [] + if (rawData.status !== 1) return result + for (const key of Object.keys(rawData.data)) { + let tag = rawData.data[key] + result.push({ + id: tag.special_id, + name: tag.special_name, + source: 'kg', + }) + } + return result + }, + filterTagInfo(rawData) { + const result = [] + for (const name of Object.keys(rawData)) { + result.push({ + name, + list: rawData[name].data.map(tag => ({ + parent_id: tag.parent_id, + parent_name: tag.pname, + id: tag.id, + name: tag.name, + source: 'kg', + })), + }) + } + return result + }, + + getSongList(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), + ) + return this._requestObj_list.promise.then(({ body }) => { + if (!body || body.status !== 1) return this.getSongList(sortId, tagId, page, ++tryNum) + return this.filterList(body.special_db) + }) + }, + getSongListRecommend(tryNum = 0) { + if (this._requestObj_listRecommend) this._requestObj_listRecommend.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_listRecommend = httpFetch( + 'http://everydayrec.service.kugou.com/guess_special_recommend', + { + method: 'post', + headers: { + 'User-Agent': 'KuGou2012-8275-web_browser_event_handler', + }, + body: { + appid: 1001, + clienttime: 1566798337219, + clientver: 8275, + key: 'f1f93580115bb106680d2375f8032d96', + mid: '21511157a05844bd085308bc76ef3343', + platform: 'pc', + userid: '262643156', + return_min: 6, + return_max: 15, + }, + }, + ) + return this._requestObj_listRecommend.promise.then(({ body }) => { + if (body.status !== 1) return this.getSongListRecommend(++tryNum) + return this.filterList(body.data.special_list) + }) + }, + filterList(rawData) { + return rawData.map(item => ({ + play_count: item.total_play_count || formatPlayCount(item.play_count), + id: 'id_' + item.specialid, + author: item.nickname, + name: item.specialname, + time: dateFormat(item.publish_time || item.publishtime, 'Y-M-D'), + img: item.img || item.imgurl, + total: item.songcount, + grade: item.grade, + desc: item.intro, + source: 'kg', + })) + }, + + async createHttp(url, options, retryNum = 0) { + if (retryNum > 2) throw new Error('try max num') + let result + try { + result = await httpFetch(url, options).promise + } catch (err) { + console.log(err) + return this.createHttp(url, options, ++retryNum) + } + // console.log(result.statusCode, result.body) + if (result.statusCode !== 200 || + ( + (result.body.error_code !== undefined + ? result.body.error_code + : result.body.errcode !== undefined + ? result.body.errcode + : result.body.err_code + ) !== 0) + ) return this.createHttp(url, options, ++retryNum) + if (result.body.data) return result.body.data + if (Array.isArray(result.body.info)) return result.body + return result.body.info + }, + + createTask(hashs) { + let data = { + area_code: '1', + show_privilege: 1, + show_album_info: '1', + is_publish: '', + appid: 1005, + clientver: 11451, + mid: '1', + dfid: '-', + clienttime: Date.now(), + key: 'OIlwieks28dk2k092lksi2UIkp', + fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname', + } + let list = hashs + let tasks = [] + while (list.length) { + tasks.push(Object.assign({ data: list.slice(0, 100) }, data)) + if (list.length < 100) break + list = list.slice(100) + } + let url = 'http://gateway.kugou.com/v2/album_audio/audio' + return tasks.map(task => this.createHttp(url, { + method: 'POST', + body: task, + headers: { + 'KG-THash': '13a3164', + 'KG-RC': '1', + 'KG-Fake': '0', + 'KG-RF': '00869891', + 'User-Agent': 'Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi', + 'x-router': 'kmr.service.kugou.com', + }, + }).then(data => data.map(s => s[0]))) + }, + async getMusicInfos(list) { + return this.filterData2( + await Promise.all( + this.createTask( + this.deDuplication(list) + .map(item => ({ hash: item.hash })), + )) + .then(([...datas]) => datas.flat())) + }, + + async getUserListDetailByCode(id) { + const songInfo = await this.createHttp('http://t.kugou.com/command/', { + method: 'POST', + headers: { + 'KG-RC': 1, + 'KG-THash': 'network_super_call.cpp:3676261689:379', + 'User-Agent': '', + }, + body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: id }, + }) + // console.log(songInfo) + // type 1单曲,2歌单,3电台,4酷狗码,5别人的播放队列 + let songList + let info = songInfo.info + switch (info.type) { + case 2: + if (!info.global_collection_id) return this.getListDetailBySpecialId(info.id) + break + + default: + break + } + if (info.global_collection_id) return this.getUserListDetail2(info.global_collection_id) + if (info.userid != null) { + songList = await this.createHttp('http://www2.kugou.kugou.com/apps/kucodeAndShare/app/', { + method: 'POST', + headers: { + 'KG-RC': 1, + 'KG-THash': 'network_super_call.cpp:3676261689:379', + 'User-Agent': '', + }, + body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: { id: info.id, type: 3, userid: info.userid, collect_type: 0, page: 1, pagesize: info.count } }, + }) + // console.log(songList) + } + let list = await this.getMusicInfos(songList || songInfo.list) + return { + list, + page: 1, + limit: info.count, + total: list.length, + source: 'kg', + info: { + name: info.name, + img: (info.img_size && info.img_size.replace('{size}', 240)) || info.img, + // desc: body.result.info.list_desc, + author: info.username, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetail3(chain, page) { + const songInfo = await this.createHttp(`http://m.kugou.com/schain/transfer?pagesize=${this.listDetailLimit}&chain=${chain}&su=1&page=${page}&n=0.7928855356604456`, { + 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', + }, + }) + if (!songInfo.list) { + if (songInfo.global_collection_id) return this.getUserListDetail2(songInfo.global_collection_id) + else return this.getUserListDetail4(songInfo, chain, page).catch(() => this.getUserListDetail5(chain)) + } + let list = await this.getMusicInfos(songInfo.list) + // console.log(info, songInfo) + return { + list, + page: 1, + limit: this.listDetailLimit, + total: list.length, + source: 'kg', + info: { + name: songInfo.info.name, + img: songInfo.info.img, + // desc: body.result.info.list_desc, + author: songInfo.info.username, + // play_count: formatPlayCount(info.count), + }, + } + }, + + deDuplication(datas) { + let ids = new Set() + return datas.filter(({ hash }) => { + if (ids.has(hash)) return false + ids.add(hash) + return true + }) + }, + + async decodeGcid(gcid) { + const params = 'dfid=-&appid=1005&mid=0&clientver=20109&clienttime=640612895&uuid=-' + const body = { + ret_info: 1, + data: [ + { + id: gcid, + id_type: 2, + }, + ], + } + const result = await this.createHttp(`https://t.kugou.com/v1/songlist/batch_decode?${params}&signature=${signatureParams(params, 'android', JSON.stringify(body))}`, { + method: 'POST', + headers: { + 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HUAWEI HMA-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36', + Referer: 'https://m.kugou.com/', + }, + body, + }) + return result.list[0].global_collection_id + }, + + async getUserListDetailByLink({ info }, link) { + let listInfo = info['0'] + let total = listInfo.count + let tasks = [] + let page = 0 + while (total) { + const limit = total > 90 ? 90 : total + total -= limit + page += 1 + tasks.push(this.createHttp(link.replace(/pagesize=\d+/, 'pagesize=' + limit).replace(/page=\d+/, 'page=' + page), { + 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, + }, + }).then(data => data.list.info)) + } + let result = await Promise.all(tasks).then(([...datas]) => datas.flat()) + result = await this.getMusicInfos(result) + // console.log(result) + return { + list: result, + page, + limit: this.listDetailLimit, + total: result.length, + source: 'kg', + info: { + name: listInfo.name, + img: listInfo.pic && listInfo.pic.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.list_create_username, + // play_count: formatPlayCount(listInfo.count), + }, + } + }, + createGetListDetail2Task(id, total) { + let tasks = [] + let page = 0 + while (total) { + const limit = total > 300 ? 300 : total + total -= limit + page += 1 + const params = 'appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&page=' + page + '&pagesize=' + limit + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-' + tasks.push(this.createHttp(`https://mobiles.kugou.com/api/v5/special/song_v2?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163263991', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163263991', + }, + }).then(data => data.info)) + } + return Promise.all(tasks).then(([...datas]) => datas.flat()) + }, + async getUserListDetail2(global_collection_id) { + let id = global_collection_id + if (id.length > 1000) throw new Error('get list error') + const params = 'appid=1058&specialid=0&global_specialid=' + id + '&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-' + let info = await this.createHttp(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163242519', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163242519', + }, + }) + const songInfo = await this.createGetListDetail2Task(id, info.songcount) + let list = await this.getMusicInfos(songInfo) + // console.log(info, songInfo, list) + return { + list, + page: 1, + limit: this.listDetailLimit, + total: list.length, + source: 'kg', + info: { + name: info.specialname, + img: info.imgurl && info.imgurl.replace('{size}', 240), + desc: info.intro, + author: info.nickname, + play_count: formatPlayCount(info.playcount), + }, + } + }, + + async getListInfoByChain(chain) { + if (this.cache.has(chain)) return this.cache.get(chain) + const { body } = await httpFetch(`https://m.kugou.com/share/?chain=${chain}&id=${chain}`, { + 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', + }, + }).promise + let result = body.match(/var\sphpParam\s=\s({.+?});/) + if (result) result = JSON.parse(result[1]) + this.cache.set(chain, result) + return result + }, + + async getUserListDetailByPcChain(chain) { + let key = `${chain}_pc_list` + if (this.cache.has(key)) return this.cache.get(key) + const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + }, + }).promise + let result = body.match(/var\sdataFromSmarty\s=\s(\[.+?\])/) + if (result) result = JSON.parse(result[1]) + this.cache.set(chain, result) + result = await this.getMusicInfos(result) + // console.log(info, songInfo) + return result + }, + + async getUserListDetail4(songInfo, chain, page) { + const limit = 100 + const [listInfo, list] = await Promise.all([ + this.getListInfoByChain(chain), + this.getUserListDetailById(songInfo.id, page, limit), + ]) + return { + list: list || [], + page, + limit, + total: list.length ?? 0, + source: 'kg', + info: { + name: listInfo.specialname, + img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.nickname, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetail5(chain) { + const [listInfo, list] = await Promise.all([ + this.getListInfoByChain(chain), + this.getUserListDetailByPcChain(chain), + ]) + return { + list: list || [], + page: 1, + limit: this.listDetailLimit, + total: list.length ?? 0, + source: 'kg', + info: { + name: listInfo.specialname, + img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.nickname, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetailById(id, page, limit) { + const signature = await handleSignature(id, page, limit) + let info = await this.createHttp(`https://pubsongscdn.kugou.com/v2/get_other_list_file?srcappid=2919&clientver=20000&appid=1058&type=0&module=playlist&page=${page}&pagesize=${limit}&specialid=${id}&signature=${signature}`, { + headers: { + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + }, + }) + + // console.log(info) + let result = await this.getMusicInfos(info.info) + // console.log(info, songInfo) + return result + }, + + async getUserListDetail(link, page, retryNum = 0) { + if (retryNum > 3) return Promise.reject(new Error('link try max num')) + if (link.includes('#')) link = link.replace(/#.*$/, '') + if (link.includes('global_collection_id')) return this.getUserListDetail2(link.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')) + if (link.includes('gcid_')) { + let gcid = link.match(/gcid_\w+/)?.[0] + if (gcid) { + const global_collection_id = await this.decodeGcid(gcid) + if (global_collection_id) return this.getUserListDetail2(global_collection_id) + } + } + if (link.includes('chain=')) return this.getUserListDetail3(link.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (link.includes('.html')) { + if (link.includes('zlist.html')) { + link = link.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list') + if (link.includes('pagesize')) { + link = link.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page) + } else { + link += `&pagesize=${this.listDetailLimit}&page=${page}` + } + } else if (!link.includes('song.html')) return this.getUserListDetail3(link.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page) + } + + 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, body } = await requestObj_listDetailLink.promise + // console.log(body, location) + if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum) + if (location) { + // console.log(location) + if (location.includes('global_collection_id')) return this.getUserListDetail2(location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')) + if (location.includes('gcid_')) { + let gcid = link.match(/gcid_\w+/)?.[0] + if (gcid) { + const global_collection_id = await this.decodeGcid(gcid) + if (global_collection_id) return this.getUserListDetail2(global_collection_id) + } + } + if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (location.includes('.html')) { + if (location.includes('zlist.html')) { + let link = location.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list') + if (link.includes('pagesize')) { + link = link.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page) + } else { + link += `&pagesize=${this.listDetailLimit}&page=${page}` + } + return this.getUserListDetail(link, page, ++retryNum) + } else return this.getUserListDetail3(location.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page) + } + // console.log('location', location) + return this.getUserListDetail(location, page, ++retryNum) + } + if (typeof body == 'string') { + let global_collection_id = body.match(/"global_collection_id":"(\w+)"/)?.[1] + if (!global_collection_id) { + let gcid = body.match(/"encode_gic":"(\w+)"/)?.[1] + if (!gcid) gcid = body.match(/"encode_src_gid":"(\w+)"/)?.[1] + if (gcid) global_collection_id = await this.decodeGcid(gcid) + } + if (!global_collection_id) throw new Error('get list error') + return this.getUserListDetail2(global_collection_id) + } + if (body.errcode !== 0) return this.getUserListDetail(link, page, ++retryNum) + return this.getUserListDetailByLink(body, link) + }, + + async getListDetail(id, page) { // 获取歌曲列表内的音乐 + id = id.toString() + if (id.includes('special/single/')) { + id = id.replace(this.regExps.listDetailLink, '$1') + } else if (/https?:/.test(id)) { + // fix https://www.kugou.com/songlist/xxx/?uid=xxx&chl=qq_client&cover=http%3A%2F%2Fimge.kugou.com%xxx.jpg&iszlist=1 + return this.getUserListDetail(id.replace(/^.*?http/, 'http'), page) + } else if (/^\d+$/.test(id)) { + return this.getUserListDetailByCode(id) + } else if (id.startsWith('id_')) { + id = id.replace('id_', '') + } + // if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + + return this.getListDetailBySpecialId(id, page) + }, + filterData(rawList) { + // console.log(rawList) + return rawList.map(item => { + const types = [] + const _types = {} + if (item.filesize !== 0) { + let size = sizeFormate(item.filesize) + types.push({ type: '128k', size, hash: item.hash }) + _types['128k'] = { + size, + hash: item.hash, + } + } + if (item.filesize_320 !== 0) { + let size = sizeFormate(item.filesize_320) + types.push({ type: '320k', size, hash: item.hash_320 }) + _types['320k'] = { + size, + hash: item.hash_320, + } + } + if (item.filesize_ape !== 0) { + let size = sizeFormate(item.filesize_ape) + types.push({ type: 'ape', size, hash: item.hash_ape }) + _types.ape = { + size, + hash: item.hash_ape, + } + } + if (item.filesize_flac !== 0) { + let size = sizeFormate(item.filesize_flac) + types.push({ type: 'flac', size, hash: item.hash_flac }) + _types.flac = { + size, + hash: item.hash_flac, + } + } + return { + singer: decodeName(item.singername), + name: decodeName(item.songname), + albumName: decodeName(item.album_name), + albumId: item.album_id, + songmid: item.audio_id, + source: 'kg', + interval: formatPlayTime(item.duration / 1000), + img: null, + lrc: null, + hash: item.hash, + types, + _types, + typeUrl: {}, + } + }) + }, + // getSinger(singers) { + // let arr = [] + // singers?.forEach(singer => { + // arr.push(singer.name) + // }) + // return arr.join('、') + // }, + // v9 API + // filterDatav9(rawList) { + // console.log(rawList) + // return rawList.map(item => { + // const types = [] + // const _types = {} + // item.relate_goods.forEach(qualityObj => { + // if (qualityObj.level === 2) { + // let size = sizeFormate(qualityObj.size) + // types.push({ type: '128k', size, hash: qualityObj.hash }) + // _types['128k'] = { + // size, + // hash: qualityObj.hash, + // } + // } else if (qualityObj.level === 4) { + // let size = sizeFormate(qualityObj.size) + // types.push({ type: '320k', size, hash: qualityObj.hash }) + // _types['320k'] = { + // size, + // hash: qualityObj.hash, + // } + // } else if (qualityObj.level === 5) { + // let size = sizeFormate(qualityObj.size) + // types.push({ type: 'flac', size, hash: qualityObj.hash }) + // _types.flac = { + // size, + // hash: qualityObj.hash, + // } + // } else if (qualityObj.level === 6) { + // let size = sizeFormate(qualityObj.size) + // types.push({ type: 'flac24bit', size, hash: qualityObj.hash }) + // _types.flac24bit = { + // size, + // hash: qualityObj.hash, + // } + // } + // }) + // const nameInfo = item.name.split(' - ') + // return { + // singer: this.getSinger(item.singerinfo), + // name: decodeName((nameInfo[1] ?? nameInfo[0]).trim()), + // albumName: decodeName(item.albuminfo.name), + // albumId: item.albuminfo.id, + // songmid: item.audio_id, + // source: 'kg', + // interval: formatPlayTime(item.timelen / 1000), + // img: null, + // lrc: null, + // hash: item.hash, + // types, + // _types, + // typeUrl: {}, + // } + // }) + // }, + + // hash list filter + filterData2(rawList) { + // console.log(rawList) + let ids = new Set() + let list = [] + rawList.forEach(item => { + if (!item) return + if (ids.has(item.audio_info.audio_id)) return + ids.add(item.audio_info.audio_id) + const types = [] + const _types = {} + if (item.audio_info.filesize !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize)) + types.push({ type: '128k', size, hash: item.audio_info.hash }) + _types['128k'] = { + size, + hash: item.audio_info.hash, + } + } + if (item.audio_info.filesize_320 !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_320)) + types.push({ type: '320k', size, hash: item.audio_info.hash_320 }) + _types['320k'] = { + size, + hash: item.audio_info.hash_320, + } + } + if (item.audio_info.filesize_flac !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_flac)) + types.push({ type: 'flac', size, hash: item.audio_info.hash_flac }) + _types.flac = { + size, + hash: item.audio_info.hash_flac, + } + } + if (item.audio_info.filesize_high !== '0') { + let size = sizeFormate(parseInt(item.audio_info.filesize_high)) + types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high }) + _types.flac24bit = { + size, + hash: item.audio_info.hash_high, + } + } + list.push({ + singer: decodeName(item.author_name), + name: decodeName(item.songname), + albumName: decodeName(item.album_info.album_name), + albumId: item.album_info.album_id, + songmid: item.audio_info.audio_id, + source: 'kg', + interval: formatPlayTime(parseInt(item.audio_info.timelength) / 1000), + img: null, + lrc: null, + hash: item.audio_info.hash, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + }) + return list + }, + + // 获取列表信息 + getListInfo(tagId, tryNum = 0) { + if (this._requestObj_listInfo) this._requestObj_listInfo.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_listInfo = httpFetch(this.getInfoUrl(tagId)) + return this._requestObj_listInfo.promise.then(({ body }) => { + if (body.status !== 1) return this.getListInfo(tagId, ++tryNum) + return { + limit: body.data.params.pagesize, + page: body.data.params.p, + total: body.data.params.total, + source: 'kg', + } + }) + }, + + // 获取列表数据 + getList(sortId, tagId, page) { + let tasks = [this.getSongList(sortId, tagId, page)] + tasks.push( + this.currentTagInfo.id === tagId + ? Promise.resolve(this.currentTagInfo.info) + : this.getListInfo(tagId).then(info => { + this.currentTagInfo.id = tagId + this.currentTagInfo.info = Object.assign({}, info) + return info + }), + ) + if (!tagId && page === 1 && sortId === this.sortList[0].id) tasks.push(this.getSongListRecommend()) // 如果是所有类别,则顺便获取推荐列表 + return Promise.all(tasks).then(([list, info, recommendList]) => { + if (recommendList) list.unshift(...recommendList) + return { + list, + ...info, + } + }) + }, + + // 获取标签 + 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.getInfoUrl()) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.status !== 1) return this.getTags(++tryNum) + return { + hotTag: this.filterInfoHotTag(body.data.hotTag), + tags: this.filterTagInfo(body.data.tagids), + source: 'kg', + } + }) + }, + + getDetailPageUrl(id) { + if (typeof id == 'string') { + if (/^https?:\/\//.test(id)) return id + id = id.replace('id_', '') + } + return `https://www.kugou.com/yy/special/single/${id}.html` + }, + + search(text, page, limit = 20) { + // http://msearchretry.kugou.com/api/v3/search/special?version=9209&keyword=%E5%91%A8%E6%9D%B0%E4%BC%A6&pagesize=20&filter=0&page=1&sver=2&with_res_tag=0 + // return httpFetch(`http://ioscdn.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&correct=1&sver=5`) + return httpFetch(`http://msearchretry.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&filter=0&version=7910&sver=2`) + .promise.then(({ body }) => { + if (body.errcode != 0) throw new Error('filed') + // console.log(body.data.info) + return { + list: body.data.info.map(item => { + return { + play_count: formatPlayCount(item.playcount), + id: 'id_' + item.specialid, + author: item.nickname, + name: item.specialname, + time: dateFormat(item.publishtime, 'Y-M-D'), + img: item.imgurl, + grade: item.grade, + desc: item.intro, + total: item.songcount, + source: 'kg', + } + }), + limit, + total: body.data.total, + source: 'kg', + } + }) + }, +} + +// getList +// getTags +// getListDetail diff --git a/musicSdk/kg/temp/musicSearch-new.js b/musicSdk/kg/temp/musicSearch-new.js new file mode 100644 index 0000000..5bb9952 --- /dev/null +++ b/musicSdk/kg/temp/musicSearch-new.js @@ -0,0 +1,112 @@ +import { decodeName, formatPlayTime, sizeFormate } from '../index.js' +import { signatureParams, createHttpFetch } from './util.js' +import { formatSingerName } from '../../utils.js' + +export default { + limit: 30, + total: 0, + page: 0, + allPage: 1, + musicSearch(str, page, limit) { + const sign = signatureParams(`userid=0&area_code=1&appid=1005&dopicfull=1&page=${page}&token=0&privilegefilter=0&requestid=0&pagesize=${limit}&user_labels=&clienttime=0&sec_aggre=1&iscorrection=1&uuid=0&mid=0&keyword=${str}&dfid=-&clientver=11409&platform=AndroidFilter&tag=`, 3) + return createHttpFetch(`https://gateway.kugou.com/complexsearch/v3/search/song?userid=0&area_code=1&appid=1005&dopicfull=1&page=${page}&token=0&privilegefilter=0&requestid=0&pagesize=${limit}&user_labels=&clienttime=0&sec_aggre=1&iscorrection=1&uuid=0&mid=0&dfid=-&clientver=11409&platform=AndroidFilter&tag=&keyword=${encodeURIComponent(str)}&signature=${sign}`, { + 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: 'https://kugou.com', + }, + }).then(body => body) + }, + filterList(raw) { + let ids = new Set() + const list = [] + + raw.forEach(item => { + if (ids.has(item.Audioid)) return + ids.add(item.Audioid) + + const types = [] + const _types = {} + if (item.FileSize !== 0) { + let size = sizeFormate(item.FileSize) + types.push({ type: '128k', size, hash: item.FileHash }) + _types['128k'] = { + size, + hash: item.FileHash, + } + } + if (item.HQ != undefined) { + let size = sizeFormate(item.HQ.FileSize) + types.push({ type: '320k', size, hash: item.HQ.Hash }) + _types['320k'] = { + size, + hash: item.HQ.Hash, + } + } + if (item.SQ != undefined) { + let size = sizeFormate(item.SQ.FileSize) + types.push({ type: 'flac', size, hash: item.SQ.Hash }) + _types.flac = { + size, + hash: item.SQ.Hash, + } + } + if (item.Res != undefined) { + let size = sizeFormate(item.Res.FileSize) + types.push({ type: 'flac24bit', size, hash: item.Res.Hash }) + _types.flac24bit = { + size, + hash: item.Res.Hash, + } + } + list.push({ + singer: decodeName(formatSingerName(item.Singers)), + name: decodeName(item.SongName), + albumName: decodeName(item.AlbumName), + albumId: item.AlbumID, + songmid: item.Audioid, + source: 'kg', + interval: formatPlayTime(item.Duration), + _interval: item.Duration, + img: null, + lrc: null, + otherSource: null, + hash: item.FileHash, + types, + _types, + typeUrl: {}, + }) + }) + + return list + }, + handleResult(rawData) { + const rawList = [] + rawData.forEach(item => { + rawList.push(item) + item.Grp.forEach(e => rawList.push(e)) + }) + + return this.filterList(rawList) + }, + 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(data => { + let list = this.handleResult(data.lists) + if (!list) return this.search(str, page, limit, retryNum) + + this.total = data.total + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + limit, + total: this.total, + source: 'kg', + }) + }) + }, +} diff --git a/musicSdk/kg/temp/songList-new.js b/musicSdk/kg/temp/songList-new.js new file mode 100644 index 0000000..af919a7 --- /dev/null +++ b/musicSdk/kg/temp/songList-new.js @@ -0,0 +1,794 @@ +import { httpFetch } from '../../../request.js' +import { formatSingerName } from '../../utils.js' +import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../../index.js' +import { signatureParams, createHttpFetch } from './../util.js' +import { getMusicInfosByList } from '../musicInfo.js' +import album from '../album.js' + +export default { + _requestObj_tags: null, + _requestObj_listInfo: null, + _requestObj_list: null, + _requestObj_listRecommend: null, + listDetailLimit: 10000, + currentTagInfo: { + id: undefined, + info: undefined, + }, + sortList: [ + { + name: '推荐', + id: '5', + }, + { + name: '最热', + id: '6', + }, + { + name: '最新', + id: '7', + }, + { + name: '热藏', + id: '3', + }, + { + name: '飙升', + id: '8', + }, + ], + cache: new Map(), + collectionIdListInfoCache: new Map(), + regExps: { + listData: /global\.data = (\[.+\]);/, + listInfo: /global = {[\s\S]+?name: "(.+)"[\s\S]+?pic: "(.+)"[\s\S]+?};/, + // https://www.kugou.com/yy/special/single/1067062.html + listDetailLink: /^.+\/(\d+)\.html(?:\?.*|&.*$|#.*$|$)/, + }, + + /** + * 获取歌曲列表内的音乐 + * @param {*} id + * @param {*} page + */ + async getListDetail(id, page) { + id = id.toString() + + if (id.includes('special/single/')) id = id.replace(this.regExps.listDetailLink, '$1') + // fix https://www.kugou.com/songlist/xxx/?uid=xxx&chl=qq_client&cover=http%3A%2F%2Fimge.kugou.com%xxx.jpg&iszlist=1 + if (/https?:/.test(id)) { + if (id.includes('#')) id = id.replace(/#.*$/, '') + if (id.includes('global_collection_id')) return this.getUserListDetailByCollectionId(id.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (id.includes('chain=')) return this.getUserListDetail3(id.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (id.includes('.html')) { + if (id.includes('zlist.html')) { + id = id.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list') + if (id.includes('pagesize')) { + id = id.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page) + } else { + id += `&pagesize=${this.listDetailLimit}&page=${page}` + } + } else if (!id.includes('song.html')) return this.getUserListDetail3(id.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page) + } + return this.getUserListDetail(id.replace(/^.*?http/, 'http'), page) + } + if (/^\d+$/.test(id)) return this.getUserListDetailByCode(id, page) + if (id.startsWith('gid_')) return this.getUserListDetailByCollectionId(id.replace('gid_', ''), page) + if (id.startsWith('id_')) return this.getUserListDetailBySpecialId(id.replace('id_', ''), page) + + return new Error('Failed.') + }, + + /** + * 获取SpecialId歌单 + * @param {*} id + */ + async getUserListDetailBySpecialId(id, page, tryNum = 0) { + if (tryNum > 2) throw new Error('try max num') + + const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise + let listData = body.match(this.regExps.listData) + let listInfo = body.match(this.regExps.listInfo) + if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum) + let list = await getMusicInfosByList(JSON.parse(listData[1])) + let name + let pic + if (listInfo) { + name = listInfo[1] + pic = listInfo[2] + } + let desc = this.parseHtmlDesc(body) + + + return { + list, + page: 1, + limit: 10000, + total: list.length, + source: 'kg', + info: { + name, + img: pic, + desc, + // author: body.result.info.userinfo.username, + // play_count: formatPlayCount(body.result.listen_num), + }, + } + }, + parseHtmlDesc(html) { + const prefix = '
' + let index = html.indexOf(prefix) + if (index < 0) return null + const afterStr = html.substring(index + prefix.length) + index = afterStr.indexOf('
') + if (index < 0) return null + return decodeName(afterStr.substring(0, index)) + }, + + /** + * 使用SpecialId获取CollectionId + * @param {*} specialId + */ + async getCollectionIdBySpecialId(specialId) { + return httpFetch(`http://mobilecdnbj.kugou.com/api/v5/special/info?specialid=${specialId}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; HLK-AL00) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Mobile Safari/537.36 EdgA/104.0.1293.70', + }, + }).promise.then(({ body }) => { + // console.log('getCollectionIdBySpecialId', body) + if (!body.data.global_specialid) return Promise.reject(new Error('Failed to get global collection id.')) + return body.data.global_specialid + }) + }, + + /** + * 获取歌单URL + * @param {*} sortId + * @param {*} tagId + * @param {*} page + */ + getSongListUrl(sortId, tagId, page) { + if (tagId == null) tagId = '' + return `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_ajax=1&cdn=cdn&t=${sortId}&c=${tagId}&p=${page}` + }, + getInfoUrl(tagId) { + return tagId + ? `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&cdn=cdn&t=5&c=${tagId}` + : 'http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&' + }, + getSongListDetailUrl(id) { + return `http://www2.kugou.kugou.com/yueku/v9/special/single/${id}-5-9999.html` + }, + + filterInfoHotTag(rawData) { + const result = [] + if (rawData.status !== 1) return result + for (const key of Object.keys(rawData.data)) { + let tag = rawData.data[key] + result.push({ + id: tag.special_id, + name: tag.special_name, + source: 'kg', + }) + } + return result + }, + + filterTagInfo(rawData) { + const result = [] + for (const name of Object.keys(rawData)) { + result.push({ + name, + list: rawData[name].data.map(tag => ({ + parent_id: tag.parent_id, + parent_name: tag.pname, + id: tag.id, + name: tag.name, + source: 'kg', + })), + }) + } + return result + }, + filterSongList(rawData) { + return rawData.map(item => ({ + play_count: item.total_play_count || formatPlayCount(item.play_count), + id: 'id_' + item.specialid, + author: item.nickname, + name: item.specialname, + time: dateFormat(item.publish_time || item.publishtime, 'Y-M-D'), + img: item.img || item.imgurl, + total: item.songcount, + grade: item.grade, + desc: item.intro, + source: 'kg', + })) + }, + + getSongList(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), + ) + return this._requestObj_list.promise.then(({ body }) => { + if (!body || body.status !== 1) return this.getSongList(sortId, tagId, page, ++tryNum) + return this.filterSongList(body.special_db) + }) + }, + getSongListRecommend(tryNum = 0) { + if (this._requestObj_listRecommend) this._requestObj_listRecommend.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_listRecommend = httpFetch( + 'http://everydayrec.service.kugou.com/guess_special_recommend', + { + method: 'post', + headers: { + 'User-Agent': 'KuGou2012-8275-web_browser_event_handler', + }, + body: { + appid: 1001, + clienttime: 1566798337219, + clientver: 8275, + key: 'f1f93580115bb106680d2375f8032d96', + mid: '21511157a05844bd085308bc76ef3343', + platform: 'pc', + userid: '262643156', + return_min: 6, + return_max: 15, + }, + }, + ) + return this._requestObj_listRecommend.promise.then(({ body }) => { + if (body.status !== 1) return this.getSongListRecommend(++tryNum) + return this.filterSongList(body.data.special_list) + }) + }, + + /** + * 通过CollectionId获取歌单详情 + * @param {*} id + */ + async getUserListInfoByCollectionId(id) { + if (!id || id.length > 1000) return Promise.reject(new Error('get list error')) + if (this.collectionIdListInfoCache.has(id)) return this.collectionIdListInfoCache.get(id) + + const params = `appid=1058&specialid=0&global_specialid=${id}&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-` + return createHttpFetch(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163242519', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163242519', + }, + }).then(body => { + let info = { + type: body.type, + userName: body.nickname, + userAvatar: body.user_avatar, + imageUrl: body.imgurl, + desc: body.intro, + name: body.specialname, + globalSpecialid: body.global_specialid, + total: body.songcount, + playCount: body.playcount, + } + + this.collectionIdListInfoCache.set(id, info) + return info + }) + }, + /** + * 通过SpecialId获取歌单 + * @param {*} id + */ + // async getUserListDetailBySpecialId(id, page = 1, limit = 300) { + // if (!id || id.length > 1000) return Promise.reject(new Error('get list error.')) + // const listInfo = await this.getListInfoBySpecialId(id) + + // const params = `specialid=${id}&need_sort=1&module=CloudMusic&clientver=11589&pagesize=${limit}&userid=0&page=${page}&type=0&area_code=1&appid=1005` + // return createHttpFetch(`http://pubsongs.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params, 2)}`, { + // headers: { + // 'User-Agent': 'Android10-AndroidPhone-11589-201-0-playlist-wifi', + // }, + // }).then(body => { + // if (!body.info) return Promise.reject(new Error('Get list failed.')) + // const songList = this.filterListByCollectionId(body.info) + + // return { + // list: songList || [], + // page, + // limit, + // total: body.count, + // source: 'kg', + // info: { + // name: listInfo.name, + // img: listInfo.image, + // desc: listInfo.desc, + // // author: listInfo.userName, + // // play_count: formatPlayCount(listInfo.playCount), + // }, + // } + // }) + // }, + /** + * 通过CollectionId获取歌单 + * @param {*} id + */ + async getUserListDetailByCollectionId(id, page = 1, limit = 300) { + if (!id || id.length > 1000) return Promise.reject(new Error('ID error.')) + const listInfo = await this.getUserListInfoByCollectionId(id) + + const params = `need_sort=1&module=CloudMusic&clientver=11589&pagesize=${limit}&global_collection_id=${id}&userid=0&page=${page}&type=0&area_code=1&appid=1005` + return createHttpFetch(`http://pubsongs.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params, 'android')}`, { + headers: { + 'User-Agent': 'Android10-AndroidPhone-11589-201-0-playlist-wifi', + }, + }).then(body => { + if (!body.info) return Promise.reject(new Error('Get list failed.')) + const songList = this.filterListByCollectionId(body.info) + + return { + list: songList || [], + page, + limit, + total: listInfo.total, + source: 'kg', + info: { + name: listInfo.name, + img: listInfo.imageUrl && listInfo.imageUrl.replace('{size}', 240), + desc: listInfo.desc, + author: listInfo.userName, + play_count: formatPlayCount(listInfo.playCount), + }, + } + }) + }, + /** + * 过滤GlobalSpecialId歌单数据 + * @param {*} rawData + */ + filterListByCollectionId(rawData) { + let ids = new Set() + let list = [] + rawData.forEach(item => { + if (!item) return + if (ids.has(item.hash)) return + ids.add(item.hash) + const types = [] + const _types = {} + + item.relate_goods.forEach(data => { + let size = sizeFormate(data.size) + switch (data.level) { + case 2: + types.push({ type: '128k', size, hash: data.hash }) + _types['128k'] = { + size, + hash: data.hash, + } + break + case 4: + types.push({ type: '320k', size, hash: data.hash }) + _types['320k'] = { + size, + hash: data.hash, + } + break + case 5: + types.push({ type: 'flac', size, hash: data.hash }) + _types.flac = { + size, + hash: data.hash, + } + break + case 6: + types.push({ type: 'flac24bit', size, hash: data.hash }) + _types.flac24bit = { + size, + hash: data.hash, + } + break + } + }) + + list.push({ + singer: formatSingerName(item.singerinfo, 'name') || decodeName(item.name).split(' - ')[0].replace(/&/g, '、'), + name: decodeName(item.name).split(' - ')[1], + albumName: decodeName(item.albuminfo.name), + albumId: item.albuminfo.id, + songmid: item.audio_id, + source: 'kg', + interval: formatPlayTime(parseInt(item.timelen) / 1000), + img: null, + lrc: null, + hash: item.hash, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + }) + return list + }, + /** + * 通过酷狗码获取歌单 + * @param {*} id + * @param {*} page + */ + async getUserListDetailByCode(id, page = 1) { + // type 1单曲,2歌单,3电台,4酷狗码,5别人的播放队列 + const codeData = await createHttpFetch('http://t.kugou.com/command/', { + method: 'POST', + headers: { + 'KG-RC': 1, + 'KG-THash': 'network_super_call.cpp:3676261689:379', + 'User-Agent': '', + }, + body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: id }, + }) + if (!codeData) return Promise.reject(new Error('Get list failed.')) + const codeInfo = codeData.info + + switch (codeInfo.type) { + case 2: + if (!codeInfo.global_collection_id) return this.getUserListDetailBySpecialId(codeInfo.id, page) + break + case 3: + return album.getAlbumDetail(codeInfo.id, page) + } + if (codeInfo.global_collection_id) return this.getUserListDetailByCollectionId(codeInfo.global_collection_id, page) + + if (codeInfo.userid != null) { + const songList = await createHttpFetch('http://www2.kugou.kugou.com/apps/kucodeAndShare/app/', { + method: 'POST', + headers: { + 'KG-RC': 1, + 'KG-THash': 'network_super_call.cpp:3676261689:379', + 'User-Agent': '', + }, + body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: { id: codeInfo.id, type: 3, userid: codeInfo.userid, collect_type: 0, page: 1, pagesize: codeInfo.count } }, + }) + // console.log(songList) + let list = await getMusicInfosByList(songList || codeInfo.list) + return { + list, + page: 1, + limit: codeInfo.count, + total: list.length, + source: 'kg', + info: { + name: codeInfo.name, + img: (codeInfo.img_size && codeInfo.img_size.replace('{size}', 240)) || codeInfo.img, + // desc: body.result.info.list_desc, + author: codeInfo.username, + // play_count: formatPlayCount(info.count), + }, + } + } + }, + + async getUserListDetail3(chain, page) { + const songInfo = await createHttpFetch(`http://m.kugou.com/schain/transfer?pagesize=${this.listDetailLimit}&chain=${chain}&su=1&page=${page}&n=0.7928855356604456`, { + 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', + }, + }) + if (!songInfo.list) { + if (songInfo.global_collection_id) return this.getUserListDetailByCollectionId(songInfo.global_collection_id, page) + else return this.getUserListDetail4(songInfo, chain, page).catch(() => this.getUserListDetail5(chain)) + } + let list = await getMusicInfosByList(songInfo.list) + // console.log(info, songInfo) + return { + list, + page: 1, + limit: this.listDetailLimit, + total: list.length, + source: 'kg', + info: { + name: songInfo.info.name, + img: songInfo.info.img, + // desc: body.result.info.list_desc, + author: songInfo.info.username, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetailByLink({ info }, link) { + let listInfo = info['0'] + let total = listInfo.count + let tasks = [] + let page = 0 + while (total) { + const limit = total > 90 ? 90 : total + total -= limit + page += 1 + tasks.push(createHttpFetch(link.replace(/pagesize=\d+/, 'pagesize=' + limit).replace(/page=\d+/, 'page=' + page), { + 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, + }, + }).then(data => data.list.info)) + } + let result = await Promise.all(tasks).then(([...datas]) => datas.flat()) + result = await getMusicInfosByList(result) + // console.log(result) + return { + list: result, + page, + limit: this.listDetailLimit, + total: result.length, + source: 'kg', + info: { + name: listInfo.name, + img: listInfo.pic && listInfo.pic.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.list_create_username, + // play_count: formatPlayCount(listInfo.count), + }, + } + }, + createGetListDetail2Task(id, total) { + let tasks = [] + let page = 0 + while (total) { + const limit = total > 300 ? 300 : total + total -= limit + page += 1 + const params = 'appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&page=' + page + '&pagesize=' + limit + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-' + tasks.push(createHttpFetch(`https://mobiles.kugou.com/api/v5/special/song_v2?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163263991', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163263991', + }, + }).then(data => data.info)) + } + return Promise.all(tasks).then(([...datas]) => datas.flat()) + }, + async getUserListDetail2(global_collection_id) { + let id = global_collection_id + if (id.length > 1000) throw new Error('get list error') + const params = 'appid=1058&specialid=0&global_specialid=' + id + '&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-' + let info = await createHttpFetch(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`, { + headers: { + mid: '1586163242519', + Referer: 'https://m3ws.kugou.com/share/index.php', + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1', + dfid: '-', + clienttime: '1586163242519', + }, + }) + const songInfo = await this.createGetListDetail2Task(id, info.songcount) + let list = await getMusicInfosByList(songInfo) + // console.log(info, songInfo, list) + return { + list, + page: 1, + limit: this.listDetailLimit, + total: list.length, + source: 'kg', + info: { + name: info.specialname, + img: info.imgurl && info.imgurl.replace('{size}', 240), + desc: info.intro, + author: info.nickname, + play_count: formatPlayCount(info.playcount), + }, + } + }, + + async getListInfoByChain(chain) { + if (this.cache.has(chain)) return this.cache.get(chain) + const { body } = await httpFetch(`https://m.kugou.com/share/?chain=${chain}&id=${chain}`, { + 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', + }, + }).promise + // console.log(body) + let result = body.match(/var\sphpParam\s=\s({.+?});/) + if (result) result = JSON.parse(result[1]) + this.cache.set(chain, result) + return result + }, + + async getUserListDetailByPcChain(chain) { + let key = `${chain}_pc_list` + if (this.cache.has(key)) return this.cache.get(key) + const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + }, + }).promise + let result = body.match(/var\sdataFromSmarty\s=\s(\[.+?\])/) + if (result) result = JSON.parse(result[1]) + this.cache.set(chain, result) + result = await getMusicInfosByList(result) + // console.log(info, songInfo) + return result + }, + + async getUserListDetail4(songInfo, chain, page) { + const limit = 100 + const [listInfo, list] = await Promise.all([ + this.getListInfoByChain(chain), + this.getUserListDetailBySpecialId(songInfo.id, page, limit), + ]) + return { + list: list || [], + page, + limit, + total: list.length ?? 0, + source: 'kg', + info: { + name: listInfo.specialname, + img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.nickname, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetail5(chain) { + const [listInfo, list] = await Promise.all([ + this.getListInfoByChain(chain), + this.getUserListDetailByPcChain(chain), + ]) + return { + list: list || [], + page: 1, + limit: this.listDetailLimit, + total: list.length ?? 0, + source: 'kg', + info: { + name: listInfo.specialname, + img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240), + // desc: body.result.info.list_desc, + author: listInfo.nickname, + // play_count: formatPlayCount(info.count), + }, + } + }, + + async getUserListDetail(link, page, retryNum = 0) { + if (retryNum > 3) return Promise.reject(new Error('link try max num')) + + const requestLink = 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, + }, + follow_max: 2, + }) + const { headers: { location }, statusCode, body } = await requestLink.promise + // console.log(body, location, statusCode) + if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum) + if (typeof body == 'string') { + if (body.includes('"global_collection_id":')) return this.getUserListDetailByCollectionId(body.replace(/^[\s\S]+?"global_collection_id":"(\w+)"[\s\S]+?$/, '$1'), page) + if (body.includes('"albumid":')) return album.getAlbumDetail(body.replace(/^[\s\S]+?"albumid":(\w+)[\s\S]+?$/, '$1'), page) + if (body.includes('"album_id":') && link.includes('album/info')) return album.getAlbumDetail(body.replace(/^[\s\S]+?"album_id":(\w+)[\s\S]+?$/, '$1'), page) + if (body.includes('list_id = "') && link.includes('album/info')) return album.getAlbumDetail(body.replace(/^[\s\S]+?list_id = "(\w+)"[\s\S]+?$/, '$1'), page) + } + if (location) { + // 概念版分享链接 https://t1.kugou.com/xxx + if (location.includes('global_specialid')) return this.getUserListDetailByCollectionId(location.replace(/^.*?global_specialid=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (location.includes('global_collection_id')) return this.getUserListDetailByCollectionId(location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page) + if (location.includes('.html')) { + if (location.includes('zlist.html')) { + let link = location.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list') + if (link.includes('pagesize')) { + link = link.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page) + } else { + link += `&pagesize=${this.listDetailLimit}&page=${page}` + } + return this.getUserListDetail(link, page, ++retryNum) + } else return this.getUserListDetail3(location.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page) + } + return this.getUserListDetail(location, page, ++retryNum) + } + if (body.errcode !== 0) return this.getUserListDetail(link, page, ++retryNum) + return this.getUserListDetailByLink(body, link) + }, + + // 获取列表信息 + getListInfo(tagId, tryNum = 0) { + if (this._requestObj_listInfo) this._requestObj_listInfo.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_listInfo = httpFetch(this.getInfoUrl(tagId)) + return this._requestObj_listInfo.promise.then(({ body }) => { + if (body.status !== 1) return this.getListInfo(tagId, ++tryNum) + return { + limit: body.data.params.pagesize, + page: body.data.params.p, + total: body.data.params.total, + source: 'kg', + } + }) + }, + + // 获取列表数据 + getList(sortId, tagId, page) { + let tasks = [this.getSongList(sortId, tagId, page)] + tasks.push( + this.currentTagInfo.id === tagId + ? Promise.resolve(this.currentTagInfo.info) + : this.getListInfo(tagId).then(info => { + this.currentTagInfo.id = tagId + this.currentTagInfo.info = Object.assign({}, info) + return info + }), + ) + if (!tagId && page === 1 && sortId === this.sortList[0].id) tasks.push(this.getSongListRecommend()) // 如果是所有类别,则顺便获取推荐列表 + return Promise.all(tasks).then(([list, info, recommendList]) => { + if (recommendList) list.unshift(...recommendList) + return { + list, + ...info, + } + }) + }, + + // 获取标签 + 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.getInfoUrl()) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.status !== 1) return this.getTags(++tryNum) + return { + hotTag: this.filterInfoHotTag(body.data.hotTag), + tags: this.filterTagInfo(body.data.tagids), + source: 'kg', + } + }) + }, + + getDetailPageUrl(id) { + if (typeof id == 'string') { + if (/^https?:\/\//.test(id)) return id + id = id.replace('id_', '') + } + return `https://www.kugou.com/yy/special/single/${id}.html` + }, + + search(text, page, limit = 20) { + const params = `userid=1384394652&req_custom=1&appid=1005&req_multi=1&version=11589&page=${page}&filter=0&pagesize=${limit}&order=0&clienttime=1681779443&iscorrection=1&searchsong=0&keyword=${text}&mid=288799920684148686226285199951543865551&dfid=3eSBsO1u97EY1zeIZd40hH4p&clientver=11589&platform=AndroidFilter` + const url = encodeURI(`http://complexsearchretry.kugou.com/v1/search/special?${params}&signature=${signatureParams(params, 'android')}`) + return createHttpFetch(url).then(body => { + // console.log(body) + return { + list: body.lists.map(item => { + return { + play_count: formatPlayCount(item.total_play_count), + id: item.gid ? `gid_${item.gid}` : `id_${item.specialid}`, + author: item.nickname, + name: item.specialname, + time: dateFormat(item.publish_time, 'Y-M-D'), + img: item.img, + grade: item.grade, + desc: item.intro, + total: item.song_count, + source: 'kg', + } + }), + limit, + total: body.total, + source: 'kg', + } + }) + // http://msearchretry.kugou.com/api/v3/search/special?version=9209&keyword=%E5%91%A8%E6%9D%B0%E4%BC%A6&pagesize=20&filter=0&page=1&sver=2&with_res_tag=0 + // http://ioscdn.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&plat=2&version=7910&correct=1&sver=5 + // http://msearchretry.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&filter=0&version=7910&sver=2 + }, +} + +// getList +// getTags +// getListDetail diff --git a/musicSdk/kg/tipSearch.js b/musicSdk/kg/tipSearch.js new file mode 100644 index 0000000..038dcb1 --- /dev/null +++ b/musicSdk/kg/tipSearch.js @@ -0,0 +1,25 @@ +import { createHttpFetch } from './util.js' + +export default { + requestObj: null, + cancelTipSearch() { + if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp() + }, + tipSearchBySong(str) { + this.cancelTipSearch() + this.requestObj = createHttpFetch(`https://searchtip.kugou.com/getSearchTip?MusicTipCount=10&keyword=${encodeURIComponent(str)}`, { + headers: { + referer: 'https://www.kugou.com/', + }, + }) + return this.requestObj.then(body => { + return body[0].RecordDatas + }) + }, + handleResult(rawData) { + return rawData.map(info => info.HintInfo) + }, + async search(str) { + return this.tipSearchBySong(str).then(result => this.handleResult(result)) + }, +} diff --git a/musicSdk/kg/util.js b/musicSdk/kg/util.js new file mode 100644 index 0000000..3e83b43 --- /dev/null +++ b/musicSdk/kg/util.js @@ -0,0 +1,47 @@ +import { toMD5 } from '../utils.js' +import { httpFetch } from '../request.js' + +// s.content[0].lyricContent.forEach(([str]) => { +// console.log(str) +// }) + +/** + * 签名 + * @param {*} params + * @param {*} apiver + */ +export const signatureParams = (params, platform = 'android', body = '') => { + let keyparam = 'OIlwieks28dk2k092lksi2UIkp' + if (platform === 'web') keyparam = 'NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt' + let param_list = params.split('&') + param_list.sort() + let sign_params = `${keyparam}${param_list.join('')}${body}${keyparam}` + return toMD5(sign_params) +} + +/** + * 创建一个适用于KG的Http请求 + * @param {*} url + * @param {*} options + * @param {*} retryNum + */ +export const createHttpFetch = async(url, options, retryNum = 0) => { + 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) + } + // console.log(result.statusCode, result.body) + if (result.statusCode !== 200 || + ( + result.body.error_code ?? + result.body.errcode ?? + result.body.err_code) != 0 + ) return createHttpFetch(url, options, ++retryNum) + if (result.body.data) return result.body.data + if (Array.isArray(result.body.info)) return result.body + return result.body.info +} diff --git a/musicSdk/kg/vendors/infSign.min.js b/musicSdk/kg/vendors/infSign.min.js new file mode 100644 index 0000000..c1a9d43 --- /dev/null +++ b/musicSdk/kg/vendors/infSign.min.js @@ -0,0 +1,2 @@ +!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/album.js b/musicSdk/kw/album.js new file mode 100644 index 0000000..cf0d361 --- /dev/null +++ b/musicSdk/kw/album.js @@ -0,0 +1,131 @@ +import { httpFetch } from '../request.js' +import { decodeName } from '../index.js' +import { formatSinger, objStr2JSON } from './util.js' + +// let requestObj_list +export default { + limit_list: 36, + limit_song: 1000, + filterListDetail(rawList, albumName, albumId) { + // console.log(rawList) + // console.log(rawList.length, rawList2.length) + return rawList.map((item, inedx) => { + let formats = item.formats.split('|') + let types = [] + let _types = {} + if (formats.includes('MP3128')) { + types.push({ type: '128k', size: null }) + _types['128k'] = { + size: null, + } + } + // if (formats.includes('MP3192')) { + // types.push({ type: '192k', size: null }) + // _types['192k'] = { + // size: null, + // } + // } + if (formats.includes('MP3H')) { + types.push({ type: '320k', size: null }) + _types['320k'] = { + size: null, + } + } + // if (formats.includes('AL')) { + // types.push({ type: 'ape', size: null }) + // _types.ape = { + // size: null, + // } + // } + if (formats.includes('ALFLAC')) { + types.push({ type: 'flac', size: null }) + _types.flac = { + size: null, + } + } + if (formats.includes('HIRFLAC')) { + types.push({ type: 'flac24bit', size: null }) + _types.flac24bit = { + size: null, + } + } + // types.reverse() + return { + singer: formatSinger(decodeName(item.artist)), + name: decodeName(item.name), + albumName, + albumId, + songmid: item.id, + source: 'kw', + interval: null, + img: item.pic, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + /** + * 格式化播放数量 + * @param {*} num + */ + formatPlayCount(num) { + if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿' + if (num > 10000) return parseInt(num / 1000) / 10 + '万' + return num + }, + getAlbumListDetail(id, page, retryNum = 0) { + if (retryNum > 2) return Promise.reject(new Error('try max num')) + const requestObj_listDetail = httpFetch(`http://search.kuwo.cn/r.s?pn=${page - 1}&rn=${this.limit_song}&stype=albuminfo&albumid=${id}&show_copyright_off=0&encoding=utf&vipver=MUSIC_9.1.0`) + return requestObj_listDetail.promise.then(({ statusCode, body }) => { + if (statusCode !== 200) return this.getAlbumListDetail(id, page, ++retryNum) + body = objStr2JSON(body) + // console.log(body) + if (!body.musiclist) return this.getAlbumListDetail(id, page, ++retryNum) + body.name = decodeName(body.name) + return { + list: this.filterListDetail(body.musiclist, body.name, body.albumid), + page, + limit: this.limit_song, + total: parseInt(body.songnum), + source: 'kw', + info: { + name: body.name, + img: body.img || body.hts_img, + desc: decodeName(body.info), + author: decodeName(body.artist), + // play_count: this.formatPlayCount(body.playnum), + }, + } + }) + }, + // getAlbumListDetail(id, page, retryNum = 0) { + // if (retryNum > 2) return Promise.reject(new Error('try max num')) + // return tokenRequest(`http://www.kuwo.cn/api/www/album/albumInfo?albumId=${id}&pn=${page}&rn=${this.limit_song}&httpsStatus=1`).then((resp) => { + // return resp.promise.then(({ statusCode, body }) => { + // console.log(body) + // return Promise.reject(new Error('failed')) + // // if (statusCode !== 200) return this.getAlbumListDetail(id, page, ++retryNum) + // // const data = body.data + // // console.log(data) + // // if (!data.musicList) return this.getAlbumListDetail(id, page, ++retryNum) + // // return { + // // list: this.filterListDetail(data.musiclist), + // // page, + // // limit: this.limit_song, + // // total: data.total, + // // source: 'kw', + // // info: { + // // name: data.album, + // // img: data.pic, + // // desc: data.albuminfo, + // // author: data.artist, + // // play_count: this.formatPlayCount(data.playCnt), + // // }, + // // } + // }) + // }) + // }, +} diff --git a/musicSdk/kw/api-temp.js b/musicSdk/kw/api-temp.js new file mode 100644 index 0000000..9e53f47 --- /dev/null +++ b/musicSdk/kw/api-temp.js @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..fca67d9 --- /dev/null +++ b/musicSdk/kw/api-test.js @@ -0,0 +1,37 @@ +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/kw/comment.js b/musicSdk/kw/comment.js new file mode 100644 index 0000000..f3fbb3e --- /dev/null +++ b/musicSdk/kw/comment.js @@ -0,0 +1,82 @@ +import { httpFetch } from '../request.js' +import { dateFormat2 } from '../index.js' + +export default { + _requestObj: null, + _requestObj2: null, + async getComment({ songmid }, page = 1, limit = 20) { + if (this._requestObj) this._requestObj.cancelHttp() + + const _requestObj = httpFetch(`http://ncomment.kuwo.cn/com.s?f=web&type=get_comment&aapiver=1&prod=kwplayer_ar_10.5.2.0&digest=15&sid=${songmid}&start=${limit * (page - 1)}&msgflag=1&count=${limit}&newver=3&uid=0`, { + headers: { + 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)', + }, + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.code != '200') throw new Error('获取评论失败') + // console.log(body) + + const total = body.comments_counts + return { + source: 'kw', + comments: this.filterComment(body.comments), + total, + page, + limit, + maxPage: Math.ceil(total / limit) || 1, + } + }, + async getHotComment({ songmid }, page = 1, limit = 100) { + if (this._requestObj2) this._requestObj2.cancelHttp() + + const _requestObj2 = httpFetch(`http://ncomment.kuwo.cn/com.s?f=web&type=get_rec_comment&aapiver=1&prod=kwplayer_ar_10.5.2.0&digest=15&sid=${songmid}&start=${limit * (page - 1)}&msgflag=1&count=${limit}&newver=3&uid=0`, { + headers: { + 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)', + }, + }) + const { body, statusCode } = await _requestObj2.promise + if (statusCode != 200 || body.code != '200') throw new Error('获取热门评论失败') + // console.log(body) + + const total = body.hot_comments_counts + return { + source: 'kw', + comments: this.filterComment(body.hot_comments), + total, + page, + limit, + maxPage: Math.ceil(total / limit) || 1, + } + }, + filterComment(rawList) { + if (!rawList) return [] + return rawList.map(item => { + return { + id: item.id, + text: item.msg, + time: item.time, + timeStr: dateFormat2(Number(item.time) * 1000), + userName: item.u_name, + avatar: item.u_pic, + userId: item.u_id, + likedCount: item.like_num, + images: item.mpic ? [decodeURIComponent(item.mpic)] : [], + reply: item.child_comments + ? item.child_comments.map(i => { + return { + id: i.id, + text: i.msg, + time: i.time, + timeStr: dateFormat2(Number(i.time) * 1000), + userName: i.u_name, + avatar: i.u_pic, + userId: i.u_id, + likedCount: i.like_num, + images: i.mpic ? [i.mpic] : [], + } + }) + : [], + } + }) + }, +} diff --git a/musicSdk/kw/hotSearch.js b/musicSdk/kw/hotSearch.js new file mode 100644 index 0000000..27730f9 --- /dev/null +++ b/musicSdk/kw/hotSearch.js @@ -0,0 +1,22 @@ +import { httpFetch } from '../request.js' + +export default { + _requestObj: 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://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1¬race=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1', { + headers: { + 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)', + }, + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.status !== 'ok') throw new Error('获取热搜词失败') + // console.log(body, statusCode) + return { source: 'kw', list: this.filterList(body.tagvalue) } + }, + filterList(rawList) { + return rawList.map(item => item.key) + }, +} diff --git a/musicSdk/kw/index.js b/musicSdk/kw/index.js new file mode 100644 index 0000000..0aa1e1b --- /dev/null +++ b/musicSdk/kw/index.js @@ -0,0 +1,102 @@ +import { httpFetch } from '../request.js' +import tipSearch from './tipSearch.js' +import musicSearch from './musicSearch.js' +import { formatSinger } from './util.js' +import leaderboard from './leaderboard.js' +import lyric from './lyric.js' +import pic from './pic.js' +import { apis } from '../api-source.js' +import songList from './songList.js' +import hotSearch from './hotSearch.js' +import comment from './comment.js' + +const kw = { + _musicInfoRequestObj: null, + _musicInfoPromiseCancelFn: null, + _musicPicRequestObj: null, + _musicPicPromiseCancelFn: null, + // context: null, + + + // init(context) { + // if (this.isInited) return + // this.isInited = true + // this.context = context + + // // this.musicSearch.search('我又想你了').then(res => { + // // console.log(res) + // // }) + + // // this.getMusicUrl('62355680', '320k').then(url => { + // // console.log(url) + // // }) + // }, + + tipSearch, + musicSearch, + leaderboard, + songList, + hotSearch, + comment, + getLyric(songInfo, isGetLyricx) { + // let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer + return lyric.getLyric(songInfo, isGetLyricx) + }, + handleMusicInfo(songInfo) { + return this.getMusicInfo(songInfo).then(info => { + // console.log(JSON.stringify(info)) + songInfo.name = info.name + songInfo.singer = formatSinger(info.artist) + songInfo.img = info.pic + songInfo.albumName = info.album + return songInfo + // return Object.assign({}, songInfo, { + // name: info.name, + // singer: formatSinger(info.artist), + // img: info.pic, + // albumName: info.album, + // }) + }) + }, + + getMusicUrl(songInfo, type) { + return apis('kw').getMusicUrl(songInfo, type) + }, + + getMusicInfo(songInfo) { + if (this._musicInfoRequestObj) this._musicInfoRequestObj.cancelHttp() + this._musicInfoRequestObj = httpFetch(`http://www.kuwo.cn/api/www/music/musicInfo?mid=${songInfo.songmid}`) + return this._musicInfoRequestObj.promise.then(({ body }) => { + return body.code === 200 ? body.data : Promise.reject(new Error(body.msg)) + }) + }, + + getMusicUrls(musicInfo, cb) { + let tasks = [] + let songId = musicInfo.songmid + musicInfo.types.forEach(type => { + tasks.push(kw.getMusicUrl(songId, type.type).promise) + }) + Promise.all(tasks).then(urlInfo => { + let typeUrl = {} + urlInfo.forEach(info => { + typeUrl[info.type] = info.url + }) + cb(typeUrl) + }) + }, + + getPic(songInfo) { + return pic.getPic(songInfo) + }, + + getMusicDetailPageUrl(songInfo) { + return `http://www.kuwo.cn/play_detail/${songInfo.songmid}` + }, + + // init() { + // return getToken() + // }, +} + +export default kw diff --git a/musicSdk/kw/leaderboard.js b/musicSdk/kw/leaderboard.js new file mode 100644 index 0000000..8a82d73 --- /dev/null +++ b/musicSdk/kw/leaderboard.js @@ -0,0 +1,229 @@ +import { httpFetch } from '../request.js' +import { formatPlayTime, decodeName } from '../index.js' +import { formatSinger, wbdCrypto } from './util.js' + + +const boardList = [{ id: 'kw__93', name: '飙升榜', bangid: '93' }, { id: 'kw__17', name: '新歌榜', bangid: '17' }, { id: 'kw__16', name: '热歌榜', bangid: '16' }, { id: 'kw__158', name: '抖音热歌榜', bangid: '158' }, { id: 'kw__292', name: '铃声榜', bangid: '292' }, { id: 'kw__284', name: '热评榜', bangid: '284' }, { id: 'kw__290', name: 'ACG新歌榜', bangid: '290' }, { id: 'kw__286', name: '台湾KKBOX榜', bangid: '286' }, { id: 'kw__279', name: '冬日暖心榜', bangid: '279' }, { id: 'kw__281', name: '巴士随身听榜', bangid: '281' }, { id: 'kw__255', name: 'KTV点唱榜', bangid: '255' }, { id: 'kw__280', name: '家务进行曲榜', bangid: '280' }, { id: 'kw__282', name: '熬夜修仙榜', bangid: '282' }, { id: 'kw__283', name: '枕边轻音乐榜', bangid: '283' }, { id: 'kw__278', name: '古风音乐榜', bangid: '278' }, { id: 'kw__264', name: 'Vlog音乐榜', bangid: '264' }, { id: 'kw__242', name: '电音榜', bangid: '242' }, { id: 'kw__187', name: '流行趋势榜', bangid: '187' }, { id: 'kw__204', name: '现场音乐榜', bangid: '204' }, { id: 'kw__186', name: 'ACG神曲榜', bangid: '186' }, { id: 'kw__185', name: '最强翻唱榜', bangid: '185' }, { id: 'kw__26', name: '经典怀旧榜', bangid: '26' }, { id: 'kw__104', name: '华语榜', bangid: '104' }, { id: 'kw__182', name: '粤语榜', bangid: '182' }, { id: 'kw__22', name: '欧美榜', bangid: '22' }, { id: 'kw__184', name: '韩语榜', bangid: '184' }, { id: 'kw__183', name: '日语榜', bangid: '183' }, { id: 'kw__145', name: '会员畅听榜', bangid: '145' }, { id: 'kw__153', name: '网红新歌榜', bangid: '153' }, { id: 'kw__64', name: '影视金曲榜', bangid: '64' }, { id: 'kw__176', name: 'DJ嗨歌榜', bangid: '176' }, { id: 'kw__106', name: '真声音', bangid: '106' }, { id: 'kw__12', name: 'Billboard榜', bangid: '12' }, { id: 'kw__49', name: 'iTunes音乐榜', bangid: '49' }, { id: 'kw__180', name: 'beatport电音榜', bangid: '180' }, { id: 'kw__13', name: '英国UK榜', bangid: '13' }, { id: 'kw__164', name: '百大DJ榜', bangid: '164' }, { id: 'kw__246', name: 'YouTube音乐排行榜', bangid: '246' }, { id: 'kw__265', name: '韩国Genie榜', bangid: '265' }, { id: 'kw__14', name: '韩国M-net榜', bangid: '14' }, { id: 'kw__8', name: '香港电台榜', bangid: '8' }, { id: 'kw__15', name: '日本公信榜', bangid: '15' }, { id: 'kw__151', name: '腾讯音乐人原创榜', bangid: '151' }] + +const sortQualityArray = array => { + const qualityMap = { + flac24bit: 4, + flac: 3, + '320k': 2, + '128k': 1, + } + const rawQualityArray = [] + const newQualityArray = [] + + array.forEach((item, index) => { + const type = qualityMap[item.type] + if (!type) return + rawQualityArray.push({ type, index }) + }) + + rawQualityArray.sort((a, b) => a.type - b.type) + + rawQualityArray.forEach(item => { + newQualityArray.push(array[item.index]) + }) + + return newQualityArray +} + +export default { + list: [ + { + id: 'kwbiaosb', + name: '飙升榜', + bangid: 93, + }, + { + id: 'kwregb', + name: '热歌榜', + bangid: 16, + }, + { + id: 'kwhuiyb', + name: '会员榜', + bangid: 145, + }, + { + id: 'kwdouyb', + name: '抖音榜', + bangid: 158, + }, + { + id: 'kwqsb', + name: '趋势榜', + bangid: 187, + }, + { + id: 'kwhuaijb', + name: '怀旧榜', + bangid: 26, + }, + { + id: 'kwhuayb', + name: '华语榜', + bangid: 104, + }, + { + id: 'kwyueyb', + name: '粤语榜', + bangid: 182, + }, + { + id: 'kwoumb', + name: '欧美榜', + bangid: 22, + }, + { + id: 'kwhanyb', + name: '韩语榜', + bangid: 184, + }, + { + id: 'kwriyb', + name: '日语榜', + bangid: 183, + }, + ], + // getUrl: (p, l, id) => `http://kbangserver.kuwo.cn/ksong.s?from=pc&fmt=json&pn=${p - 1}&rn=${l}&type=bang&data=content&id=${id}&show_copyright_off=0&pcmp4=1&isbang=1`, + regExps: { + mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/, + }, + limit: 100, + _requestBoardsObj: null, + + getBoardsData() { + if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp() + this._requestBoardsObj = httpFetch('http://qukudata.kuwo.cn/q.k?op=query&cont=tree&node=2&pn=0&rn=1000&fmt=json&level=2') + return this._requestBoardsObj.promise + }, + getData(url) { + const requestDataObj = httpFetch(url) + return requestDataObj.promise + }, + filterData(rawList) { + return rawList.map(item => { + let types = [] + const _types = {} + const qualitys = new Set() + + item.n_minfo.split(';').forEach(i => { + const info = i.match(this.regExps.mInfo) + if (!info) return + + const quality = info[2] + const size = info[4].toLocaleUpperCase() + + if (qualitys.has(quality)) return + qualitys.add(quality) + + switch (quality) { + case '4000': + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { size } + break + case '2000': + types.push({ type: 'flac', size }) + _types.flac = { size } + break + case '320': + types.push({ type: '320k', size }) + _types['320k'] = { size } + break + case '128': + types.push({ type: '128k', size }) + _types['128k'] = { size } + break + } + }) + types = sortQualityArray(types) + + return { + singer: formatSinger(decodeName(item.artist)), + name: decodeName(item.name), + albumName: decodeName(item.album), + albumId: item.albumId, + songmid: item.id, + source: 'kw', + interval: formatPlayTime(parseInt(item.duration)), + img: item.pic, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + + filterBoardsData(rawList) { + // console.log(rawList) + let list = [] + for (const board of rawList) { + if (board.source != '1') continue + list.push({ + id: 'kw__' + board.sourceid, + name: board.name, + bangid: String(board.sourceid), + }) + } + 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) + // if (response.statusCode !== 200 || !response.body.child) return this.getBoards(retryNum) + // const list = this.filterBoardsData(response.body.child) + // // console.log(list) + // console.log(JSON.stringify(list)) + // this.list = list + // return { + // list, + // source: 'kw', + // } + this.list = boardList + return { + list: boardList, + source: 'kw', + } + }, + + getList(id, page, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + + const requestBody = { uid: '', devId: '', sFrom: 'kuwo_sdk', user_type: 'AP', carSource: 'kwplayercar_ar_6.0.1.0_apk_keluze.apk', id, pn: page - 1, rn: this.limit } + const requestUrl = `https://wbd.kuwo.cn/api/bd/bang/bang_info?${wbdCrypto.buildParam(requestBody)}` + const request = httpFetch(requestUrl).promise + + return request.then(({ statusCode, body }) => { + const rawData = wbdCrypto.decodeData(body) + // console.log(rawData) + const data = rawData.data + if (statusCode !== 200 || rawData.code != 200 || !data.musiclist) return this.getList(id, page, retryNum) + + const total = parseInt(data.total) + const list = this.filterData(data.musiclist) + + return { + total, + list, + limit: this.limit, + page, + source: 'kw', + } + }) + }, + + // getDetailPageUrl(id) { + // return `http://www.kuwo.cn/rankList/${id}` + // }, +} diff --git a/musicSdk/kw/lyric.js b/musicSdk/kw/lyric.js new file mode 100644 index 0000000..11f2e3d --- /dev/null +++ b/musicSdk/kw/lyric.js @@ -0,0 +1,270 @@ +import { httpFetch } from '../request.js' +import { decodeLyric, lrcTools } from './util.js' +import { decodeName } from '../index.js' + +/* +export default { + formatTime(time) { + let m = parseInt(time / 60) + let s = (time % 60).toFixed(2) + return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s) + }, + sortLrcArr(arr) { + const lrcSet = new Set() + let lrc = [] + let lrcT = [] + + for (const item of arr) { + if (lrcSet.has(item.time)) { + const tItem = lrc.pop() + tItem.time = lrc[lrc.length - 1].time + lrcT.push(tItem) + lrc.push(item) + } else { + lrc.push(item) + lrcSet.add(item.time) + } + } + + if (lrcT.length && lrc.length > lrcT.length) { + const tItem = lrc.pop() + tItem.time = lrc[lrc.length - 1].time + lrcT.push(tItem) + } + + return { + lrc, + lrcT, + } + }, + transformLrc(songinfo, lrclist) { + return `[ti:${songinfo.songName}]\n[ar:${songinfo.artist}]\n[al:${songinfo.album}]\n[by:]\n[offset:0]\n${lrclist ? lrclist.map(l => `[${this.formatTime(l.time)}]${l.lineLyric}\n`).join('') : '暂无歌词'}` + }, + getLyric(songId) { + const requestObj = httpFetch(`http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=${songId}`) + requestObj.promise = requestObj.promise.then(({ body }) => { + // console.log(body) + if (!body.data?.lrclist?.length) return Promise.reject(new Error('Get lyric failed')) + let lrcInfo + try { + lrcInfo = this.sortLrcArr(body.data.lrclist) + } catch { + return Promise.reject(new Error('Get lyric failed')) + } + // console.log(body.data.lrclist) + // console.log(lrcInfo.lrc, lrcInfo.lrcT) + // console.log({ + // lyric: decodeName(this.transformLrc(body.data.songinfo, lrc)), + // tlyric: decodeName(this.transformLrc(body.data.songinfo, lrcT)), + // }) + return { + lyric: decodeName(this.transformLrc(body.data.songinfo, lrcInfo.lrc)), + tlyric: lrcInfo.lrcT.length ? decodeName(this.transformLrc(body.data.songinfo, lrcInfo.lrcT)) : '', + } + }) + return requestObj + }, +} + */ + +const buf_key = Buffer.from('yeelion') +const buf_key_len = buf_key.length +const buildParams = (id, isGetLyricx) => { + let params = `user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_${id}` + if (isGetLyricx) params += '&lrcx=1' + const buf_str = Buffer.from(params) + const buf_str_len = buf_str.length + const output = new Uint16Array(buf_str_len) + let i = 0 + while (i < buf_str_len) { + let j = 0 + while (j < buf_key_len && i < buf_str_len) { + output[i] = buf_key[j] ^ buf_str[i] + i++ + j++ + } + } + return Buffer.from(output).toString('base64') +} + +// console.log(buildParams('207527604', false)) +// console.log(buildParams('207527604', true)) + +const timeExp = /^\[([\d:.]*)\]{1}/g +const existTimeExp = /\[\d{1,2}:.*\d{1,4}\]/ +const lyricxTag = /^<-?\d+,-?\d+>/ +export default { + /* sortLrcArr(arr) { + const lrcSet = new Set() + let lrc = [] + let lrcT = [] + let markIndex = [] + for (const item of arr) { + if (lrcSet.has(item.time)) { + if (lrc.length < 2) continue + const index = lrc.findIndex(l => l.time == item.time) + markIndex.push(index) + if (index == lrc.length - 1) { + lrcT.push({ ...lrc[index], time: item.time }) + lrc.push(item) + } else { + lrcT.push({ ...lrc[index], time: lrc[index + 1].time }) + if (item.text) { + // const lastIndex = lrc.length - 1 + // markIndex.push(lastIndex) + // lrcT.push({ ...lrc[lastIndex], time: lrc[lastIndex - 1].time }) + lrc.push(item) + } + } + } else { + lrc.push(item) + lrcSet.add(item.time) + } + } + + // console.log(markIndex) + markIndex = Array.from(new Set(markIndex)) + for (let index = markIndex.length - 1; index >= 0; index--) { + lrc.splice(markIndex[index], 1) + } + + // if (lrcT.length) { + // if (lrc.length * 0.4 < lrcT.length) { // 翻译数量需大于歌词数量的0.4倍,否则认为没有翻译 + // const tItem = lrc.pop() + // tItem.time = lrc[lrc.length - 1].time + // lrcT.push(tItem) + // } else { + // lrc = arr + // lrcT = [] + // } + // } + + console.log(lrc, lrcT) + + return { + lrc, + lrcT, + } + }, */ + sortLrcArr(arr) { + const lrcSet = new Set() + let lrc = [] + let lrcT = [] + + let isLyricx = false + for (const item of arr) { + if (lrcSet.has(item.time)) { + if (lrc.length < 2) continue + const tItem = lrc.pop() + tItem.time = lrc[lrc.length - 1].time + lrcT.push(tItem) + lrc.push(item) + } else { + lrc.push(item) + lrcSet.add(item.time) + } + if (!isLyricx && lyricxTag.test(item.text)) isLyricx = true + } + + if (!isLyricx && lrcT.length > lrc.length * 0.3 && lrc.length - lrcT.length > 6) { + throw new Error('failed') + // if (lrc.length * 0.4 < lrcT.length) { // 翻译数量需大于歌词数量的0.4倍,否则认为没有翻译 + // const tItem = lrc.pop() + // tItem.time = lrc[lrc.length - 1].time + // lrcT.push(tItem) + // } else { + // lrc = arr + // lrcT = [] + // } + } + + return { + lrc, + lrcT, + } + }, + transformLrc(tags, lrclist) { + return `${tags.join('\n')}\n${lrclist ? lrclist.map(l => `[${l.time}]${l.text}\n`).join('') : '暂无歌词'}` + }, + parseLrc(lrc) { + const lines = lrc.split(/\r\n|\r|\n/) + let tags = [] + let lrcArr = [] + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim() + let result = timeExp.exec(line) + if (result) { + const text = line.replace(timeExp, '').trim() + let time = RegExp.$1 + if (/\.\d\d$/.test(time)) time += '0' + lrcArr.push({ + time, + text, + }) + } else if (lrcTools.rxps.tagLine.test(line)) { + tags.push(line) + } + } + const lrcInfo = this.sortLrcArr(lrcArr) + return { + lyric: decodeName(this.transformLrc(tags, lrcInfo.lrc)), + tlyric: lrcInfo.lrcT.length ? decodeName(this.transformLrc(tags, lrcInfo.lrcT)) : '', + } + }, + // getLyric2(musicInfo, isGetLyricx = true) { + // const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`) + // requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => { + // if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body))) + // return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => { + // let lrcInfo + // console.log(Buffer.from(base64Data, 'base64').toString()) + // try { + // lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString()) + // } catch { + // return Promise.reject(new Error('Get lyric failed')) + // } + // if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '') + // lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric) + // // console.log(lrcInfo.lyric) + // // console.log(lrcInfo.tlyric) + // // console.log(lrcInfo.lxlyric) + // // console.log(JSON.stringify(lrcInfo)) + // }) + // }) + // return requestObj + // }, + getLyric(musicInfo, isGetLyricx = true) { + // this.getLyric2(musicInfo) + const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`) + requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => { + if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body))) + return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => { + // let lrcInfo + // try { + // lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString()) + // } catch { + // return Promise.reject(new Error('Get lyric failed')) + // } + let lrcInfo + // console.log(Buffer.from(base64Data, 'base64').toString()) + try { + lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString()) + } catch (err) { + return Promise.reject(new Error('Get lyric failed')) + } + // console.log(lrcInfo) + if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '') + try { + lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric) + } catch { + lrcInfo.lxlyric = '' + } + lrcInfo.lyric = lrcInfo.lyric.replace(lrcTools.rxps.wordTimeAll, '') + if (!existTimeExp.test(lrcInfo.lyric)) return Promise.reject(new Error('Get lyric failed')) + // console.log(lrcInfo) + return lrcInfo + }) + }) + return requestObj + }, +} diff --git a/musicSdk/kw/musicSearch.js b/musicSdk/kw/musicSearch.js new file mode 100644 index 0000000..ee9131d --- /dev/null +++ b/musicSdk/kw/musicSearch.js @@ -0,0 +1,126 @@ +// import '../../polyfill/array.find.js' + +import { httpFetch } from '../request.js' +import { formatPlayTime, decodeName } from '../index.js' +// import { debug } from '../../utils/env.js' +import { formatSinger } from './util.js' + +export default { + regExps: { + mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/, + }, + limit: 30, + total: 0, + page: 0, + allPage: 1, + // cancelFn: null, + musicSearch(str, page, limit) { + const musicSearchRequestObj = httpFetch(`http://search.kuwo.cn/r.s?client=kt&all=${encodeURIComponent(str)}&pn=${page - 1}&rn=${limit}&uid=794762570&ver=kwplayer_ar_9.2.2.1&vipver=1&show_copyright_off=1&newver=1&ft=music&cluster=0&strategy=2012&encoding=utf8&rformat=json&vermerge=1&mobi=1&issubtitle=1`) + return musicSearchRequestObj.promise + }, + // getImg(songId) { + // return httpGet(`http://player.kuwo.cn/webmusic/sj/dtflagdate?flag=6&rid=MUSIC_${songId}`) + // }, + // getLrc(songId) { + // return httpGet(`http://mobile.kuwo.cn/mpage/html5/songinfoandlrc?mid=${songId}&flag=0`) + // }, + handleResult(rawData) { + const result = [] + if (!rawData) return result + // console.log(rawData) + for (let i = 0; i < rawData.length; i++) { + const info = rawData[i] + let songId = info.MUSICRID.replace('MUSIC_', '') + // const format = (info.FORMATS || info.formats).split('|') + + if (!info.N_MINFO) { + console.log('N_MINFO is undefined') + return null + } + + const types = [] + const _types = {} + + let infoArr = info.N_MINFO.split(';') + for (let info of infoArr) { + info = info.match(this.regExps.mInfo) + if (info) { + switch (info[2]) { + case '4000': + types.push({ type: 'flac24bit', size: info[4] }) + _types.flac24bit = { + size: info[4].toLocaleUpperCase(), + } + break + case '2000': + types.push({ type: 'flac', size: info[4] }) + _types.flac = { + size: info[4].toLocaleUpperCase(), + } + break + case '320': + types.push({ type: '320k', size: info[4] }) + _types['320k'] = { + size: info[4].toLocaleUpperCase(), + } + break + case '128': + types.push({ type: '128k', size: info[4] }) + _types['128k'] = { + size: info[4].toLocaleUpperCase(), + } + break + } + } + } + types.reverse() + + let interval = parseInt(info.DURATION) + + result.push({ + name: decodeName(info.SONGNAME), + singer: formatSinger(decodeName(info.ARTIST)), + source: 'kw', + // img = (info.album.name === '' || info.album.name === '空') + // ? `http://player.kuwo.cn/webmusic/sj/dtflagdate?flag=6&rid=MUSIC_160911.jpg` + // : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${info.album.mid}.jpg` + songmid: songId, + albumId: decodeName(info.ALBUMID || ''), + interval: Number.isNaN(interval) ? 0 : formatPlayTime(interval), + albumName: info.ALBUM ? decodeName(info.ALBUM) : '', + lrc: null, + img: null, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + } + // console.log(result) + return result + }, + search(str, page = 1, limit, retryNum = 0) { + if (retryNum > 2) 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(({ body: result }) => { + // console.log(result) + if (!result || (result.TOTAL !== '0' && result.SHOW === '0')) return this.search(str, page, limit, ++retryNum) + let list = this.handleResult(result.abslist) + + if (list == null) return this.search(str, page, limit, ++retryNum) + + this.total = parseInt(result.TOTAL) + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + total: this.total, + limit, + source: 'kw', + }) + }) + }, +} diff --git a/musicSdk/kw/pic.js b/musicSdk/kw/pic.js new file mode 100644 index 0000000..72a334c --- /dev/null +++ b/musicSdk/kw/pic.js @@ -0,0 +1,9 @@ +import { httpFetch } from '../request.js' + +export default { + getPic({ songmid }) { + const requestObj = httpFetch(`http://artistpicserver.kuwo.cn/pic.web?corp=kuwo&type=rid_pic&pictype=500&size=500&rid=${songmid}`) + requestObj.promise = requestObj.promise.then(({ body }) => /^http/.test(body) ? body : null) + return requestObj.promise + }, +} diff --git a/musicSdk/kw/songList.js b/musicSdk/kw/songList.js new file mode 100644 index 0000000..b54400a --- /dev/null +++ b/musicSdk/kw/songList.js @@ -0,0 +1,488 @@ +import { httpFetch } from '../request.js' +import { formatPlayTime, decodeName } from '../index.js' +import { formatSinger, objStr2JSON } from './util.js' +import album from './album.js' + +export default { + _requestObj_tags: null, + _requestObj_hotTags: null, + _requestObj_list: null, + limit_list: 36, + limit_song: 1000, + successCode: 200, + sortList: [ + { + name: '最新', + id: 'new', + }, + { + name: '最热', + id: 'hot', + }, + ], + regExps: { + mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/, + // http://www.kuwo.cn/playlist_detail/2886046289 + // https://m.kuwo.cn/h5app/playlist/2736267853?t=qqfriend + listDetailLink: /^.+\/playlist(?:_detail)?\/(\d+)(?:\?.*|&.*$|#.*$|$)/, + }, + tagsUrl: 'http://wapi.kuwo.cn/api/pc/classify/playlist/getTagList?cmd=rcm_keyword_playlist&user=0&prod=kwplayer_pc_9.0.5.0&vipver=9.0.5.0&source=kwplayer_pc_9.0.5.0&loginUid=0&loginSid=0&appUid=76039576', + hotTagUrl: 'http://wapi.kuwo.cn/api/pc/classify/playlist/getRcmTagList?loginUid=0&loginSid=0&appUid=76039576', + getListUrl({ sortId, id, type, page }) { + if (!id) return `http://wapi.kuwo.cn/api/pc/classify/playlist/getRcmPlayList?loginUid=0&loginSid=0&appUid=76039576&&pn=${page}&rn=${this.limit_list}&order=${sortId}` + switch (type) { + case '10000': return `http://wapi.kuwo.cn/api/pc/classify/playlist/getTagPlayList?loginUid=0&loginSid=0&appUid=76039576&pn=${page}&id=${id}&rn=${this.limit_list}` + case '43': return `http://mobileinterfaces.kuwo.cn/er.s?type=get_pc_qz_data&f=web&id=${id}&prod=pc` + } + // http://wapi.kuwo.cn/api/pc/classify/playlist/getTagPlayList?loginUid=0&loginSid=0&appUid=76039576&id=173&pn=1&rn=100 + }, + getListDetailUrl(id, page) { + // http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=2858093057&pn=0&rn=100&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1 + return `http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=${id}&pn=${page - 1}&rn=${this.limit_song}&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1` + // http://mobileinterfaces.kuwo.cn/er.s?type=get_pc_qz_data&f=web&id=140&prod=pc + }, + + // http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=2849349915&pn=0&rn=100&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1 + // 获取标签 + 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) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getTag(++tryNum) + return this.filterTagInfo(body.data) + }) + }, + // 获取标签 + getHotTag(tryNum = 0) { + if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_hotTags = httpFetch(this.hotTagUrl) + return this._requestObj_hotTags.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getHotTag(++tryNum) + return this.filterInfoHotTag(body.data[0].data) + }) + }, + filterInfoHotTag(rawList) { + return rawList.map(item => ({ + id: `${item.id}-${item.digest}`, + name: item.name, + source: 'kw', + })) + }, + filterTagInfo(rawList) { + return rawList.map(type => ({ + name: type.name, + list: type.data.map(item => ({ + parent_id: type.id, + parent_name: type.name, + id: `${item.id}-${item.digest}`, + name: item.name, + source: 'kw', + })), + })) + }, + + // 获取列表数据 + 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')) + let id + let type + if (tagId) { + let arr = tagId.split('-') + id = arr[0] + type = arr[1] + } else { + id = null + } + this._requestObj_list = httpFetch(this.getListUrl({ sortId, id, type, page })) + return this._requestObj_list.promise.then(({ body }) => { + if (!id || type == '10000') { + if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) + return { + list: this.filterList(body.data.data), + total: body.data.total, + page: body.data.pn, + limit: body.data.rn, + source: 'kw', + } + } else if (!body.length) { + return this.getList(sortId, tagId, page, ++tryNum) + } + return { + list: this.filterList2(body), + total: 1000, + page, + limit: 1000, + source: 'kw', + } + }) + }, + + + /** + * 格式化播放数量 + * @param {*} num + */ + formatPlayCount(num) { + if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿' + if (num > 10000) return parseInt(num / 1000) / 10 + '万' + return num + }, + filterList(rawData) { + return rawData.map(item => ({ + play_count: this.formatPlayCount(item.listencnt), + id: `digest-${item.digest}__${item.id}`, + author: item.uname, + name: item.name, + // time: item.publish_time, + total: item.total, + img: item.img, + grade: item.favorcnt / 10, + desc: item.desc, + source: 'kw', + })) + }, + filterList2(rawData) { + // console.log(rawData) + const list = [] + rawData.forEach(item => { + if (!item.label) return + list.push(...item.list.map(item => ({ + play_count: item.play_count && this.formatPlayCount(item.listencnt), + id: `digest-${item.digest}__${item.id}`, + author: item.uname, + name: item.name, + total: item.total, + // time: item.publish_time, + img: item.img, + grade: item.favorcnt && item.favorcnt / 10, + desc: item.desc, + source: 'kw', + }))) + }) + return list + }, + + getListDetailDigest8(id, page, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + + const requestObj = httpFetch(this.getListDetailUrl(id, page)) + return requestObj.promise.then(({ body }) => { + if (body.result !== 'ok') return this.getListDetail(id, page, ++tryNum) + return { + list: this.filterListDetail(body.musiclist), + page, + limit: body.rn, + total: body.total, + source: 'kw', + info: { + name: body.title, + img: body.pic, + desc: body.info, + author: body.uname, + play_count: this.formatPlayCount(body.playnum), + }, + } + }) + }, + getListDetailDigest5Info(id, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + const requestObj = httpFetch(`http://qukudata.kuwo.cn/q.k?op=query&cont=ninfo&node=${id}&pn=0&rn=1&fmt=json&src=mbox&level=2`) + return requestObj.promise.then(({ statusCode, body }) => { + if (statusCode != 200 || !body.child) return this.getListDetail(id, ++tryNum) + // console.log(body) + return body.child.length ? body.child[0].sourceid : null + }) + }, + getListDetailDigest5Music(id, page, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + const requestObj = httpFetch(`http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=${id}&pn=${page - 1}}&rn=${this.limit_song}&encode=utf-8&keyset=pl2012&identity=kuwo&pcmp4=1`) + return requestObj.promise.then(({ body }) => { + // console.log(body) + if (body.result !== 'ok') return this.getListDetail(id, page, ++tryNum) + return { + list: this.filterListDetail(body.musiclist), + page, + limit: body.rn, + total: body.total, + source: 'kw', + info: { + name: body.title, + img: body.pic, + desc: body.info, + author: body.uname, + play_count: this.formatPlayCount(body.playnum), + }, + } + }) + }, + async getListDetailDigest5(id, page, retryNum) { + const detailId = await this.getListDetailDigest5Info(id, retryNum) + return this.getListDetailDigest5Music(detailId, page, retryNum) + }, + + filterBDListDetail(rawList) { + return rawList.map(item => { + let types = [] + let _types = {} + for (let info of item.audios) { + info.size = info.size?.toLocaleUpperCase() + switch (info.bitrate) { + case '4000': + types.push({ type: 'flac24bit', size: info.size }) + _types.flac24bit = { + size: info.size, + } + break + case '2000': + types.push({ type: 'flac', size: info.size }) + _types.flac = { + size: info.size, + } + break + case '320': + types.push({ type: '320k', size: info.size }) + _types['320k'] = { + size: info.size, + } + break + case '128': + types.push({ type: '128k', size: info.size }) + _types['128k'] = { + size: info.size, + } + break + } + } + types.reverse() + + return { + singer: item.artists.map(s => s.name).join('、'), + name: item.name, + albumName: item.album, + albumId: item.albumId, + songmid: item.id, + source: 'kw', + interval: formatPlayTime(item.duration), + img: item.albumPic, + releaseDate: item.releaseDate, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + getReqId() { + function t() { + return (65536 * (1 + Math.random()) | 0).toString(16).substring(1) + } + return t() + t() + t() + t() + t() + t() + t() + t() + }, + async getListDetailMusicListByBDListInfo(id, source) { + const { body: infoData } = await httpFetch(`https://bd-api.kuwo.cn/api/service/playlist/info/${id}?reqId=${this.getReqId()}&source=${source}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + plat: 'h5', + }, + }).promise.catch(() => ({ code: 0 })) + + if (infoData.code != 200) return null + + return { + name: infoData.data.name, + img: infoData.data.pic, + desc: infoData.data.description, + author: infoData.data.creatorName, + play_count: infoData.data.playNum, + } + }, + async getListDetailMusicListByBDUserPub(id) { + const { body: infoData } = await httpFetch(`https://bd-api.kuwo.cn/api/ucenter/users/pub/${id}?reqId=${this.getReqId()}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + plat: 'h5', + }, + }).promise.catch(() => ({ code: 0 })) + + if (infoData.code != 200) return null + + // console.log(infoData) + return { + name: infoData.data.userInfo.nickname + '喜欢的音乐', + img: infoData.data.userInfo.headImg, + desc: '', + author: infoData.data.userInfo.nickname, + play_count: '', + } + }, + async getListDetailMusicListByBDList(id, source, page, tryNum = 0) { + const { body: listData } = await httpFetch(`https://bd-api.kuwo.cn/api/service/playlist/${id}/musicList?reqId=${this.getReqId()}&source=${source}&pn=${page}&rn=${this.limit_song}`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + plat: 'h5', + }, + }).promise.catch(() => { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + return this.getListDetailMusicListByBDList(id, source, page, ++tryNum) + }) + + if (listData.code !== 200) return Promise.reject(new Error('failed')) + + return { + list: this.filterBDListDetail(listData.data.list), + page, + limit: listData.data.pageSize, + total: listData.data.total, + source: 'kw', + } + }, + async getListDetailMusicListByBD(id, page) { + const uid = /uid=(\d+)/.exec(id)?.[1] + const listId = /playlistId=(\d+)/.exec(id)?.[1] + const source = /source=(\d+)/.exec(id)?.[1] + if (!listId) return Promise.reject(new Error('failed')) + + const task = [this.getListDetailMusicListByBDList(listId, source, page)] + switch (source) { + case '4': + task.push(this.getListDetailMusicListByBDListInfo(listId, source)) + break + case '5': + task.push(this.getListDetailMusicListByBDUserPub(uid ?? listId)) + break + } + const [listData, info] = await Promise.all(task) + listData.info = info ?? { + name: '', + img: '', + desc: '', + author: '', + play_count: '', + } + // console.log(listData) + return listData + }, + + // 获取歌曲列表内的音乐 + getListDetail(id, page, retryNum = 0) { + // console.log(id) + // https://h5app.kuwo.cn/m/bodian/collection.html?uid=000&playlistId=000&source=5&ownerId=000 + // https://h5app.kuwo.cn/m/bodian/collection.html?uid=000&playlistId=000&source=4&ownerId= + if (/\/bodian\//.test(id)) return this.getListDetailMusicListByBD(id, page) + if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + else if (/^digest-/.test(id)) { + let [digest, _id] = id.split('__') + digest = digest.replace('digest-', '') + id = _id + switch (digest) { + case '8': + break + case '13': return album.getAlbumListDetail(id, page, retryNum) + case '5': + default: return this.getListDetailDigest5(id, page, retryNum) + } + } + return this.getListDetailDigest8(id, page, retryNum) + }, + filterListDetail(rawData) { + // console.log(rawData) + return rawData.map(item => { + let infoArr = item.N_MINFO.split(';') + let types = [] + let _types = {} + for (let info of infoArr) { + info = info.match(this.regExps.mInfo) + if (info) { + switch (info[2]) { + case '4000': + types.push({ type: 'flac24bit', size: info[4] }) + _types.flac24bit = { + size: info[4].toLocaleUpperCase(), + } + break + case '2000': + types.push({ type: 'flac', size: info[4] }) + _types.flac = { + size: info[4].toLocaleUpperCase(), + } + break + case '320': + types.push({ type: '320k', size: info[4] }) + _types['320k'] = { + size: info[4].toLocaleUpperCase(), + } + break + case '128': + types.push({ type: '128k', size: info[4] }) + _types['128k'] = { + size: info[4].toLocaleUpperCase(), + } + break + } + } + } + types.reverse() + + return { + singer: formatSinger(decodeName(item.artist)), + name: decodeName(item.name), + albumName: decodeName(item.album), + albumId: item.albumid, + songmid: item.id, + source: 'kw', + interval: formatPlayTime(parseInt(item.duration)), + img: null, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + getTags() { + return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'kw' })) + }, + getDetailPageUrl(id) { + if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1') + else if (/^digest-/.test(id)) { + let result = id.split('__') + id = result[1] + } + return `http://www.kuwo.cn/playlist_detail/${id}` + }, + + search(text, page, limit = 20) { + return httpFetch(`http://search.kuwo.cn/r.s?all=${encodeURIComponent(text)}&pn=${page - 1}&rn=${limit}&rformat=json&encoding=utf8&ver=mbox&vipver=MUSIC_8.7.7.0_BCS37&plat=pc&devid=28156413&ft=playlist&pay=0&needliveshow=0`) + .promise.then(({ body }) => { + body = objStr2JSON(body) + // console.log(body) + return { + list: body.abslist.map(item => { + return { + play_count: this.formatPlayCount(item.playcnt), + id: String(item.playlistid), + author: decodeName(item.nickname), + name: decodeName(item.name), + total: item.songnum, + // time: item.publish_time, + img: item.pic, + desc: decodeName(item.intro), + source: 'kw', + } + }), + limit, + total: parseInt(body.TOTAL), + source: 'kw', + } + }) + }, +} + +// getList +// getTags +// getListDetail diff --git a/musicSdk/kw/tipSearch.js b/musicSdk/kw/tipSearch.js new file mode 100644 index 0000000..f9253b8 --- /dev/null +++ b/musicSdk/kw/tipSearch.js @@ -0,0 +1,32 @@ +// import { decodeName } from '../index.js' +// import { tokenRequest } from './util.js' +import { httpFetch } from '../request.js' + +export default { + regExps: { + relWord: /RELWORD=(.+)/, + }, + requestObj: null, + async tipSearchBySong(str) { + // 报错403,加了referer还是有问题(直接换一个 + // this.requestObj = await tokenRequest(`http://www.kuwo.cn/api/www/search/searchKey?key=${encodeURIComponent(str)}`) + + this.cancelTipSearch() + this.requestObj = httpFetch(`https://tips.kuwo.cn/t.s?corp=kuwo&newver=3&p2p=1¬race=0&c=mbox&w=${encodeURIComponent(str)}&encoding=utf8&rformat=json`, { + Referer: 'http://www.kuwo.cn/', + }) + return this.requestObj.promise.then(({ body, statusCode }) => { + if (statusCode != 200 || !body.WORDITEMS) return Promise.reject(new Error('请求失败')) + return body.WORDITEMS + }) + }, + handleResult(rawData) { + return rawData.map(item => item.RELWORD) + }, + cancelTipSearch() { + if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp() + }, + async search(str) { + return this.tipSearchBySong(str).then(result => this.handleResult(result)) + }, +} diff --git a/musicSdk/kw/util.js b/musicSdk/kw/util.js new file mode 100644 index 0000000..61b2c05 --- /dev/null +++ b/musicSdk/kw/util.js @@ -0,0 +1,218 @@ +// import { httpGet, httpFetch } from '../request.js' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '../../src/common/ipcNames.js' +import { rendererInvoke } from '../../src/common/rendererIpc.js' +import { createCipheriv, createDecipheriv } from 'crypto' +import { toMD5 } from '../utils.js' + +// const kw_token = { +// token: null, +// isGetingToken: false, +// } + +// const translationMap = { +// "{'": '{"', +// "'}\n": '"}', +// "'}": '"}', +// "':'": '":"', +// "','": '","', +// "':{'": '":{"', +// "':['": '":["', +// "'}],'": '"}],"', +// "':[{'": '":[{"', +// "'},'": '"},"', +// "'},{'": '"},{"', +// "':[],'": '":[],"', +// "':{},'": '":{},"', +// "'}]}": '"}]}', +// } + +// export const objStr2JSON = str => { +// return JSON.parse(str.replace(/(^{'|'}\n$|'}$|':'|','|':\[{'|'}\],'|':{'|'},'|'},{'|':\['|':\[\],'|':{},'|'}]})/g, s => translationMap[s])) +// } + +export const objStr2JSON = str => { + return JSON.parse(str.replace(/('(?=(,\s*')))|('(?=:))|((?<=([:,]\s*))')|((?<={)')|('(?=}))/g, '"')) +} + + +export const formatSinger = rawData => rawData.replace(/&/g, '、') + +export const matchToken = headers => { + try { + return headers['set-cookie'][0].match(/kw_token=(\w+)/)[1] + } catch (err) { + return null + } +} + +// const wait = time => new Promise(resolve => setTimeout(() => resolve(), time)) + + +// export const getToken = (retryNum = 0) => new Promise((resolve, reject) => { +// if (retryNum > 2) return Promise.reject(new Error('try max num')) + +// if (kw_token.isGetingToken) return wait(1000).then(() => getToken(retryNum).then(token => resolve(token))) +// if (kw_token.token) return resolve(kw_token.token) +// kw_token.isGetingToken = true +// httpGet('http://www.kuwo.cn/', (err, resp) => { +// kw_token.isGetingToken = false +// if (err) return getToken(++retryNum) +// if (resp.statusCode != 200) return reject(new Error('获取失败')) +// const token = kw_token.token = matchToken(resp.headers) +// resolve(token) +// }) +// }) + +export const decodeLyric = base64Data => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, base64Data) + +// export const tokenRequest = async(url, options = {}) => { +// let token = kw_token.token +// if (!token) token = await getToken() +// if (!options.headers) { +// options.headers = { +// Referer: 'http://www.kuwo.cn/', +// csrf: token, +// cookie: 'kw_token=' + token, +// } +// } +// const requestObj = httpFetch(url, options) +// requestObj.promise = requestObj.promise.then(resp => { +// // console.log(resp) +// if (resp.statusCode == 200) { +// kw_token.token = matchToken(resp.headers) +// } +// return resp +// }) +// return requestObj +// } + +export const lrcTools = { + rxps: { + wordLine: /^(\[\d{1,2}:.*\d{1,4}\])\s*(\S+(?:\s+\S+)*)?\s*/, + tagLine: /\[(ver|ti|ar|al|offset|by|kuwo):\s*(\S+(?:\s+\S+)*)\s*\]/, + wordTimeAll: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/g, + wordTime: /<(-?\d+),(-?\d+)(?:,-?\d+)?>/, + }, + offset: 1, + offset2: 1, + isOK: false, + lines: [], + tags: [], + getWordInfo(str, str2, prevWord) { + const offset = parseInt(str) + const offset2 = parseInt(str2) + let startTime = Math.abs((offset + offset2) / (this.offset * 2)) + let endTime = Math.abs((offset - offset2) / (this.offset2 * 2)) + startTime + if (prevWord) { + if (startTime < prevWord.endTime) { + prevWord.endTime = startTime + if (prevWord.startTime > prevWord.endTime) { + prevWord.startTime = prevWord.endTime + } + + prevWord.newTimeStr = `<${prevWord.startTime},${prevWord.endTime - prevWord.startTime}>` + // console.log(prevWord) + } + } + return { + startTime, + endTime, + timeStr: `<${startTime},${endTime - startTime}>`, + } + }, + parseLine(line) { + if (line.length < 6) return + let result = this.rxps.wordLine.exec(line) + if (result) { + const time = result[1] + let words = result[2] + if (words == null) { + words = '' + } + const wordTimes = words.match(this.rxps.wordTimeAll) + if (!wordTimes) return + // console.log(wordTimes) + let preTimeInfo + for (const timeStr of wordTimes) { + const result = this.rxps.wordTime.exec(timeStr) + const wordInfo = this.getWordInfo(result[1], result[2], preTimeInfo) + words = words.replace(timeStr, wordInfo.timeStr) + if (preTimeInfo?.newTimeStr) words = words.replace(preTimeInfo.timeStr, preTimeInfo.newTimeStr) + preTimeInfo = wordInfo + } + this.lines.push(time + words) + return + } + result = this.rxps.tagLine.exec(line) + if (!result) return + if (result[1] == 'kuwo') { + let content = result[2] + if (content != null && content.includes('][')) { + content = content.substring(0, content.indexOf('][')) + } + const valueOf = parseInt(content, 8) + this.offset = Math.trunc(valueOf / 10) + this.offset2 = Math.trunc(valueOf % 10) + if (this.offset == 0 || Number.isNaN(this.offset) || this.offset2 == 0 || Number.isNaN(this.offset2)) { + this.isOK = false + } + } else { + this.tags.push(line) + } + }, + parse(lrc) { + // console.log(lrc) + const lines = lrc.split(/\r\n|\r|\n/) + const tools = Object.create(this) + tools.isOK = true + tools.offset = 1 + tools.offset2 = 1 + tools.lines = [] + tools.tags = [] + + for (const line of lines) { + if (!tools.isOK) throw new Error('failed') + tools.parseLine(line) + } + if (!tools.lines.length) return '' + let lrcs = tools.lines.join('\n') + if (tools.tags.length) lrcs = `${tools.tags.join('\n')}\n${lrcs}` + // console.log(lrcs) + return lrcs + }, +} + + +const createAesEncrypt = (buffer, mode, key, iv) => { + const cipher = createCipheriv(mode, key, iv) + return Buffer.concat([cipher.update(buffer), cipher.final()]) +} + +const createAesDecrypt = (buffer, mode, key, iv) => { + const cipher = createDecipheriv(mode, key, iv) + return Buffer.concat([cipher.update(buffer), cipher.final()]) +} + +export const wbdCrypto = { + aesMode: 'aes-128-ecb', + aesKey: Buffer.from([112, 87, 39, 61, 199, 250, 41, 191, 57, 68, 45, 114, 221, 94, 140, 228], 'binary'), + aesIv: '', + appId: 'y67sprxhhpws', + decodeData(base64Result) { + const data = Buffer.from(decodeURIComponent(base64Result), 'base64') + return JSON.parse(createAesDecrypt(data, this.aesMode, this.aesKey, this.aesIv).toString()) + }, + createSign(data, time) { + const str = `${this.appId}${data}${time}` + return toMD5(str).toUpperCase() + }, + buildParam(jsonData) { + const data = Buffer.from(JSON.stringify(jsonData)) + const time = Date.now() + + const encodeData = createAesEncrypt(data, this.aesMode, this.aesKey, this.aesIv).toString('base64') + const sign = this.createSign(encodeData, time) + + return `data=${encodeURIComponent(encodeData)}&time=${time}&appId=${this.appId}&sign=${sign}` + }, +} diff --git a/musicSdk/message.js b/musicSdk/message.js new file mode 100644 index 0000000..d7cce9d --- /dev/null +++ b/musicSdk/message.js @@ -0,0 +1,4 @@ +// 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 new file mode 100644 index 0000000..884c046 --- /dev/null +++ b/musicSdk/mg/album.js @@ -0,0 +1,51 @@ +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 + * @param {*} page + */ + 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, + image: info.imgItems.length ? info.imgItems[0].img : null, + desc: info.summary, + author: info.singer, + play_count: formatPlayCount(info.opNumItem.playNum), + total: info.totalCount, + } + }, +} diff --git a/musicSdk/mg/api-test.js b/musicSdk/mg/api-test.js new file mode 100644 index 0000000..4b81328 --- /dev/null +++ b/musicSdk/mg/api-test.js @@ -0,0 +1,26 @@ +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.js b/musicSdk/mg/comment.js new file mode 100644 index 0000000..b1af131 --- /dev/null +++ b/musicSdk/mg/comment.js @@ -0,0 +1,96 @@ +import { httpFetch } from '../request.js' +import getSongId from './songId.js' +import { dateFormat2 } from '../index.js' + +export default { + _requestObj: null, + _requestObj2: null, + _requestObj3: 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.js b/musicSdk/mg/hotSearch.js new file mode 100644 index 0000000..ced94c8 --- /dev/null +++ b/musicSdk/mg/hotSearch.js @@ -0,0 +1,18 @@ +import { httpFetch } from '../request.js' + +export default { + _requestObj: 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.js b/musicSdk/mg/index.js new file mode 100644 index 0000000..1723376 --- /dev/null +++ b/musicSdk/mg/index.js @@ -0,0 +1,32 @@ +import { apis } from '../api-source.js' +import leaderboard from './leaderboard.js' +import songList from './songList.js' +import musicSearch from './musicSearch.js' +import pic from './pic.js' +import lyric from './lyric.js' +import hotSearch from './hotSearch.js' +import comment from './comment.js' +// import tipSearch from './tipSearch.js' + +const mg = { + // tipSearch, + songList, + musicSearch, + leaderboard, + hotSearch, + comment, + 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.js b/musicSdk/mg/leaderboard.js new file mode 100644 index 0000000..922d66c --- /dev/null +++ b/musicSdk/mg/leaderboard.js @@ -0,0 +1,243 @@ +import { httpFetch } from '../request.js' +import { filterMusicInfoList } from './musicInfo.js' + +// 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' }, +// ] +const boardList = [ + { + 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, + getBoardsData() { + if (this.requestBoardsObj) this._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.js b/musicSdk/mg/lyric.js new file mode 100644 index 0000000..8925d84 --- /dev/null +++ b/musicSdk/mg/lyric.js @@ -0,0 +1,104 @@ +import { httpFetch } from '../request.js' +import { getMusicInfo } from './musicInfo.js' +import { decrypt } from './utils/mrc.js' + +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 lxlrcLines = [] + const lrcLines = [] + + 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 = startTime + let ms = time % 1000 + time /= 1000 + let m = parseInt(time / 60).toString().padStart(2, '0') + time %= 60 + let s = parseInt(time).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('') + lxlrcLines.push(`[${time}]${newWords}`) + } + return { + lyric: lrcLines.join('\n'), + lxlyric: lxlrcLines.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 => ({ lxlyric: '', 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.js b/musicSdk/mg/musicInfo.js new file mode 100644 index 0000000..55b3b4c --- /dev/null +++ b/musicSdk/mg/musicInfo.js @@ -0,0 +1,162 @@ +import { sizeFormate, formatPlayTime } from '../index.js' +import { createHttpFetch } from './utils/index.js' +import { formatSingerName } from '../utils.js' + +const createGetMusicInfosTask = (ids) => { + let list = ids + let tasks = [] + 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) => { + // console.log(rawList) + let ids = new Set() + const list = [] + rawList.forEach(item => { + if (!item.songId || ids.has(item.songId)) return + ids.add(item.songId) + const types = [] + const _types = {} + 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 = [] + rawList.forEach(item => { + if (!item.songId || ids.has(item.songId)) return + ids.add(item.songId) + const types = [] + const _types = {} + 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) => { + return getMusicInfos([copyrightId]).then(data => data[0]) +} + +export const getMusicInfos = async(copyrightIds) => { + return filterMusicInfoList((await createGetMusicInfosTask(copyrightIds)).flat()) +} diff --git a/musicSdk/mg/musicSearch.js b/musicSdk/mg/musicSearch.js new file mode 100644 index 0000000..d92d19f --- /dev/null +++ b/musicSdk/mg/musicSearch.js @@ -0,0 +1,223 @@ +import { httpFetch } from '../request.js' +import { sizeFormate, formatPlayTime } from '../index.js' +import { toMD5, formatSingerName } from '../utils.js' + +export const createSignature = (time, str) => { + 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 = [] + 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 = [] + const _types = {} + 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.js b/musicSdk/mg/pic.js new file mode 100644 index 0000000..595ed47 --- /dev/null +++ b/musicSdk/mg/pic.js @@ -0,0 +1,28 @@ +import { httpFetch } from '../request.js' +import getSongId from './songId.js' + +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.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.js b/musicSdk/mg/songId.js new file mode 100644 index 0000000..6fdd438 --- /dev/null +++ b/musicSdk/mg/songId.js @@ -0,0 +1,31 @@ +// import { httpFetch } from '../request.js' +import { getMusicInfo } from './musicInfo.js' + +const getSongId = async(mInfo) => { + 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.js b/musicSdk/mg/songList.js new file mode 100644 index 0000000..b4c3e4d --- /dev/null +++ b/musicSdk/mg/songList.js @@ -0,0 +1,362 @@ +import { httpFetch } from '../request.js' +import { formatPlayCount } from '../index.js' +import { filterMusicInfoListV5 } from './musicInfo.js' +import { createSignature } from './musicSearch.js' +import { createHttpFetch } from './utils/index.js' + +// 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, + _requestObj_list: 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 = [], 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 = [] + 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/temp/leaderboard-old.js b/musicSdk/mg/temp/leaderboard-old.js new file mode 100644 index 0000000..023c85b --- /dev/null +++ b/musicSdk/mg/temp/leaderboard-old.js @@ -0,0 +1,195 @@ +import { httpFetch } from '../../../request.js' +import { formatPlayTime } from '../../../index.js' +// import { sizeFormate } from '../index.js' + + +// 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__migumusic', name: '音乐榜', bangid: 'migumusic', webId: 'migumusic' }, + { id: 'mg__movies', name: '影视榜', bangid: 'movies', webId: 'movies' }, + { 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__23217754', name: 'MV榜', bangid: '23217754', webId: 'mv' }, + { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151', webId: 'newalbum' }, + { 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__22272904', name: '中国TOP排行榜', bangid: '22272904' }, + { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943', webId: 'mnet' }, + { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437', webId: 'uk' }, +] +// const boardList = [ +// { id: 'mg__jianjiao_newsong', bangid: 'jianjiao_newsong', name: '尖叫新歌榜' }, +// { id: 'mg__jianjiao_hotsong', bangid: 'jianjiao_hotsong', name: '尖叫热歌榜' }, +// { id: 'mg__jianjiao_original', bangid: 'jianjiao_original', name: '尖叫原创榜' }, +// { id: 'mg__migumusic', bangid: 'migumusic', name: '音乐榜' }, +// { id: 'mg__movies', bangid: 'movies', name: '影视榜' }, +// { id: 'mg__mainland', bangid: 'mainland', name: '内地榜' }, +// { id: 'mg__hktw', bangid: 'hktw', name: '港台榜' }, +// { id: 'mg__eur_usa', bangid: 'eur_usa', name: '欧美榜' }, +// { id: 'mg__jpn_kor', bangid: 'jpn_kor', name: '日韩榜' }, +// { id: 'mg__coloring', bangid: 'coloring', name: '彩铃榜' }, +// { id: 'mg__ktv', bangid: 'ktv', name: 'KTV榜' }, +// { id: 'mg__network', bangid: 'network', name: '网络榜' }, +// { id: 'mg__newalbum', bangid: 'newalbum', name: '新专辑榜' }, +// { id: 'mg__mv', bangid: 'mv', name: 'MV榜' }, +// { id: 'mg__itunes', bangid: 'itunes', name: '美国iTunes榜' }, +// { id: 'mg__billboard', bangid: 'billboard', name: '美国billboard榜' }, +// { id: 'mg__hito', bangid: 'hito', name: 'Hito中文榜' }, +// { id: 'mg__mnet', bangid: 'mnet', name: '韩国Melon榜' }, +// { id: 'mg__uk', bangid: 'uk', name: '英国UK榜' }, +// ] + +export default { + limit: 10000, + getUrl(id, page) { + const targetBoard = boardList.find(board => board.bangid == id) + return `https://music.migu.cn/v3/music/top/${targetBoard.webId}` + // return `http://m.music.migu.cn/migu/remoting/cms_list_tag?nid=${id}&pageSize=${this.limit}&pageNo=${page - 1}` + }, + successCode: '000000', + requestBoardsObj: null, + regExps: { + listData: /var listData = (\{.+\})<\/script>/, + }, + getData(url) { + const requestObj = httpFetch(url) + return requestObj.promise + }, + getSinger(singers) { + let arr = [] + singers.forEach(singer => { + arr.push(singer.name) + }) + return arr.join('、') + }, + getIntv(interval) { + if (!interval) return 0 + let intvArr = interval.split(':') + let intv = 0 + let unit = 1 + while (intvArr.length) { + intv += (intvArr.pop()) * unit + unit *= 60 + } + return parseInt(intv) + }, + formateIntv() { + + }, + filterData(rawData) { + // console.log(JSON.stringify(rawData)) + // console.log(rawData) + let ids = new Set() + const list = [] + rawData.forEach(item => { + if (ids.has(item.copyrightId)) return + ids.add(item.copyrightId) + + const types = [] + const _types = {} + + const size = null + types.push({ type: '128k', size }) + _types['128k'] = { size } + + if (item.hq) { + const size = null + types.push({ type: '320k', size }) + _types['320k'] = { size } + } + if (item.sq) { + const size = null + types.push({ type: 'flac', size }) + _types.flac = { size } + } + + list.push({ + singer: this.getSinger(item.singers), + name: item.name, + albumName: item.album && item.album.albumName, + albumId: item.album && item.album.albumId, + songmid: item.id, + copyrightId: item.copyrightId, + source: 'mg', + interval: item.duration ? formatPlayTime(this.getIntv(item.duration)) : null, + img: item.mediumPic ? `https:${item.mediumPic}` : null, + lrc: null, + // lrcUrl: item.lrcUrl, + otherSource: null, + types, + _types, + typeUrl: {}, + }) + }) + 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.contentItemList) + // // 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 }) => { + if (statusCode !== 200) return this.getList(bangid, page, retryNum) + let listData = body.match(this.regExps.listData) + if (!listData) return this.getList(bangid, page, retryNum) + const datas = JSON.parse(RegExp.$1) + // console.log(datas) + listData = this.filterData(datas.songs.items) + return { + total: datas.songs.itemTotal, + list: this.filterData(datas.songs.items), + limit: this.limit, + page, + source: 'mg', + } + }) + }, +} diff --git a/musicSdk/mg/tipSearch.js b/musicSdk/mg/tipSearch.js new file mode 100644 index 0000000..34bfe64 --- /dev/null +++ b/musicSdk/mg/tipSearch.js @@ -0,0 +1,25 @@ +import { createHttpFetch } from './utils/index.js' + +export default { + requestObj: null, + 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.js b/musicSdk/mg/utils/index.js new file mode 100644 index 0000000..3be6097 --- /dev/null +++ b/musicSdk/mg/utils/index.js @@ -0,0 +1,29 @@ +import { httpFetch } from '../../request.js' + +/** + * 创建一个适用于MG的Http请求 + * @param {*} url + * @param {*} options + * @param {*} retryNum + */ +export const createHttpFetch = async(url, options, retryNum = 0) => { + 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.js b/musicSdk/mg/utils/mrc.js new file mode 100644 index 0000000..98ebb68 --- /dev/null +++ b/musicSdk/mg/utils/mrc.js @@ -0,0 +1,104 @@ + +// 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, key) => { + 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)] + let i = j6 + j2 = toLong(data[i] - (toLong(toLong(j2 ^ j4) + toLong(j7 ^ key[toLong(toLong(3n & j6) ^ j5)])) ^ 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] + j2 = toLong(data[0n] - toLong(toLong(toLong(key[toLong(toLong(j6 & 3n) ^ j5)] ^ 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) => { + const arrayList = [] + for (const j of data) arrayList.push(longToBytes(j).toString('utf16le')) + return arrayList.join('') +} + +// https://stackoverflow.com/a/29132118 +const longToBytes = (l) => { + const result = Buffer.alloc(8) + for (let i = 0; i < 8; i++) { + result[i] = parseInt(l & 0xFFn) + l >>= 8n + } + return result +} + + +const toBigintArray = (data) => { + const length = Math.floor(data.length / 16) + const jArr = Array(length) + for (let i = 0; i < length; i++) { + jArr[i] = toLong(data.substring(i * 16, (i * 16) + 16)) + } + return jArr +} + +// https://github.com/lyswhut/lx-music-desktop/issues/445#issuecomment-1139338682 +const MAX = 9223372036854775807n +const MIN = -9223372036854775808n +const toLong = str => { + 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) => { + // 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.js b/musicSdk/options.js new file mode 100644 index 0000000..c6ce350 --- /dev/null +++ b/musicSdk/options.js @@ -0,0 +1,9 @@ +export const bHh = '624868746c' + +export const headers = { + 'User-Agent': 'lx-music request', + [bHh]: [bHh], +} + + +export const timeout = 15000 diff --git a/musicSdk/request.js b/musicSdk/request.js new file mode 100644 index 0000000..d7a8184 --- /dev/null +++ b/musicSdk/request.js @@ -0,0 +1,4 @@ +// SDK 根请求模块(转接至 src/common/request.js) +// musicSdk 内 `import { httpFetch } from '../request.js'` 的解析目标 + +export { httpFetch } from '../src/common/request.js' diff --git a/musicSdk/tx/api-test.js b/musicSdk/tx/api-test.js new file mode 100644 index 0000000..c68f42e --- /dev/null +++ b/musicSdk/tx/api-test.js @@ -0,0 +1,29 @@ +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/tx/comment.js b/musicSdk/tx/comment.js new file mode 100644 index 0000000..48ae29b --- /dev/null +++ b/musicSdk/tx/comment.js @@ -0,0 +1,282 @@ +import { httpFetch } from '../request.js' +import { dateFormat2 } from '../index.js' +import getMusicInfo from './musicInfo.js' + +const emojis = { + e400846: '😘', + e400874: '😴', + e400825: '😃', + e400847: '😙', + e400835: '😍', + e400873: '😳', + e400836: '😎', + e400867: '😭', + e400832: '😊', + e400837: '😏', + e400875: '😫', + e400831: '😉', + e400855: '😡', + e400823: '😄', + e400862: '😨', + e400844: '😖', + e400841: '😓', + e400830: '😈', + e400828: '😆', + e400833: '😋', + e400822: '😀', + e400843: '😕', + e400829: '😇', + e400824: '😂', + e400834: '😌', + e400877: '😷', + e400132: '🍉', + e400181: '🍺', + e401067: '☕️', + e400186: '🥧', + e400343: '🐷', + e400116: '🌹', + e400126: '🍃', + e400613: '💋', + e401236: '❤️', + e400622: '💔', + e400637: '💣', + e400643: '💩', + e400773: '🔪', + e400102: '🌛', + e401328: '🌞', + e400420: '👏', + e400914: '🙌', + e400408: '👍', + e400414: '👎', + e401121: '✋', + e400396: '👋', + e400384: '👉', + e401115: '✊', + e400402: '👌', + e400905: '🙈', + e400906: '🙉', + e400907: '🙊', + e400562: '👻', + e400932: '🙏', + e400644: '💪', + e400611: '💉', + e400185: '🎁', + e400655: '💰', + e400325: '🐥', + e400612: '💊', + e400198: '🎉', + e401685: '⚡️', + e400631: '💝', + e400768: '🔥', + e400432: '👑', +} + +const songIdMap = new Map() +const promises = new Map() + +export default { + _requestObj: null, + _requestObj2: null, + async getSongId({ songId, songmid }) { + if (songId) return songId + if (songIdMap.has(songmid)) return songIdMap.get(songmid) + if (promises.has(songmid)) return (await promises.get(songmid)).songId + const promise = getMusicInfo(songmid) + promises.set(promise) + const info = await promise + songIdMap.set(songmid, info.songId) + promises.delete(songmid) + return info.songId + }, + async getComment(mInfo, page = 1, limit = 20) { + if (this._requestObj) this._requestObj.cancelHttp() + const songId = await this.getSongId(mInfo) + + const _requestObj = httpFetch('http://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg', { + method: 'POST', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + }, + form: { + uin: '0', + format: 'json', + cid: '205360772', + reqtype: '2', + biztype: '1', + topid: songId, + cmd: '8', + needmusiccrit: '1', + pagenum: page - 1, + pagesize: limit, + }, + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.code !== 0) throw new Error('获取评论失败') + // console.log(body, statusCode) + const comment = body.comment + return { + source: 'tx', + comments: this.filterNewComment(comment.commentlist), + total: comment.commenttotal, + page, + limit, + maxPage: Math.ceil(comment.commenttotal / limit) || 1, + } + }, + async getHotComment(mInfo, page = 1, limit = 20) { + // const _requestObj2 = httpFetch('http://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg', { + // method: 'POST', + // headers: { + // 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + // }, + // form: { + // uin: '0', + // format: 'json', + // cid: '205360772', + // reqtype: '2', + // biztype: '1', + // topid: songId, + // cmd: '9', + // needmusiccrit: '1', + // pagenum: page - 1, + // pagesize: limit, + // }, + // }) + if (this._requestObj2) this._requestObj2.cancelHttp() + const songId = await this.getSongId(mInfo) + + const _requestObj2 = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'POST', + body: { + comm: { + cv: 4747474, + ct: 24, + format: 'json', + inCharset: 'utf-8', + outCharset: 'utf-8', + notice: 0, + platform: 'yqq.json', + needNewCode: 1, + uin: 0, + }, + req: { + module: 'music.globalComment.CommentRead', + method: 'GetHotCommentList', + param: { + BizType: 1, + BizId: String(songId), + LastCommentSeqNo: '', + PageSize: limit, + PageNum: page - 1, + HotType: 1, + WithAirborne: 0, + PicEnable: 1, + }, + }, + }, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.0.0', + referer: 'https://y.qq.com/', + origin: 'https://y.qq.com', + }, + }) + const { body, statusCode } = await _requestObj2.promise + // console.log('body', body) + if (statusCode != 200 || body.code !== 0 || body.req.code !== 0) throw new Error('获取热门评论失败') + const comment = body.req.data.CommentList + return { + source: 'tx', + comments: this.filterHotComment(comment.Comments), + total: comment.Total, + page, + limit, + maxPage: Math.ceil(comment.Total / limit) || 1, + } + }, + filterNewComment(rawList) { + return rawList.map(item => { + let time = this.formatTime(item.time) + let timeStr = time ? dateFormat2(time) : null + if (item.middlecommentcontent) { + let firstItem = item.middlecommentcontent[0] + firstItem.avatarurl = item.avatarurl + firstItem.praisenum = item.praisenum + item.avatarurl = null + item.praisenum = null + item.middlecommentcontent.reverse() + } + return { + id: `${item.rootcommentid}_${item.commentid}`, + rootId: item.rootcommentid, + text: item.rootcommentcontent ? this.replaceEmoji(item.rootcommentcontent).replace(/\\n/g, '\n') : '', + time: item.rootcommentid == item.commentid ? time : null, + timeStr: item.rootcommentid == item.commentid ? timeStr : null, + userName: item.rootcommentnick ? item.rootcommentnick.substring(1) : '', + avatar: item.avatarurl, + userId: item.encrypt_rootcommentuin, + likedCount: item.praisenum, + reply: item.middlecommentcontent + ? item.middlecommentcontent.map(c => { + // let index = c.subcommentid.lastIndexOf('_') + return { + id: `sub_${item.rootcommentid}_${c.subcommentid}`, + text: this.replaceEmoji(c.subcommentcontent).replace(/\\n/g, '\n'), + time: c.subcommentid == item.commentid ? time : null, + timeStr: c.subcommentid == item.commentid ? timeStr : null, + userName: c.replynick.substring(1), + avatar: c.avatarurl, + userId: c.encrypt_replyuin, + likedCount: c.praisenum, + } + }) + : [], + } + }) + }, + filterHotComment(rawList) { + return rawList.map(item => { + return { + id: `${item.SeqNo}_${item.CmId}`, + rootId: item.SeqNo, + text: item.Content ? this.replaceEmoji(item.Content).replace(/\\n/g, '\n') : '', + time: item.PubTime ? this.formatTime(item.PubTime) : null, + timeStr: item.PubTime ? dateFormat2(this.formatTime(item.PubTime)) : null, + userName: item.Nick ?? '', + images: item.Pic ? [item.Pic] : [], + avatar: item.Avatar, + location: item.Location ? item.Location : '', + userId: item.EncryptUin, + likedCount: item.PraiseNum, + reply: item.SubComments + ? item.SubComments.map(c => { + return { + id: `sub_${c.SeqNo}_${c.CmId}`, + text: this.replaceEmoji(c.Content).replace(/\\n/g, '\n'), + time: c.PubTime ? this.formatTime(c.PubTime) : null, + timeStr: c.PubTime ? dateFormat2(this.formatTime(c.PubTime)) : null, + userName: c.Nick ?? '', + avatar: c.Avatar, + images: c.Pic ? [c.Pic] : [], + userId: c.EncryptUin, + likedCount: c.PraiseNum, + } + }) + : [], + } + }) + }, + replaceEmoji(msg) { + let rxp = /^\[em\](e\d+)\[\/em\]$/ + let result = msg.match(/\[em\]e\d+\[\/em\]/g) + if (!result) return msg + result = Array.from(new Set(result)) + for (let item of result) { + let code = item.replace(rxp, '$1') + msg = msg.replace(new RegExp(item.replace('[em]', '\\[em\\]').replace('[/em]', '\\[\\/em\\]'), 'g'), emojis[code] || '') + } + return msg + }, + formatTime(time) { + return String(time).length < 10 ? null : parseInt(time + '000') + }, +} diff --git a/musicSdk/tx/hotSearch.js b/musicSdk/tx/hotSearch.js new file mode 100644 index 0000000..3983869 --- /dev/null +++ b/musicSdk/tx/hotSearch.js @@ -0,0 +1,54 @@ +import { httpFetch } from '../request.js' + +export default { + _requestObj: 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('https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg', { + // method: 'get', + // headers: { + // Referer: 'https://y.qq.com/portal/player.html', + // }, + // }) + const _requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'post', + body: { + comm: { + ct: '19', + cv: '1803', + guid: '0', + patch: '118', + psrf_access_token_expiresAt: 0, + psrf_qqaccess_token: '', + psrf_qqopenid: '', + psrf_qqunionid: '', + tmeAppID: 'qqmusic', + tmeLoginType: 0, + uin: '0', + wid: '0', + }, + hotkey: { + method: 'GetHotkeyForQQMusicPC', + module: 'tencent_musicsoso_hotkey.HotkeyService', + param: { + search_id: '', + uin: 0, + }, + }, + }, + headers: { + Referer: 'https://y.qq.com/portal/player.html', + }, + }) + const { body, statusCode } = await _requestObj.promise + // console.log(body) + if (statusCode != 200 || body.code !== 0) throw new Error('获取热搜词失败') + // console.log(body) + return { source: 'tx', list: this.filterList(body.hotkey.data.vec_hotkey) } + }, + filterList(rawList) { + return rawList.map(item => item.query) + }, +} diff --git a/musicSdk/tx/index.js b/musicSdk/tx/index.js new file mode 100644 index 0000000..78d3571 --- /dev/null +++ b/musicSdk/tx/index.js @@ -0,0 +1,33 @@ +import leaderboard from './leaderboard.js' +import lyric from './lyric.js' +import songList from './songList.js' +import musicSearch from './musicSearch.js' +import { apis } from '../api-source.js' +import hotSearch from './hotSearch.js' +import comment from './comment.js' +// import tipSearch from './tipSearch.js' + +const tx = { + // tipSearch, + leaderboard, + songList, + musicSearch, + hotSearch, + comment, + + getMusicUrl(songInfo, type) { + return apis('tx').getMusicUrl(songInfo, type) + }, + getLyric(songInfo) { + // let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer + return lyric.getLyric(songInfo) + }, + async getPic(songInfo) { + return `https://y.gtimg.cn/music/photo_new/T002R500x500M000${songInfo.albumId}.jpg` + }, + getMusicDetailPageUrl(songInfo) { + return `https://y.qq.com/n/yqq/song/${songInfo.songmid}.html` + }, +} + +export default tx diff --git a/musicSdk/tx/leaderboard.js b/musicSdk/tx/leaderboard.js new file mode 100644 index 0000000..84d87dd --- /dev/null +++ b/musicSdk/tx/leaderboard.js @@ -0,0 +1,247 @@ +import { httpFetch } from '../request.js' +import { formatPlayTime, sizeFormate } from '../index.js' +import { formatSingerName } from '../utils.js' + +let boardList = [{ id: 'tx__4', name: '流行指数榜', bangid: '4' }, { id: 'tx__26', name: '热歌榜', bangid: '26' }, { id: 'tx__27', name: '新歌榜', bangid: '27' }, { id: 'tx__62', name: '飙升榜', bangid: '62' }, { id: 'tx__58', name: '说唱榜', bangid: '58' }, { id: 'tx__57', name: '喜力电音榜', bangid: '57' }, { id: 'tx__28', name: '网络歌曲榜', bangid: '28' }, { id: 'tx__5', name: '内地榜', bangid: '5' }, { id: 'tx__3', name: '欧美榜', bangid: '3' }, { id: 'tx__59', name: '香港地区榜', bangid: '59' }, { id: 'tx__16', name: '韩国榜', bangid: '16' }, { id: 'tx__60', name: '抖快榜', bangid: '60' }, { id: 'tx__29', name: '影视金曲榜', bangid: '29' }, { id: 'tx__17', name: '日本榜', bangid: '17' }, { id: 'tx__52', name: '腾讯音乐人原创榜', bangid: '52' }, { id: 'tx__36', name: 'K歌金曲榜', bangid: '36' }, { id: 'tx__61', name: '台湾地区榜', bangid: '61' }, { id: 'tx__63', name: 'DJ舞曲榜', bangid: '63' }, { id: 'tx__64', name: '综艺新歌榜', bangid: '64' }, { id: 'tx__65', name: '国风热歌榜', bangid: '65' }, { id: 'tx__67', name: '听歌识曲榜', bangid: '67' }, { id: 'tx__72', name: '动漫音乐榜', bangid: '72' }, { id: 'tx__73', name: '游戏音乐榜', bangid: '73' }, { id: 'tx__75', name: '有声榜', bangid: '75' }, { id: 'tx__131', name: '校园音乐人排行榜', bangid: '131' }] + +export default { + limit: 300, + list: [ + { + id: 'txlxzsb', + name: '流行榜', + bangid: 4, + }, + { + id: 'txrgb', + name: '热歌榜', + bangid: 26, + }, + { + id: 'txwlhgb', + name: '网络榜', + bangid: 28, + }, + { + id: 'txdyb', + name: '抖音榜', + bangid: 60, + }, + { + id: 'txndb', + name: '内地榜', + bangid: 5, + }, + { + id: 'txxgb', + name: '香港榜', + bangid: 59, + }, + { + id: 'txtwb', + name: '台湾榜', + bangid: 61, + }, + { + id: 'txoumb', + name: '欧美榜', + bangid: 3, + }, + { + id: 'txhgb', + name: '韩国榜', + bangid: 16, + }, + { + id: 'txrbb', + name: '日本榜', + bangid: 17, + }, + { + id: 'txtybb', + name: 'YouTube榜', + bangid: 128, + }, + ], + listDetailRequest(id, period, limit) { + // console.log(id, period, limit) + return 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)', + }, + body: { + toplist: { + module: 'musicToplist.ToplistInfoServer', + method: 'GetDetail', + param: { + topid: id, + num: limit, + period, + }, + }, + comm: { + uin: 0, + format: 'json', + ct: 20, + cv: 1859, + }, + }, + }).promise + }, + regExps: { + periodList: //g, + period: /data-listname="(.+?)" data-tid=".*?\/(.+?)" data-date="(.+?)" .+?<\/i>/, + }, + periods: {}, + periodUrl: 'https://c.y.qq.com/node/pc/wk_v15/top.html', + _requestBoardsObj: null, + getBoardsData() { + if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp() + this._requestBoardsObj = httpFetch('https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg?g_tk=1928093487&inCharset=utf-8&outCharset=utf-8¬ice=0&format=json&uin=0&needNewCode=1&platform=h5') + return this._requestBoardsObj.promise + }, + getData(url) { + const requestDataObj = httpFetch(url) + return requestDataObj.promise + }, + filterData(rawList) { + // console.log(rawList) + return rawList.map(item => { + let types = [] + let _types = {} + if (item.file.size_128mp3 !== 0) { + let size = sizeFormate(item.file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (item.file.size_320mp3 !== 0) { + let size = sizeFormate(item.file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (item.file.size_flac !== 0) { + let size = sizeFormate(item.file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + if (item.file.size_hires !== 0) { + let size = sizeFormate(item.file.size_hires) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + } + // types.reverse() + return { + singer: formatSingerName(item.singer, 'name'), + name: item.title, + albumName: item.album.name, + albumId: item.album.mid, + source: 'tx', + interval: formatPlayTime(item.interval), + songId: item.id, + albumMid: item.album.mid, + strMediaMid: item.file.media_mid, + songmid: item.mid, + img: (item.album.name === '' || item.album.name === '空') + ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.album.mid}.jpg`, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + getPeriods(bangid) { + return this.getData(this.periodUrl).then(({ body: html }) => { + let result = html.match(this.regExps.periodList) + if (!result) return Promise.reject(new Error('get data failed')) + result.forEach(item => { + let result = item.match(this.regExps.period) + if (!result) return + this.periods[result[2]] = { + name: result[1], + bangid: result[2], + period: result[3], + } + }) + const info = this.periods[bangid] + return info && info.period + }) + }, + filterBoardsData(rawList) { + // console.log(rawList) + let list = [] + for (const board of rawList) { + // 排除 MV榜 + if (board.id == 201) continue + + if (board.topTitle.startsWith('巅峰榜·')) { + board.topTitle = board.topTitle.substring(4, board.topTitle.length) + } + if (!board.topTitle.endsWith('榜')) board.topTitle += '榜' + list.push({ + id: 'tx__' + board.id, + name: board.topTitle, + bangid: String(board.id), + }) + } + 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) + // if (response.statusCode !== 200 || response.body.code !== 0) return this.getBoards(retryNum) + // const list = this.filterBoardsData(response.body.data.topList) + // console.log(list) + // console.log(JSON.stringify(list)) + // this.list = list + // return { + // list, + // source: 'tx', + // } + this.list = boardList + return { + list: boardList, + source: 'tx', + } + }, + getList(bangid, page, retryNum = 0) { + if (++retryNum > 3) return Promise.reject(new Error('try max num')) + bangid = parseInt(bangid) + let info = this.periods[bangid] + let p = info ? Promise.resolve(info.period) : this.getPeriods(bangid) + return p.then(period => { + return this.listDetailRequest(bangid, period, this.limit).then(resp => { + if (resp.body.code !== 0) return this.getList(bangid, page, retryNum) + return { + total: resp.body.toplist.data.songInfoList.length, + list: this.filterData(resp.body.toplist.data.songInfoList), + limit: this.limit, + page: 1, + source: 'tx', + } + }) + }) + }, + + getDetailPageUrl(id) { + if (typeof id == 'string') id = id.replace('tx__', '') + return `https://y.qq.com/n/ryqq/toplist/${id}` + }, +} diff --git a/musicSdk/tx/lyric.js b/musicSdk/tx/lyric.js new file mode 100644 index 0000000..0d810fb --- /dev/null +++ b/musicSdk/tx/lyric.js @@ -0,0 +1,294 @@ +import { httpFetch } from '../request.js' +import getMusicInfo from './musicInfo.js' +import { rendererInvoke } from '../../src/common/rendererIpc.js' +import { WIN_MAIN_RENDERER_EVENT_NAME } from '../../src/common/ipcNames.js' + +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 }) + + +const parseTools = { + rxps: { + info: /^{"/, + lineTime: /^\[(\d+),\d+\]/, + lineTime2: /^\[([\d:.]+)\]/, + wordTime: /\(\d+,\d+\)/, + wordTimeAll: /(\(\d+,\d+\))/g, + timeLabelFixRxp: /(?:\.0+|0+)$/, + }, + msFormat(timeMs) { + if (Number.isNaN(timeMs)) return '' + let ms = timeMs % 1000 + timeMs /= 1000 + let m = parseInt(timeMs / 60).toString().padStart(2, '0') + timeMs %= 60 + let s = parseInt(timeMs).toString().padStart(2, '0') + return `[${m}:${s}.${String(ms).padStart(3, '0')}]` + }, + parseLyric(lrc) { + lrc = lrc.trim() + lrc = lrc.replace(/\r/g, '') + if (!lrc) return { lyric: '', lxlyric: '' } + const lines = lrc.split('\n') + + const lxlrcLines = [] + const lrcLines = [] + + for (let line of lines) { + line = line.trim() + let result = this.rxps.lineTime.exec(line) + if (!result) { + if (line.startsWith('[offset')) { + lxlrcLines.push(line) + lrcLines.push(line) + } + if (this.rxps.lineTime2.test(line)) { + // lxlrcLines.push(line) + lrcLines.push(line) + } + continue + } + + const startMsTime = parseInt(result[1]) + const startTimeStr = this.msFormat(startMsTime) + if (!startTimeStr) continue + + let words = line.replace(this.rxps.lineTime, '') + + lrcLines.push(`${startTimeStr}${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 `<${Math.max(parseInt(result[1]) - startMsTime, 0)},${result[2]}>` + }) + const wordArr = words.split(this.rxps.wordTime) + const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('') + lxlrcLines.push(`${startTimeStr}${newWords}`) + } + return { + lyric: lrcLines.join('\n'), + lxlyric: lxlrcLines.join('\n'), + } + }, + parseRlyric(lrc) { + lrc = lrc.trim() + lrc = lrc.replace(/\r/g, '') + if (!lrc) return { lyric: '', lxlyric: '' } + const lines = lrc.split('\n') + + const lrcLines = [] + + for (let line of lines) { + line = line.trim() + let result = this.rxps.lineTime.exec(line) + if (!result) continue + + const startMsTime = parseInt(result[1]) + const startTimeStr = this.msFormat(startMsTime) + if (!startTimeStr) continue + + let words = line.replace(this.rxps.lineTime, '') + + lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`) + } + return lrcLines.join('\n') + }, + removeTag(str) { + return str.replace(/^[\S\s]*?LyricContent="/, '').replace(/"\/>[\S\s]*?$/, '') + }, + getIntv(interval) { + if (!interval) return 0 + if (!interval.includes('.')) interval += '.0' + let arr = interval.split(/:|\./) + while (arr.length < 3) arr.unshift('0') + const [m, s, ms] = arr + return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms) + }, + fixRlrcTimeTag(rlrc, lrc) { + // console.log(lrc) + // console.log(rlrc) + const rlrcLines = rlrc.split('\n') + let lrcLines = lrc.split('\n') + // let temp = [] + let newLrc = [] + rlrcLines.forEach((line) => { + const result = this.rxps.lineTime2.exec(line) + if (!result) return + const words = line.replace(this.rxps.lineTime2, '') + if (!words.trim()) return + const t1 = this.getIntv(result[1]) + + while (lrcLines.length) { + const lrcLine = lrcLines.shift() + const lrcLineResult = this.rxps.lineTime2.exec(lrcLine) + if (!lrcLineResult) continue + const t2 = this.getIntv(lrcLineResult[1]) + if (Math.abs(t1 - t2) < 100) { + newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0])) + break + } + // temp.push(line) + } + // lrcLines = [...temp, ...lrcLines] + // temp = [] + }) + return newLrc.join('\n') + }, + fixTlrcTimeTag(tlrc, lrc) { + // console.log(lrc) + // console.log(tlrc) + const tlrcLines = tlrc.split('\n') + let lrcLines = lrc.split('\n') + // let temp = [] + let newLrc = [] + tlrcLines.forEach((line) => { + const result = this.rxps.lineTime2.exec(line) + if (!result) return + const words = line.replace(this.rxps.lineTime2, '') + if (!words.trim()) return + let time = result[1] + if (time.includes('.')) { + time += ''.padStart(3 - time.split('.')[1].length, '0') + } + const t1 = this.getIntv(time) + + while (lrcLines.length) { + const lrcLine = lrcLines.shift() + const lrcLineResult = this.rxps.lineTime2.exec(lrcLine) + if (!lrcLineResult) continue + const t2 = this.getIntv(lrcLineResult[1]) + if (Math.abs(t1 - t2) < 100) { + newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0])) + break + } + // temp.push(line) + } + // lrcLines = [...temp, ...lrcLines] + // temp = [] + }) + return newLrc.join('\n') + }, + parse(lrc, tlrc, rlrc) { + const info = { + lyric: '', + tlyric: '', + rlyric: '', + lxlyric: '', + } + if (lrc) { + let { lyric, lxlyric } = this.parseLyric(this.removeTag(lrc)) + info.lyric = lyric + info.lxlyric = lxlyric + // console.log(lyric) + // console.log(lxlyric) + } + if (rlrc) info.rlyric = this.fixRlrcTimeTag(this.parseRlyric(this.removeTag(rlrc)), info.lyric) + if (tlrc) info.tlyric = this.fixTlrcTimeTag(tlrc, info.lyric) + // console.log(info.lyric) + // console.log(info.tlyric) + // console.log(info.rlyric) + + return info + }, +} + + +export default { + successCode: 0, + async getSongId({ songId, songmid }) { + if (songId) return songId + if (songIdMap.has(songmid)) return songIdMap.get(songmid) + if (promises.has(songmid)) return (await promises.get(songmid)).songId + const promise = getMusicInfo(songmid) + promises.set(promise) + const info = await promise + songIdMap.set(songmid, info.songId) + promises.delete(songmid) + return info.songId + }, + async parseLyric(lrc, tlrc, rlrc) { + const { lyric, tlyric, rlyric } = await decodeLyric(lrc, tlrc, rlrc) + // return { + + // } + // console.log(lyric) + // console.log(tlyric) + // console.log(rlyric) + return parseTools.parse(lyric, tlyric, rlyric) + }, + getLyric(mInfo, retryNum = 0) { + if (retryNum > 3) return Promise.reject(new Error('Get lyric failed')) + + return { + cancelHttp() { + + }, + promise: this.getSongId(mInfo).then(songId => { + const requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'post', + headers: { + referer: 'https://y.qq.com', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36', + }, + body: { + comm: { + ct: '19', + cv: '1859', + uin: '0', + }, + req: { + method: 'GetPlayLyricInfo', + module: 'music.musichallSong.PlayLyricInfo', + param: { + format: 'json', + crypt: 1, + ct: 19, + cv: 1873, + interval: 0, + lrc_t: 0, + qrc: 1, + qrc_t: 0, + roma: 1, + roma_t: 0, + songID: songId, + trans: 1, + trans_t: 0, + type: -1, + }, + }, + }, + }) + return requestObj.promise.then(({ body }) => { + // console.log(body) + if (body.code != this.successCode || body.req.code != this.successCode) return this.getLyric(songId, ++retryNum) + const data = body.req.data + return this.parseLyric(data.lyric, data.trans, data.roma) + }) + }), + } + }, +} + +// export default { +// regexps: { +// matchLrc: /.+"lyric":"([\w=+/]*)".+/, +// }, +// getLyric(songmid) { +// const requestObj = httpFetch(`https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?songmid=${songmid}&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&platform=yqq`, { +// headers: { +// Referer: 'https://y.qq.com/portal/player.html', +// }, +// }) +// requestObj.promise = requestObj.promise.then(({ body }) => { +// if (body.code != 0 || !body.lyric) return Promise.reject(new Error('Get lyric failed')) +// return { +// lyric: decodeName(b64DecodeUnicode(body.lyric)), +// tlyric: decodeName(b64DecodeUnicode(body.trans)), +// } +// }) +// return requestObj +// }, +// } diff --git a/musicSdk/tx/musicInfo.js b/musicSdk/tx/musicInfo.js new file mode 100644 index 0000000..e2a28ee --- /dev/null +++ b/musicSdk/tx/musicInfo.js @@ -0,0 +1,98 @@ +import { httpFetch } from '../request.js' +import { formatPlayTime, sizeFormate } from '../index.js' + +const getSinger = (singers) => { + let arr = [] + singers.forEach(singer => { + arr.push(singer.name) + }) + return arr.join('、') +} + +export default (songmid) => { + const requestObj = 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)', + }, + body: { + comm: { + ct: '19', + cv: '1859', + uin: '0', + }, + req: { + module: 'music.pf_song_detail_svr', + method: 'get_song_detail_yqq', + param: { + song_type: 0, + song_mid: songmid, + }, + }, + }, + }) + return requestObj.promise.then(({ body }) => { + // console.log(body) + if (body.code != 0 || body.req.code != 0) return Promise.reject(new Error('获取歌曲信息失败')) + const item = body.req.data.track_info + if (!item.file?.media_mid) return null + + let types = [] + let _types = {} + const file = item.file + if (file.size_128mp3 != 0) { + let size = sizeFormate(file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (file.size_320mp3 !== 0) { + let size = sizeFormate(file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (file.size_flac !== 0) { + let size = sizeFormate(file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + if (file.size_hires !== 0) { + let size = sizeFormate(file.size_hires) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + } + // types.reverse() + let albumId = '' + let albumName = '' + if (item.album) { + albumName = item.album.name + albumId = item.album.mid + } + return { + singer: getSinger(item.singer), + name: item.title, + albumName, + albumId, + source: 'tx', + interval: formatPlayTime(item.interval), + songId: item.id, + albumMid: item.album?.mid ?? '', + strMediaMid: item.file.media_mid, + songmid: item.mid, + img: (albumId === '' || albumId === '空') + ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg`, + types, + _types, + typeUrl: {}, + } + }) +} + diff --git a/musicSdk/tx/musicSearch.js b/musicSdk/tx/musicSearch.js new file mode 100644 index 0000000..5c49e82 --- /dev/null +++ b/musicSdk/tx/musicSearch.js @@ -0,0 +1,168 @@ +import { formatPlayTime, sizeFormate } from '../index.js' +import { formatSingerName } from '../utils.js' +import { signRequest } from './utils/index.js' + +export default { + limit: 50, + total: 0, + page: 0, + allPage: 1, + successCode: 0, + musicSearch(str, page, limit, retryNum = 0) { + if (retryNum > 5) return Promise.reject(new Error('搜索失败')) + const searchRequest = signRequest({ + comm: { + ct: '11', + cv: '14090508', + v: '14090508', + tmeAppID: 'qqmusic', + phonetype: 'EBG-AN10', + deviceScore: '553.47', + devicelevel: '50', + newdevicelevel: '20', + rom: 'HuaWei/EMOTION/EmotionUI_14.2.0', + os_ver: '12', + OpenUDID: '0', + OpenUDID2: '0', + QIMEI36: '0', + udid: '0', + chid: '0', + aid: '0', + oaid: '0', + taid: '0', + tid: '0', + wid: '0', + uid: '0', + sid: '0', + modeSwitch: '6', + teenMode: '0', + ui_mode: '2', + nettype: '1020', + v4ip: '', + }, + req: { + module: 'music.search.SearchCgiService', + method: 'DoSearchForQQMusicMobile', + param: { + search_type: 0, + searchid: Math.random().toString().slice(2), + query: str, + page_num: page, + num_per_page: limit, + highlight: 0, + nqc_flag: 0, + multi_zhida: 0, + cat: 2, + grp: 1, + sin: 0, + sem: 0, + }, + }, + }) + return searchRequest.then(({ body }) => { + // console.log(body) + if (!body || !body.req || body.code != this.successCode || body.req.code != this.successCode) { + return this.musicSearch(str, page, limit, ++retryNum) + } + return body.req.data + }) + }, + // randomInt(min, max) { + // return Math.floor(Math.random() * (max - min + 1)) + min + // }, + // getSearchId() { + // const e = BigInt(this.randomInt(1, 20)) + // const t = e * 18014398509481984n + // const n = BigInt(this.randomInt(0, 4194304)) * 4294967296n + // const a = BigInt(Date.now()) + // const r = (a * 1000n) % (24n * 60n * 60n * 1000n) + // return String(t + n + r) + // }, + handleResult(rawList) { + // console.log(rawList) + if (!rawList || !Array.isArray(rawList)) return [] + const list = [] + rawList.forEach(item => { + if (!item.file?.media_mid) return + + let types = [] + let _types = {} + const file = item.file + if (file.size_128mp3 != 0) { + let size = sizeFormate(file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (file.size_320mp3 !== 0) { + let size = sizeFormate(file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (file.size_flac !== 0) { + let size = sizeFormate(file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + if (file.size_hires !== 0) { + let size = sizeFormate(file.size_hires) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + } + // types.reverse() + let albumId = '' + let albumName = '' + if (item.album) { + albumName = item.album.name + albumId = item.album.mid + } + list.push({ + singer: formatSingerName(item.singer, 'name'), + // name: item.name + (item.title_extra ?? ''), + name: item.title, + albumName, + albumId, + source: 'tx', + interval: formatPlayTime(item.interval), + songId: item.id, + albumMid: item.album?.mid ?? '', + strMediaMid: item.file.media_mid, + songmid: item.mid, + img: (albumId === '' || albumId === '空') + ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg`, + types, + _types, + typeUrl: {}, + }) + }) + // console.log(list) + return list + }, + search(str, page = 1, limit) { + if (limit == null) limit = this.limit + // http://newlyric.kuwo.cn/newlyric.lrc?62355680 + return this.musicSearch(str, page, limit).then(({ body, meta }) => { + let list = this.handleResult(body.item_song) + + this.total = meta.estimate_sum + this.page = page + this.allPage = Math.ceil(this.total / limit) + + return Promise.resolve({ + list, + allPage: this.allPage, + limit, + total: this.total, + source: 'tx', + }) + }) + }, +} diff --git a/musicSdk/tx/singer.js b/musicSdk/tx/singer.js new file mode 100644 index 0000000..7ddbc44 --- /dev/null +++ b/musicSdk/tx/singer.js @@ -0,0 +1,249 @@ +import { httpFetch } from '../request.js' + +import { formatPlayTime, sizeFormate } from '../index.js' +import { formatSingerName } from '../utils.js' + +export const filterMusicInfoItem = item => { + const types = [] + const _types = {} + if (item.file.size_128mp3 != 0) { + let size = sizeFormate(item.file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (item.file.size_320mp3 !== 0) { + let size = sizeFormate(item.file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (item.file.size_flac !== 0) { + let size = sizeFormate(item.file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + if (item.file.size_hires !== 0) { + let size = sizeFormate(item.file.size_hires) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + } + + const albumId = item.album.id ?? '' + const albumMid = item.album.mid ?? '' + const albumName = item.album.name ?? '' + return { + source: 'tx', + singer: formatSingerName(item.singer, 'name'), + name: item.title, + albumName, + albumId, + albumMid, + interval: formatPlayTime(item.interval), + songId: item.id, + songmid: item.mid, + strMediaMid: item.file.media_mid, + img: (albumId === '' || albumId === '空') + ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumMid}.jpg`, + types, + _types, + typeUrl: {}, + } +} + + +/** + * 创建一个适用于TX的Http请求 + * @param {*} url + * @param {*} options + * @param {*} retryNum + */ +const createMusicuFetch = async(data, options, retryNum = 0) => { + if (retryNum > 2) throw new Error('try max num') + + let result + try { + result = await httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', { + method: 'POST', + body: { + comm: { + cv: 4747474, + ct: 24, + format: 'json', + inCharset: 'utf-8', + outCharset: 'utf-8', + uin: 0, + }, + ...data, + }, + headers: { + 'User-Angent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + }, + }).promise + } catch (err) { + console.log(err) + return createMusicuFetch(data, options, ++retryNum) + } + if (result.statusCode !== 200 || result.body.code != 0) return createMusicuFetch(data, options, ++retryNum) + + return result.body +} + +export default { + /** + * 获取歌手信息 + * @param {*} id + */ + getInfo(id) { + return createMusicuFetch({ + req_1: { + module: 'music.musichallSinger.SingerInfoInter', + method: 'GetSingerDetail', + param: { + singer_mid: [id], + ex_singer: 1, + wiki_singer: 1, + group_singer: 0, + pic: 1, + photos: 0, + }, + }, + req_2: { + module: 'music.musichallAlbum.AlbumListServer', + method: 'GetAlbumList', + param: { + singerMid: id, + order: 0, + begin: 0, + num: 1, + songNumTag: 0, + singerID: 0, + }, + }, + req_3: { + module: 'musichall.song_list_server', + method: 'GetSingerSongList', + param: { + singerMid: id, + order: 1, + begin: 0, + num: 1, + }, + }, + }).then(body => { + if (body.req_1.code != 0 || body.req_2 != 0 || body.req_3 != 0) throw new Error('get singer info faild.') + + const info = body.req_1.data.singer_list[0] + const music = body.req_3.data + const album = body.req_3.data + return { + source: 'tx', + id: info.basic_info.singer_mid, + info: { + name: info.basic_info.name, + desc: info.ex_info.desc, + avatar: info.pic.pic, + gender: info.ex_info.genre === 1 ? 'man' : 'woman', + }, + count: { + music: music.totalNum, + album: album.total, + }, + } + }) + }, + /** + * 获取歌手专辑列表 + * @param {*} id + * @param {*} page + * @param {*} limit + */ + getAlbumList(id, page = 1, limit = 10) { + if (page === 1) page = 0 + return createMusicuFetch({ + req: { + module: 'music.musichallAlbum.AlbumListServer', + method: 'GetAlbumList', + param: { + singerMid: id, + order: 0, + begin: page * limit, + num: limit, + songNumTag: 0, + singerID: 0, + }, + }, + }).then(body => { + if (body.req.code != 0) throw new Error('get singer album faild.') + + const list = this.filterAlbumList(body.req.data.albumList) + return { + source: 'tx', + list, + limit, + page, + total: body.req.data.total, + } + }) + }, + /** + * 获取歌手歌曲列表 + * @param {*} id + * @param {*} page + * @param {*} limit + */ + async getSongList(id, page = 1, limit = 100) { + if (page === 1) page = 0 + return createMusicuFetch({ + req: { + module: 'musichall.song_list_server', + method: 'GetSingerSongList', + param: { + singerMid: id, + order: 1, + begin: page * limit, + num: limit, + }, + }, + }).then(body => { + if (body.req.code != 0) throw new Error('get singer song list faild.') + + const list = this.filterSongList(body.req.data.songList) + return { + source: 'tx', + list, + limit, + page, + total: body.req.data.totalNum, + } + }) + }, + filterAlbumList(raw) { + return raw.map(item => { + return { + id: item.albumID, + mid: item.albumMid, + count: item.totalNum, + info: { + name: item.albumName, + author: item.singerName, + img: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.albumMid}.jpg`, + desc: null, + }, + } + }) + }, + filterSongList(raw) { + raw.map(item => { + return filterMusicInfoItem(item.songInfo) + }) + }, +} + diff --git a/musicSdk/tx/songList.js b/musicSdk/tx/songList.js new file mode 100644 index 0000000..3c983c5 --- /dev/null +++ b/musicSdk/tx/songList.js @@ -0,0 +1,328 @@ +import { httpFetch } from '../request.js' +import { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../index.js' +import { formatSingerName } from '../utils.js' + +export default { + _requestObj_tags: null, + _requestObj_hotTags: null, + _requestObj_list: null, + limit_list: 36, + limit_song: 100000, + successCode: 0, + sortList: [ + { + name: '最热', + id: 5, + }, + { + name: '最新', + id: 2, + }, + ], + regExps: { + hotTagHtml: /class="c_bg_link js_tag_item" data-id="\w+">.+?<\/a>/g, + hotTag: /data-id="(\w+)">(.+?)<\/a>/, + + // https://y.qq.com/n/yqq/playlist/7217720898.html + // https://i.y.qq.com/n2/m/share/details/taoge.html?platform=11&appshare=android_qq&appversion=9050006&id=7217720898&ADTAG=qfshare + listDetailLink: /\/playlist\/(\d+)/, + listDetailLink2: /id=(\d+)/, + }, + tagsUrl: 'https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8¬ice=0&platform=wk_v15.json&needNewCode=0&data=%7B%22tags%22%3A%7B%22method%22%3A%22get_all_categories%22%2C%22param%22%3A%7B%22qq%22%3A%22%22%7D%2C%22module%22%3A%22playlist.PlaylistAllCategoriesServer%22%7D%7D', + hotTagUrl: 'https://c.y.qq.com/node/pc/wk_v15/category_playlist.html', + getListUrl(sortId, id, page) { + if (id) { + id = parseInt(id) + return `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8¬ice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({ + comm: { cv: 1602, ct: 20 }, + playlist: { + method: 'get_category_content', + param: { + titleid: id, + caller: '0', + category_id: id, + size: this.limit_list, + page: page - 1, + use_page: 1, + }, + module: 'playlist.PlayListCategoryServer', + }, + }))}` + } + return `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8¬ice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({ + comm: { cv: 1602, ct: 20 }, + playlist: { + method: 'get_playlist_by_tag', + param: { id: 10000000, sin: this.limit_list * (page - 1), size: this.limit_list, order: sortId, cur_page: page }, + module: 'playlist.PlayListPlazaServer', + }, + }))}` + }, + getListDetailUrl(id) { + return `https://c.y.qq.com/qzone/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg?type=1&json=1&utf8=1&onlysong=0&new_format=1&disstid=${id}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0` + }, + + // http://nplserver.kuwo.cn/pl.svc?op=getlistinfo&pid=2849349915&pn=0&rn=100&encode=utf8&keyset=pl2012&identity=kuwo&pcmp4=1&vipver=MUSIC_9.0.5.0_W1&newver=1 + // 获取标签 + 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) + return this._requestObj_tags.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getTag(++tryNum) + return this.filterTagInfo(body.tags.data.v_group) + }) + }, + // 获取标签 + getHotTag(tryNum = 0) { + if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp() + if (tryNum > 2) return Promise.reject(new Error('try max num')) + this._requestObj_hotTags = httpFetch(this.hotTagUrl) + return this._requestObj_hotTags.promise.then(({ statusCode, body }) => { + if (statusCode !== 200) return this.getHotTag(++tryNum) + return this.filterInfoHotTag(body) + }) + }, + filterInfoHotTag(html) { + let hotTag = html.match(this.regExps.hotTagHtml) + const hotTags = [] + if (!hotTag) return hotTags + + hotTag.forEach(tagHtml => { + let result = tagHtml.match(this.regExps.hotTag) + if (!result) return + hotTags.push({ + id: parseInt(result[1]), + name: result[2], + source: 'tx', + }) + }) + return hotTags + }, + filterTagInfo(rawList) { + return rawList.map(type => ({ + name: type.group_name, + list: type.v_item.map(item => ({ + parent_id: type.group_id, + parent_name: type.group_name, + id: item.id, + name: item.name, + source: 'tx', + })), + })) + }, + + // 获取列表数据 + 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), + ) + // console.log(this.getListUrl(sortId, tagId, page)) + return this._requestObj_list.promise.then(({ body }) => { + if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum) + return tagId ? this.filterList2(body.playlist.data, page) : this.filterList(body.playlist.data, page) + }) + }, + + filterList(data, page) { + return { + list: data.v_playlist.map(item => ({ + play_count: formatPlayCount(item.access_num), + id: String(item.tid), + author: item.creator_info.nick, + name: item.title, + time: item.modify_time ? dateFormat(item.modify_time * 1000, 'Y-M-D') : '', + img: item.cover_url_medium, + // grade: item.favorcnt / 10, + total: item.song_ids?.length, + desc: decodeName(item.desc).replace(/
    /g, '\n'), + source: 'tx', + })), + total: data.total, + page, + limit: this.limit_list, + source: 'tx', + } + }, + filterList2({ content }, page) { + // console.log(content.v_item) + return { + list: content.v_item.map(({ basic }) => ({ + play_count: formatPlayCount(basic.play_cnt), + id: String(basic.tid), + author: basic.creator.nick, + name: basic.title, + // time: basic.publish_time, + img: basic.cover.medium_url || basic.cover.default_url, + // grade: basic.favorcnt / 10, + desc: decodeName(basic.desc).replace(/
    /g, '\n'), + source: 'tx', + })), + total: content.total_cnt, + page, + limit: this.limit_list, + source: 'tx', + } + }, + + async handleParseId(link, retryNum = 0) { + if (retryNum > 2) return Promise.reject(new Error('link try max num')) + + const requestObj_listDetailLink = httpFetch(link) + const { headers: { location }, statusCode } = await requestObj_listDetailLink.promise + // console.log(headers) + if (statusCode > 400) return this.handleParseId(link, ++retryNum) + return location == null ? link : location + }, + + async getListId(id) { + if ((/[?&:/]/.test(id))) { + if (!this.regExps.listDetailLink.test(id)) { + id = await this.handleParseId(id) + } + let result = this.regExps.listDetailLink.exec(id) + if (!result) { + result = this.regExps.listDetailLink2.exec(id) + if (!result) throw new Error('failed') + } + id = result[1] + // console.log(id) + } + return id + }, + // 获取歌曲列表内的音乐 + async getListDetail(id, tryNum = 0) { + if (tryNum > 2) return Promise.reject(new Error('try max num')) + + id = await this.getListId(id) + + const requestObj_listDetail = httpFetch(this.getListDetailUrl(id), { + headers: { + Origin: 'https://y.qq.com', + Referer: `https://y.qq.com/n/yqq/playsquare/${id}.html`, + }, + }) + const { body } = await requestObj_listDetail.promise + + if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum) + const cdlist = body.cdlist[0] + return { + list: this.filterListDetail(cdlist.songlist), + page: 1, + limit: cdlist.songlist.length + 1, + total: cdlist.songlist.length, + source: 'tx', + info: { + name: cdlist.dissname, + img: cdlist.logo, + desc: decodeName(cdlist.desc).replace(/
    /g, '\n'), + author: cdlist.nickname, + play_count: formatPlayCount(cdlist.visitnum), + }, + } + }, + filterListDetail(rawList) { + // console.log(rawList) + return rawList.map(item => { + let types = [] + let _types = {} + if (item.file.size_128mp3 !== 0) { + let size = sizeFormate(item.file.size_128mp3) + types.push({ type: '128k', size }) + _types['128k'] = { + size, + } + } + if (item.file.size_320mp3 !== 0) { + let size = sizeFormate(item.file.size_320mp3) + types.push({ type: '320k', size }) + _types['320k'] = { + size, + } + } + if (item.file.size_flac !== 0) { + let size = sizeFormate(item.file.size_flac) + types.push({ type: 'flac', size }) + _types.flac = { + size, + } + } + if (item.file.size_hires !== 0) { + let size = sizeFormate(item.file.size_hires) + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { + size, + } + } + // types.reverse() + return { + singer: formatSingerName(item.singer, 'name'), + name: item.title, + albumName: item.album.name, + albumId: item.album.mid, + source: 'tx', + interval: formatPlayTime(item.interval), + songId: item.id, + albumMid: item.album.mid, + strMediaMid: item.file.media_mid, + songmid: item.mid, + img: (item.album.name === '' || item.album.name === '空') + ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : '' + : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.album.mid}.jpg`, + lrc: null, + otherSource: null, + types, + _types, + typeUrl: {}, + } + }) + }, + getTags() { + return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'tx' })) + }, + + async getDetailPageUrl(id) { + id = await this.getListId(id) + + return `https://y.qq.com/n/ryqq/playlist/${id}` + }, + + search(text, page, limit = 20, retryNum = 0) { + if (retryNum > 5) throw new Error('max retry') + return httpFetch(`http://c.y.qq.com/soso/fcgi-bin/client_music_search_songlist?page_no=${page - 1}&num_per_page=${limit}&format=json&query=${encodeURIComponent(text)}&remoteplace=txt.yqq.playlist&inCharset=utf8&outCharset=utf-8`, { + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', + Referer: 'http://y.qq.com/portal/search.html', + }, + }) + .promise.then(({ body }) => { + if (body.code != 0) return this.search(text, page, limit, ++retryNum) + // console.log(body.data.list) + return { + list: body.data.list.map(item => { + return { + play_count: formatPlayCount(item.listennum), + id: String(item.dissid), + author: decodeName(item.creator.name), + name: decodeName(item.dissname), + time: dateFormat(item.createtime, 'Y-M-D'), + img: item.imgurl, + // grade: item.favorcnt / 10, + total: item.song_count, + desc: decodeName(decodeName(item.introduction)).replace(/
    /g, '\n'), + source: 'tx', + } + }), + limit, + total: body.data.sum, + source: 'tx', + } + }) + }, +} + +// getList +// getTags +// getListDetail diff --git a/musicSdk/tx/tipSearch.js b/musicSdk/tx/tipSearch.js new file mode 100644 index 0000000..ec21628 --- /dev/null +++ b/musicSdk/tx/tipSearch.js @@ -0,0 +1,29 @@ +import { httpFetch } from '../request.js' + +export default { + // regExps: { + // relWord: /RELWORD=(.+)/, + // }, + requestObj: null, + tipSearch(str) { + this.cancelTipSearch() + this.requestObj = httpFetch(`https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg?is_xml=0&format=json&key=${encodeURIComponent(str)}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0`, { + headers: { + Referer: 'https://y.qq.com/portal/player.html', + }, + }) + return this.requestObj.promise.then(({ statusCode, body }) => { + if (statusCode != 200 || body.code != 0) return Promise.reject(new Error('请求失败')) + return body.data + }) + }, + handleResult(rawData) { + return rawData.map(info => `${info.name} - ${info.singer}`) + }, + cancelTipSearch() { + if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp() + }, + async search(str) { + return this.tipSearch(str).then(result => this.handleResult(result.song.itemlist)) + }, +} diff --git a/musicSdk/tx/utils/crypto.js b/musicSdk/tx/utils/crypto.js new file mode 100644 index 0000000..78618d1 --- /dev/null +++ b/musicSdk/tx/utils/crypto.js @@ -0,0 +1,28 @@ +import crypto from 'node:crypto' + +const PART_1_INDEXES = [23, 14, 6, 36, 16, 40, 7, 19] +const PART_2_INDEXES = [16, 1, 32, 12, 19, 27, 8, 5] +const SCRAMBLE_VALUES = [89, 39, 179, 150, 218, 82, 58, 252, 177, 52, 186, 123, 120, 64, 242, 133, 143, 161, 121, 179] + +async function hashSHA1(data) { + return crypto.createHash('sha1').update(data).digest('hex') +} + +function pickHashByIdx(hash, indexes) { + return indexes.map((idx) => hash[idx]).join('') +} + +function base64Encode(data) { + return Buffer.from(data) + .toString('base64') + .replace(/[\\/+=]/g, '') +} + +export async function zzcSign(text) { + const hash = await hashSHA1(text) + const part1 = pickHashByIdx(hash, PART_1_INDEXES) + const part2 = pickHashByIdx(hash, PART_2_INDEXES) + const part3 = SCRAMBLE_VALUES.map((value, i) => value ^ parseInt(hash.slice(i * 2, i * 2 + 2), 16)) + const b64Part = base64Encode(part3).replace(/[\\/+=]/g, '') + return `zzc${part1}${b64Part}${part2}`.toLowerCase() +} diff --git a/musicSdk/tx/utils/index.js b/musicSdk/tx/utils/index.js new file mode 100644 index 0000000..1d89814 --- /dev/null +++ b/musicSdk/tx/utils/index.js @@ -0,0 +1,16 @@ + +import { httpFetch } from '../../request.js' +import { zzcSign } from './crypto.js' + +export const signRequest = async(data) => { + // console.log(data) + const sign = await zzcSign(JSON.stringify(data)) + // console.log('sign', sign) + return httpFetch(`https://u.y.qq.com/cgi-bin/musics.fcg?sign=${sign}`, { + method: 'post', + headers: { + 'User-Agent': 'QQMusic 14090508(android 12)', + }, + body: data, + }).promise +} diff --git a/musicSdk/utils.js b/musicSdk/utils.js new file mode 100644 index 0000000..1d23179 --- /dev/null +++ b/musicSdk/utils.js @@ -0,0 +1,50 @@ +import crypto from 'crypto' +import dns from 'dns' +import { decodeName } from '../src/common/utils.js' + +export const toMD5 = str => crypto.createHash('md5').update(str).digest('hex') + + +const ipMap = new Map() +export const getHostIp = hostname => { + const result = ipMap.get(hostname) + if (typeof result === 'object') return result + if (result === true) return + ipMap.set(hostname, true) + // console.log(hostname) + dns.lookup(hostname, { + // family: 4, + all: false, + }, (err, address, family) => { + if (err) return console.log(err) + // console.log(address, family) + ipMap.set(hostname, { address, family }) + }) +} + +export const dnsLookup = (hostname, options, callback) => { + const result = getHostIp(hostname) + if (result) return callback(null, result.address, result.family) + + dns.lookup(hostname, options, callback) +} + + +/** + * 格式化歌手 + * @param singers 歌手数组 + * @param nameKey 歌手名键值 + * @param join 歌手分割字符 + */ +export const formatSingerName = (singers, nameKey = 'name', join = '、') => { + if (Array.isArray(singers)) { + const singer = [] + singers.forEach(item => { + let name = item[nameKey] + if (!name) return + singer.push(name) + }) + return decodeName(singer.join(join)) + } + return decodeName(String(singers ?? '')) +} diff --git a/musicSdk/wy/api-test.js b/musicSdk/wy/api-test.js new file mode 100644 index 0000000..1502cf6 --- /dev/null +++ b/musicSdk/wy/api-test.js @@ -0,0 +1,50 @@ +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/musicSdk/wy/comment.js b/musicSdk/wy/comment.js new file mode 100644 index 0000000..ecbabd5 --- /dev/null +++ b/musicSdk/wy/comment.js @@ -0,0 +1,208 @@ +import { httpFetch } from '../request.js' +import { weapi } from './utils/crypto.js' +import { dateFormat2 } from '../index.js' + +const emojis = [ + ['大笑', '😃'], + ['可爱', '😊'], + ['憨笑', '☺️'], + ['色', '😍'], + ['亲亲', '😙'], + ['惊恐', '😱'], + ['流泪', '😭'], + ['亲', '😚'], + ['呆', '😳'], + ['哀伤', '😔'], + ['呲牙', '😁'], + ['吐舌', '😝'], + ['撇嘴', '😒'], + ['怒', '😡'], + ['奸笑', '😏'], + ['汗', '😓'], + ['痛苦', '😖'], + ['惶恐', '😰'], + ['生病', '😨'], + ['口罩', '😷'], + ['大哭', '😂'], + ['晕', '😵'], + ['发怒', '👿'], + ['开心', '😄'], + ['鬼脸', '😜'], + ['皱眉', '😞'], + ['流感', '😢'], + ['爱心', '❤️'], + ['心碎', '💔'], + ['钟情', '💘'], + ['星星', '⭐️'], + ['生气', '💢'], + ['便便', '💩'], + ['强', '👍'], + ['弱', '👎'], + ['拜', '🙏'], + ['牵手', '👫'], + ['跳舞', '👯‍♀️'], + ['禁止', '🙅‍♀️'], + ['这边', '💁‍♀️'], + ['爱意', '💏'], + ['示爱', '👩‍❤️‍👨'], + ['嘴唇', '👄'], + ['狗', '🐶'], + ['猫', '🐱'], + ['猪', '🐷'], + ['兔子', '🐰'], + ['小鸡', '🐤'], + ['公鸡', '🐔'], + ['幽灵', '👻'], + ['圣诞', '🎅'], + ['外星', '👽'], + ['钻石', '💎'], + ['礼物', '🎁'], + ['男孩', '👦'], + ['女孩', '👧'], + ['蛋糕', '🎂'], + ['18', '🔞'], + ['圈', '⭕'], + ['叉', '❌'], +] + +const applyEmoji = text => { + for (const e of emojis) text = text.replaceAll(`[${e[0]}]`, e[1]) + return text +} + +let cursorTools = { + cache: {}, + getCursor(id, page, limit) { + let cacheData = this.cache[id] + if (!cacheData) cacheData = this.cache[id] = {} + let orderType + let cursor + let offset + if (page == 1) { + cacheData.page = 1 + cursor = cacheData.cursor = cacheData.prevCursor = Date.now() + orderType = 1 + offset = 0 + } else if (cacheData.page) { + cursor = cacheData.cursor + if (page > cacheData.page) { + orderType = 1 + offset = (page - cacheData.page - 1) * limit + } else if (page < cacheData.page) { + orderType = 0 + offset = (cacheData.page - page - 1) * limit + } else { + cursor = cacheData.cursor = cacheData.prevCursor + offset = cacheData.offset + orderType = cacheData.orderType + } + } + return { + orderType, + cursor, + offset, + } + }, + setCursor(id, cursor, orderType, offset, page) { + let cacheData = this.cache[id] + if (!cacheData) cacheData = this.cache[id] = {} + cacheData.prevCursor = cacheData.cursor + cacheData.cursor = cursor + cacheData.orderType = orderType + cacheData.offset = offset + cacheData.page = page + }, +} + +export default { + _requestObj: null, + _requestObj2: null, + async getComment({ songmid }, page = 1, limit = 20) { + if (this._requestObj) this._requestObj.cancelHttp() + + const id = 'R_SO_4_' + songmid + + const cursorInfo = cursorTools.getCursor(songmid, page, limit) + + const _requestObj = httpFetch('https://music.163.com/weapi/comment/resource/comments/get', { + method: 'post', + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36', + origin: 'https://music.163.com', + Refere: 'http://music.163.com/', + }, + form: weapi({ + cursor: cursorInfo.cursor, + offset: cursorInfo.offset, + orderType: cursorInfo.orderType, + pageNo: page, + pageSize: limit, + rid: id, + threadId: id, + }), + }) + const { body, statusCode } = await _requestObj.promise + // console.log(body) + if (statusCode != 200 || body.code !== 200) throw new Error('获取评论失败') + cursorTools.setCursor(songmid, body.data.cursor, cursorInfo.orderType, cursorInfo.offset, page) + return { source: 'wy', comments: this.filterComment(body.data.comments), total: body.data.totalCount, page, limit, maxPage: Math.ceil(body.data.totalCount / limit) || 1 } + }, + async getHotComment({ songmid }, page = 1, limit = 100) { + if (this._requestObj2) this._requestObj2.cancelHttp() + + const id = 'R_SO_4_' + songmid + page = page - 1 + + const _requestObj2 = httpFetch(`https://music.163.com/weapi/v1/resource/hotcomments/${id}`, { + method: 'post', + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36', + origin: 'https://music.163.com', + Refere: 'http://music.163.com/', + }, + form: weapi({ + rid: id, + limit, + offset: limit * page, + beforeTime: Date.now().toString(), + }), + }) + const { body, statusCode } = await _requestObj2.promise + if (statusCode != 200 || body.code !== 200) throw new Error('获取热门评论失败') + const total = body.total ?? 0 + return { source: 'wy', comments: this.filterComment(body.hotComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 } + }, + filterComment(rawList) { + return rawList.map(item => { + let data = { + id: item.commentId, + text: item.content ? applyEmoji(item.content) : '', + time: item.time ? item.time : '', + timeStr: item.time ? dateFormat2(item.time) : '', + location: item.ipLocation?.location, + userName: item.user.nickname, + avatar: item.user.avatarUrl, + userId: item.user.userId, + likedCount: item.likedCount, + reply: [], + } + + let replyData = item.beReplied && item.beReplied[0] + return replyData + ? { + id: item.commentId, + rootId: replyData.beRepliedCommentId, + text: replyData.content ? applyEmoji(replyData.content) : '', + time: item.time, + timeStr: null, + location: replyData.ipLocation?.location, + userName: replyData.user.nickname, + avatar: replyData.user.avatarUrl, + userId: replyData.user.userId, + likedCount: null, + reply: [data], + } + : data + }) + }, +} diff --git a/musicSdk/wy/hotSearch.js b/musicSdk/wy/hotSearch.js new file mode 100644 index 0000000..6507ac4 --- /dev/null +++ b/musicSdk/wy/hotSearch.js @@ -0,0 +1,20 @@ +import { eapiRequest } from './utils/index.js' + +export default { + _requestObj: null, + async getList(retryNum = 0) { + if (this._requestObj) this._requestObj.cancelHttp() + if (retryNum > 2) return Promise.reject(new Error('try max num')) + + const _requestObj = eapiRequest('/api/search/chart/detail', { + id: 'HOT_SEARCH_SONG#@#', + }) + const { body, statusCode } = await _requestObj.promise + if (statusCode != 200 || body.code !== 200) throw new Error('获取热搜词失败') + + return { source: 'wy', list: this.filterList(body.data.itemList) } + }, + filterList(rawList) { + return rawList.map(item => item.searchWord) + }, +} diff --git a/musicSdk/wy/index.js b/musicSdk/wy/index.js new file mode 100644 index 0000000..27a3580 --- /dev/null +++ b/musicSdk/wy/index.js @@ -0,0 +1,33 @@ +import leaderboard from './leaderboard.js' +import { apis } from '../api-source.js' +import getLyric from './lyric.js' +import getMusicInfo from './musicInfo.js' +import musicSearch from './musicSearch.js' +import songList from './songList.js' +import hotSearch from './hotSearch.js' +import comment from './comment.js' +// import tipSearch from './tipSearch.js' + +const wy = { + // tipSearch, + leaderboard, + musicSearch, + songList, + hotSearch, + comment, + getMusicUrl(songInfo, type) { + return apis('wy').getMusicUrl(songInfo, type) + }, + getLyric(songInfo) { + return getLyric(songInfo.songmid) + }, + getPic(songInfo) { + const requestObj = getMusicInfo(songInfo.songmid) + return requestObj.promise.then(info => info.al.picUrl) + }, + getMusicDetailPageUrl(songInfo) { + return `https://music.163.com/#/song?id=${songInfo.songmid}` + }, +} + +export default wy diff --git a/musicSdk/wy/leaderboard.js b/musicSdk/wy/leaderboard.js new file mode 100644 index 0000000..7f356f3 --- /dev/null +++ b/musicSdk/wy/leaderboard.js @@ -0,0 +1,208 @@ +import { weapi } from './utils/crypto.js' +import { httpFetch } from '../request.js' +import musicDetailApi from './musicDetail.js' + +const topList = [{ id: 'wy__19723756', name: '飙升榜', bangid: '19723756' }, + { id: 'wy__3779629', name: '新歌榜', bangid: '3779629' }, + { id: 'wy__2884035', name: '原创榜', bangid: '2884035' }, + { id: 'wy__3778678', name: '热歌榜', bangid: '3778678' }, + { id: 'wy__991319590', name: '说唱榜', bangid: '991319590' }, + { id: 'wy__71384707', name: '古典榜', bangid: '71384707' }, + { id: 'wy__1978921795', name: '电音榜', bangid: '1978921795' }, + { id: 'wy__5453912201', name: '黑胶VIP爱听榜', bangid: '5453912201' }, + { id: 'wy__71385702', name: 'ACG榜', bangid: '71385702' }, + { id: 'wy__745956260', name: '韩语榜', bangid: '745956260' }, + { id: 'wy__10520166', name: '国电榜', bangid: '10520166' }, + { id: 'wy__180106', name: 'UK排行榜周榜', bangid: '180106' }, + { id: 'wy__60198', name: '美国Billboard榜', bangid: '60198' }, + { id: 'wy__3812895', name: 'Beatport全球电子舞曲榜', bangid: '3812895' }, + { id: 'wy__21845217', name: 'KTV唛榜', bangid: '21845217' }, + { id: 'wy__60131', name: '日本Oricon榜', bangid: '60131' }, + { id: 'wy__2809513713', name: '欧美热歌榜', bangid: '2809513713' }, + { id: 'wy__2809577409', name: '欧美新歌榜', bangid: '2809577409' }, + { id: 'wy__27135204', name: '法国 NRJ Vos Hits 周榜', bangid: '27135204' }, + { id: 'wy__3001835560', name: 'ACG动画榜', bangid: '3001835560' }, + { id: 'wy__3001795926', name: 'ACG游戏榜', bangid: '3001795926' }, + { id: 'wy__3001890046', name: 'ACG VOCALOID榜', bangid: '3001890046' }, + { id: 'wy__3112516681', name: '中国新乡村音乐排行榜', bangid: '3112516681' }, + { id: 'wy__5059644681', name: '日语榜', bangid: '5059644681' }, + { id: 'wy__5059633707', name: '摇滚榜', bangid: '5059633707' }, + { id: 'wy__5059642708', name: '国风榜', bangid: '5059642708' }, + { id: 'wy__5338990334', name: '潜力爆款榜', bangid: '5338990334' }, + { id: 'wy__5059661515', name: '民谣榜', bangid: '5059661515' }, + { id: 'wy__6688069460', name: '听歌识曲榜', bangid: '6688069460' }, + { id: 'wy__6723173524', name: '网络热歌榜', bangid: '6723173524' }, + { id: 'wy__6732051320', name: '俄语榜', bangid: '6732051320' }, + { id: 'wy__6732014811', name: '越南语榜', bangid: '6732014811' }, + { id: 'wy__6886768100', name: '中文DJ榜', bangid: '6886768100' }, + { id: 'wy__6939992364', name: '俄罗斯top hit流行音乐榜', bangid: '6939992364' }, + { id: 'wy__7095271308', name: '泰语榜', bangid: '7095271308' }, + { id: 'wy__7356827205', name: 'BEAT排行榜', bangid: '7356827205' }, + { id: 'wy__7325478166', name: '编辑推荐榜VOL.44 天才女子摇滚乐队boygenius剖白卑微心迹', bangid: '7325478166' }, + { id: 'wy__7603212484', name: 'LOOK直播歌曲榜', bangid: '7603212484' }, + { id: 'wy__7775163417', name: '赏音榜', bangid: '7775163417' }, + { id: 'wy__7785123708', name: '黑胶VIP新歌榜', bangid: '7785123708' }, + { id: 'wy__7785066739', name: '黑胶VIP热歌榜', bangid: '7785066739' }, + { id: 'wy__7785091694', name: '黑胶VIP爱搜榜', bangid: '7785091694' }, +] + +export default { + limit: 100000, + list: [ + { + id: 'wybsb', + name: '飙升榜', + bangid: '19723756', + }, + { + id: 'wyrgb', + name: '热歌榜', + bangid: '3778678', + }, + { + id: 'wyxgb', + name: '新歌榜', + bangid: '3779629', + }, + { + id: 'wyycb', + name: '原创榜', + bangid: '2884035', + }, + { + id: 'wygdb', + name: '古典榜', + bangid: '71384707', + }, + { + id: 'wydouyb', + name: '抖音榜', + bangid: '2250011882', + }, + { + id: 'wyhyb', + name: '韩语榜', + bangid: '745956260', + }, + { + id: 'wydianyb', + name: '电音榜', + bangid: '1978921795', + }, + { + id: 'wydjb', + name: '电竞榜', + bangid: '2006508653', + }, + { + id: 'wyktvbb', + name: 'KTV唛榜', + bangid: '21845217', + }, + ], + getUrl(id) { + return `https://music.163.com/discover/toplist?id=${id}` + }, + regExps: { + list: /