存档:TS 重构前基线(lx 聚合源 = isolated-vm 加载器,签名方案已移除)

This commit is contained in:
archive committed 2026-08-28 23:24:49 +08:00
commit f289df4cd7
124 files changed
+14110

No files matched your search

+10
View File
@@ -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 '\\\\.\\\\./\\\\.\\\\./\")"
]
}
}
+28
View File
@@ -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/
+77
View File
@@ -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/` 内的接口文档调整。
+54
View File
@@ -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 哈希),需字节码级反编译
+13
View File
@@ -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 ﱡء()<=ﱡﹴ()}}(ﱡLine truncated
+40
View File
@@ -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
+32
View File
@@ -0,0 +1,32 @@
// Support qualitys: 128k 320k flac wav
const sources: Array<{
id: string
name: string
disabled: boolean
supportQualitys: Partial<Record<LX.OnlineSource, LX.Quality[]>>
}> = [
// {
// 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
+53
View File
@@ -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 }
+26
View File
@@ -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
+23
View File
@@ -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)
},
}
+40
View File
@@ -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
+153
View File
@@ -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(/&quot;/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(/&quot;/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',
}
})
},
}
+20
View File
@@ -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
},
}
+88
View File
@@ -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',
})
})
},
}
+263
View File
@@ -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()}&timestamp=${data.timestamp}&param=${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
+180
View File
@@ -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,
}
+63
View File
@@ -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),
},
}
},
}
+50
View File
@@ -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
+101
View File
@@ -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
})
},
}
+33
View File
@@ -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
},
}
+38
View File
@@ -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
+208
View File
@@ -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`
},
}
+108
View File
@@ -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
},
}
+116
View File
@@ -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 })))
}
+108
View File
@@ -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',
})
})
},
}
+48
View File
@@ -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
})
},
}
+85
View File
@@ -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,
},
}
})
},
}
+986
View File
@@ -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 = '<div class="pc_specail_text pc_singer_tab_content" id="specailIntroduceWrap">'
let index = html.indexOf(prefix)
if (index < 0) return null
const afterStr = html.substring(index + prefix.length)
index = afterStr.indexOf('</div>')
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
+112
View File
@@ -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',
})
})
},
}
+794
View File
@@ -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 = '<div class="pc_specail_text pc_singer_tab_content" id="specailIntroduceWrap">'
let index = html.indexOf(prefix)
if (index < 0) return null
const afterStr = html.substring(index + prefix.length)
index = afterStr.indexOf('</div>')
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
+25
View File
@@ -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))
},
}
+47
View File
@@ -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
}
+2
View File
@@ -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;e<arguments.length;e++){var o=null!=arguments[e]?arguments[e]:{};e%2?n(o,!0).forEach(function(n){t(r,n,o[n])}):Object.getOwnPropertyDescriptors?Object.defineProperties(r,Object.getOwnPropertyDescriptors(o)):n(o).forEach(function(t){Object.defineProperty(r,t,Object.getOwnPropertyDescriptor(o,t))})}return r}function e(t,n){return n={exports:{}},t(n,n.exports),n.exports}function o(t){return!!t.constructor&&"function"==typeof t.constructor.isBuffer&&t.constructor.isBuffer(t)}function i(t){return"function"==typeof t.readFloatLE&&"function"==typeof t.slice&&o(t.slice(0,0))}function c(){var t,n=arguments.length>0&&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.toStrLine truncated
+131
View File
@@ -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),
// // },
// // }
// })
// })
// },
}
+26
View File
@@ -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
+37
View File
@@ -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
+82
View File
@@ -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] : [],
}
})
: [],
}
})
},
}
+22
View File
@@ -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&notrace=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)
},
}
+102
View File
@@ -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
+229
View File
@@ -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}`
// },
}
+270
View File
@@ -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
},
}
+126
View File
@@ -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',
})
})
},
}
+9
View File
@@ -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
},
}
+488
View File
@@ -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
+32
View File
@@ -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&notrace=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))
},
}
+218
View File
@@ -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}`
},
}
+4
View File
@@ -0,0 +1,4 @@
// SDK 根消息模块(转接至 src/common/message.js)
// musicSdk 内 `import { requestMsg } from '../message.js'` 的解析目标
export { requestMsg } from '../src/common/message.js'
+51
View File
@@ -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,
}
},
}
+26
View File
@@ -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
+96
View File
@@ -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,
})),
}))
},
}
+18
View File
@@ -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)
},
}
+32
View File
@@ -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
+243
View File
@@ -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
},
}
+104
View File
@@ -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
},
}
+162
View File
@@ -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())
}
+223
View File
@@ -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',
}
})
},
}
+28
View File
@@ -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)
},
}
+31
View File
@@ -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
+362
View File
@@ -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', t Line truncated
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><div class="thumb">.+?<\/li>/g,
listInfo: /.+data-original="(.+?)".*data-id="(\d+)".*<div class="song-list-name"><a\s.*?>(.+?)<\/a>.+<i class="iconfont cf-bofangliang"><\/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
+195
View File
@@ -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',
}
})
},
}
+25
View File
@@ -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))
},
}
+29
View File
@@ -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
}
+104
View File
@@ -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))
+9
View File
@@ -0,0 +1,9 @@
export const bHh = '624868746c'
export const headers = {
'User-Agent': 'lx-music request',
[bHh]: [bHh],
}
export const timeout = 15000
+4
View File
@@ -0,0 +1,4 @@
// SDK 根请求模块(转接至 src/common/request.js)
// musicSdk 内 `import { httpFetch } from '../request.js'` 的解析目标
export { httpFetch } from '../src/common/request.js'
+29
View File
@@ -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
+282
View File
@@ -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')
},
}
+54
View File
@@ -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)
},
}
+33
View File
@@ -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
+247
View File
@@ -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: /<i class="play_cover__btn c_tx_link js_icon_play" data-listkey=".+?" data-listname=".+?" data-tid=".+?" data-date=".+?" .+?<\/i>/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&notice=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}`
},
}
+294
View File
@@ -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
// },
// }
+98
View File
@@ -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: {},
}
})
}
+168
View File
@@ -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',
})
})
},
}
+249
View File
@@ -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)
})
},
}
+328
View File
@@ -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&notice=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&notice=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&notice=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&notice=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(/<br>/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(/<br>/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(/<br>/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(/<br>/g, '\n'),
source: 'tx',
}
}),
limit,
total: body.data.sum,
source: 'tx',
}
})
},
}
// getList
// getTags
// getListDetail
+29
View File
@@ -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&notice=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))
},
}
+28
View File
@@ -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()
}
+16
View File
@@ -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
}
+50
View File
@@ -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 ?? ''))
}
+50
View File
@@ -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
+208
View File
@@ -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
})
},
}
+20
View File
@@ -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)
},
}
+33
View File
@@ -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
+208
View File
@@ -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: /<textarea id="song-list-pre-data" style="display:none;">(.+?)<\/textarea>/,
},
_requestBoardsObj: null,
getBoardsData() {
if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp()
this._requestBoardsObj = httpFetch('https://music.163.com/weapi/toplist', {
method: 'post',
form: weapi({}),
})
return this._requestBoardsObj.promise
},
getData(id) {
const requestBoardsDetailObj = httpFetch('https://music.163.com/weapi/v3/playlist/detail', {
method: 'post',
form: weapi({
id,
n: 100000,
p: 1,
}),
})
return requestBoardsDetailObj.promise
},
filterBoardsData(rawList) {
// console.log(rawList)
let list = []
for (const board of rawList) {
// 排除 MV榜
// if (board.id == 201) continue
list.push({
id: 'wy__' + board.id,
name: board.name,
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 !== 200) return this.getBoards(retryNum)
// const list = this.filterBoardsData(response.body.list)
// console.log(list)
// console.log(JSON.stringify(list))
// this.list = list
// return {
// list,
// source: 'wy',
// }
this.list = topList
return {
list: topList,
source: 'wy',
}
},
async getList(bangid, page, retryNum = 0) {
if (++retryNum > 6) return Promise.reject(new Error('try max num'))
// console.log(bangid)
let resp
try {
resp = await this.getData(bangid)
} catch (err) {
if (err.message == 'try max num') {
throw err
} else {
return this.getList(bangid, page, retryNum)
}
}
if (resp.statusCode !== 200 || resp.body.code !== 200) return this.getList(bangid, page, retryNum)
// console.log(resp.body)
let musicDetail
try {
musicDetail = await musicDetailApi.getList(resp.body.playlist.trackIds.map(trackId => trackId.id))
} catch (err) {
console.log(err)
if (err.message == 'try max num') {
throw err
} else {
return this.getList(bangid, page, retryNum)
}
}
// console.log(musicDetail)
return {
total: musicDetail.list.length,
list: musicDetail.list,
limit: this.limit,
page,
source: 'wy',
}
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('wy__', '')
return `https://music.163.com/#/discover/toplist?id=${id}`
},
}
+288
View File
@@ -0,0 +1,288 @@
import { httpFetch } from '../request.js'
import { eapi } from './utils/crypto.js'
// import { decodeName } from '../...js'
// const parseLyric = (str, lrc) => {
// if (!str) return ''
// str = str.replace(/\r/g, '')
// let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
// let result = str.match(/\[((\d+),\d+)\].*/)
// let time = parseInt(result[2])
// 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}`
// str = str.replace(result[1], time)
// let startTime = 0
// str = str.replace(/\(0,1\) /g, ' ').replace(/\(\d+,\d+\)/g, time => {
// const [start, end] = time.replace(/^\((\d+,\d+)\)$/, '$1').split(',')
// time = `<${parseInt(startTime + parseInt(start))},${end}>`
// startTime = parseInt(startTime + parseInt(end))
// return time
// })
// return str
// })
// lxlyric = decodeName(lxlyric)
// return lxlyric.trim()
// }
const eapiRequest = (url, data) => {
return httpFetch('https://interface3.music.163.com/eapi/song/lyric/v1', {
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',
// cookie: 'os=pc; deviceId=A9C064BB4584D038B1565B58CB05F95290998EE8B025AA2D07AE; osver=Microsoft-Windows-10-Home-China-build-19043-64bit; appver=2.5.2.197409; channel=netease; MUSIC_A=37a11f2eb9de9930cad479b2ad495b0e4c982367fb6f909d9a3f18f876c6b49faddb3081250c4980dd7e19d4bd9bf384e004602712cf2b2b8efaafaab164268a00b47359f85f22705cc95cb6180f3aee40f5be1ebf3148d888aa2d90636647d0c3061cd18d77b7a0; __csrf=05b50d54082694f945d7de75c210ef94; mode=Z7M-KP5(7)GZ; NMTID=00OZLp2VVgq9QdwokUgq3XNfOddQyIAAAF_6i8eJg; ntes_kaola_ad=1',
},
form: eapi(url, data),
})
// requestObj.promise = requestObj.promise.then(({ body }) => {
// // console.log(raw)
// console.log(body)
// // console.log(eapiDecrypt(raw))
// // return eapiDecrypt(raw)
// return body
// })
// return requestObj
}
const parseTools = {
rxps: {
info: /^{"/,
lineTime: /^\[(\d+),\d+\]/,
wordTime: /\(\d+,\d+,\d+\)/,
wordTimeAll: /(\(\d+,\d+,\d+\))/g,
},
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}.${ms}]`
},
parseLyric(lines) {
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)
}
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+),\d+\)/.exec(time)
return `<${Math.max(parseInt(result[1]) - startMsTime, 0)},${result[2]}>`
})
const wordArr = words.split(this.rxps.wordTime)
wordArr.shift()
const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('')
lxlrcLines.push(`${startTimeStr}${newWords}`)
}
return {
lyric: lrcLines.join('\n'),
lxlyric: lxlrcLines.join('\n'),
}
},
parseHeaderInfo(str) {
str = str.trim()
str = str.replace(/\r/g, '')
if (!str) return null
const lines = str.split('\n')
return lines.map(line => {
if (!this.rxps.info.test(line)) return line
try {
const info = JSON.parse(line)
const timeTag = this.msFormat(info.t)
return timeTag ? `${timeTag}${info.c.map(t => t.tx).join('')}` : ''
} catch {
return ''
}
})
},
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)
},
fixTimeTag(lrc, targetlrc) {
let lrcLines = lrc.split('\n')
const targetlrcLines = targetlrc.split('\n')
const timeRxp = /^\[([\d:.]+)\]/
let temp = []
let newLrc = []
targetlrcLines.forEach((line) => {
const result = timeRxp.exec(line)
if (!result) return
const words = line.replace(timeRxp, '')
if (!words.trim()) return
const t1 = this.getIntv(result[1])
while (lrcLines.length) {
const lrcLine = lrcLines.shift()
const lrcLineResult = timeRxp.exec(lrcLine)
if (!lrcLineResult) continue
const t2 = this.getIntv(lrcLineResult[1])
if (Math.abs(t1 - t2) < 100) {
const lrc = line.replace(timeRxp, lrcLineResult[0]).trim()
if (!lrc) continue
newLrc.push(lrc)
break
}
temp.push(lrcLine)
}
lrcLines = [...temp, ...lrcLines]
temp = []
})
return newLrc.join('\n')
},
parse(ylrc, ytlrc, yrlrc, lrc, tlrc, rlrc) {
const info = {
lyric: '',
tlyric: '',
rlyric: '',
lxlyric: '',
}
if (ylrc) {
let lines = this.parseHeaderInfo(ylrc)
if (lines) {
const result = this.parseLyric(lines)
if (ytlrc) {
const lines = this.parseHeaderInfo(ytlrc)
if (lines) {
// if (lines.length == result.lyricLines.length) {
info.tlyric = this.fixTimeTag(result.lyric, lines.join('\n'))
// } else info.tlyric = lines.join('\n')
}
}
if (yrlrc) {
const lines = this.parseHeaderInfo(yrlrc)
if (lines) {
// if (lines.length == result.lyricLines.length) {
info.rlyric = this.fixTimeTag(result.lyric, lines.join('\n'))
// } else info.rlyric = lines.join('\n')
}
}
const timeRxp = /^\[[\d:.]+\]/
const headers = lines.filter(l => timeRxp.test(l)).join('\n')
info.lyric = `${headers}\n${result.lyric}`
info.lxlyric = result.lxlyric
return info
}
}
if (lrc) {
const lines = this.parseHeaderInfo(lrc)
if (lines) info.lyric = lines.join('\n')
}
if (tlrc) {
const lines = this.parseHeaderInfo(tlrc)
if (lines) info.tlyric = lines.join('\n')
}
if (rlrc) {
const lines = this.parseHeaderInfo(rlrc)
if (lines) info.rlyric = lines.join('\n')
}
return info
},
}
// https://github.com/Binaryify/NeteaseCloudMusicApi/pull/1523/files
// export default songmid => {
// const requestObj = httpFetch('https://music.163.com/api/linux/forward', {
// method: 'post',
// 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
// form: linuxapi({
// method: 'POST',
// url: 'https://music.163.com/api/song/lyric?_nmclfl=1',
// params: {
// id: songmid,
// tv: -1,
// lv: -1,
// rv: -1,
// kv: -1,
// },
// }),
// })
// requestObj.promise = requestObj.promise.then(({ body }) => {
// if (body.code !== 200 || !body?.lrc?.lyric) return Promise.reject(new Error('Get lyric failed'))
// // console.log(body)
// return {
// lyric: body.lrc.lyric,
// tlyric: body.tlyric?.lyric ?? '',
// rlyric: body.romalrc?.lyric ?? '',
// // lxlyric: parseLyric(body.klyric.lyric),
// }
// })
// return requestObj
// }
// https://github.com/lyswhut/lx-music-mobile/issues/370
const fixTimeLabel = (lrc, tlrc, romalrc) => {
if (lrc) {
let newLrc = lrc.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]')
let newTlrc = tlrc?.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]') ?? tlrc
if (newLrc != lrc || newTlrc != tlrc) {
lrc = newLrc
tlrc = newTlrc
if (romalrc) romalrc = romalrc.replace(/\[(\d{2}:\d{2}):(\d{2,3})]/g, '[$1.$2]').replace(/\[(\d{2}:\d{2}\.\d{2})0]/g, '[$1]')
}
}
return { lrc, tlrc, romalrc }
}
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/lyric_new.js
export default songmid => {
const requestObj = eapiRequest('/api/song/lyric/v1', {
id: songmid,
cp: false,
tv: 0,
lv: 0,
rv: 0,
kv: 0,
yv: 0,
ytv: 0,
yrv: 0,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
// console.log(body)
if (body.code !== 200 || !body?.lrc?.lyric) return Promise.reject(new Error('Get lyric failed'))
const fixTimeLabelLrc = fixTimeLabel(body.lrc.lyric, body.tlyric?.lyric, body.romalrc?.lyric)
const info = parseTools.parse(body.yrc?.lyric, body.ytlrc?.lyric, body.yromalrc?.lyric, fixTimeLabelLrc.lrc, fixTimeLabelLrc.tlrc, fixTimeLabelLrc.romalrc)
// console.log(info)
if (!info.lyric) return Promise.reject(new Error('Get lyric failed'))
return info
})
return requestObj
}
+112
View File
@@ -0,0 +1,112 @@
import { httpFetch } from '../request.js'
import { weapi } from './utils/crypto.js'
import { formatPlayTime, sizeFormate } from '../index.js'
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/song_detail.js
export default {
getSinger(singers) {
let arr = []
singers?.forEach(singer => {
arr.push(singer.name)
})
return arr.join('、')
},
filterList({ songs, privileges }) {
// console.log(songs, privileges)
const list = []
songs.forEach((item, index) => {
const types = []
const _types = {}
let size
let privilege = privileges[index]
if (privilege.id !== item.id) privilege = privileges.find(p => p.id === item.id)
if (!privilege) return
if (privilege.maxBrLevel == 'hires') {
size = item.hr ? sizeFormate(item.hr.size) : null
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
}
switch (privilege.maxbr) {
case 999000:
size = item.sq ? sizeFormate(item.sq.size) : null
types.push({ type: 'flac', size })
_types.flac = {
size,
}
case 320000:
size = item.h ? sizeFormate(item.h.size) : null
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
case 192000:
case 128000:
size = item.l ? sizeFormate(item.l.size) : null
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
}
types.reverse()
if (item.pc) {
list.push({
singer: item.pc.ar ?? '',
name: item.pc.sn ?? '',
albumName: item.pc.alb ?? '',
albumId: item.al?.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al?.picUrl ?? '',
lrc: null,
otherSource: null,
types,
_types,
typeUrl: {},
})
} else {
list.push({
singer: this.getSinger(item.ar),
name: item.name ?? '',
albumName: item.al?.name,
albumId: item.al?.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al?.picUrl,
lrc: null,
otherSource: null,
types,
_types,
typeUrl: {},
})
}
})
// console.log(list)
return list
},
async getList(ids = [], retryNum = 0) {
if (retryNum > 2) return Promise.reject(new Error('try max num'))
const requestObj = httpFetch('https://music.163.com/weapi/v3/song/detail', {
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',
},
form: weapi({
c: '[' + ids.map(id => ('{"id":' + id + '}')).join(',') + ']',
ids: '[' + ids.join(',') + ']',
}),
})
const { body, statusCode } = await requestObj.promise
if (statusCode != 200 || body.code !== 200) throw new Error('获取歌曲详情失败')
// console.log(body)
return { source: 'wy', list: this.filterList(body) }
},
}
+25
View File
@@ -0,0 +1,25 @@
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/song_detail.js
import { httpFetch } from '../request.js'
import { weapi } from './utils/crypto.js'
export default songmid => {
const requestObj = httpFetch('https://music.163.com/weapi/v3/song/detail', {
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',
Referer: 'https://music.163.com/song?id=' + songmid,
origin: 'https://music.163.com',
},
form: weapi({
c: `[{"id":${songmid}}]`,
ids: `[${songmid}]`,
}),
})
requestObj.promise = requestObj.promise.then(({ body }) => {
// console.log(body)
if (body.code !== 200 || !body.songs.length) return Promise.reject(new Error('获取歌曲信息失败'))
return body.songs[0]
})
return requestObj
}
+119
View File
@@ -0,0 +1,119 @@
// import { httpFetch } from '../request.js'
// import { weapi } from './utils/crypto.js'
import { sizeFormate, formatPlayTime } from '../index.js'
// import musicDetailApi from './musicDetail.js'
import { eapiRequest } from './utils/index.js'
export default {
limit: 30,
total: 0,
page: 0,
allPage: 1,
musicSearch(str, page, limit) {
// const searchRequest = eapiRequest('/api/cloudsearch/pc', {
// s: str,
// type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
// limit,
// total: page == 1,
// offset: limit * (page - 1),
// })
const searchRequest = eapiRequest('/api/search/song/list/page', {
keyword: str,
needCorrect: '1',
channel: 'typing',
offset: limit * (page - 1),
scene: 'normal',
total: page == 1,
limit,
})
return searchRequest.promise.then(({ body }) => body)
},
getSinger(singers) {
let arr = []
singers.forEach(singer => {
arr.push(singer.name)
})
return arr.join('、')
},
handleResult(rawList) {
// console.log(rawList)
if (!rawList) return []
return rawList.map(item => {
item = item.baseInfo.simpleSongData
const types = []
const _types = {}
let size
if (item.privilege.maxBrLevel == 'hires') {
size = item.hr ? sizeFormate(item.hr.size) : null
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
}
switch (item.privilege.maxbr) {
case 999000:
size = item.sq ? sizeFormate(item.sq.size) : null
types.push({ type: 'flac', size })
_types.flac = {
size,
}
case 320000:
size = item.h ? sizeFormate(item.h.size) : null
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
case 192000:
case 128000:
size = item.l ? sizeFormate(item.l.size) : null
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
}
types.reverse()
return {
singer: this.getSinger(item.ar),
name: item.name,
albumName: item.al.name,
albumId: item.al.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al.picUrl,
lrc: null,
types,
_types,
typeUrl: {},
}
})
},
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 => {
// console.log(result)
if (!result || result.code !== 200) return this.search(str, page, limit, retryNum)
let list = this.handleResult(result.data.resources || [])
// console.log(list)
if (list == null) return this.search(str, page, limit, retryNum)
this.total = result.data.totalCount || 0
this.page = page
this.allPage = Math.ceil(this.total / this.limit)
return {
list,
allPage: this.allPage,
limit: this.limit,
total: this.total,
source: 'wy',
}
// return result.data
})
},
}
+150
View File
@@ -0,0 +1,150 @@
import { eapiRequest } from './utils/index.js'
import { formatPlayTime, sizeFormate } from '../index.js'
import { formatSingerName } from '../utils.js'
export default {
/**
* 获取歌手信息
* @param {*} id
*/
getInfo(id) {
return eapiRequest('/api/artist/head/info/get', { id }).then(({ body }) => {
if (!body || body.code != 200) throw new Error('get singer info faild.')
return {
source: 'wy',
id: body.artist.id,
info: {
name: body.artist.name,
desc: body.artist.briefDesc,
avatar: body.user.avatarUrl,
gender: body.user.gender === 1 ? 'man' : 'woman',
},
count: {
music: body.artist.musicSize,
album: body.artist.albumSize,
},
}
})
},
/**
* 获取歌手歌曲列表
* @param {*} id
* @param {*} page
* @param {*} limit
*/
getSongList(id, page = 1, limit = 100) {
if (page === 1) page = 0
return eapiRequest('/api/v2/artist/songs', {
id,
limit,
offset: limit * page,
}).then(({ body }) => {
if (!body.songs || body.code != 200) throw new Error('get singer song list faild.')
const list = this.filterSongList(body.songs)
return {
list,
limit,
page,
total: body.total,
source: 'wy',
}
})
},
/**
* 获取歌手专辑列表
* @param {*} id
* @param {*} page
* @param {*} limit
*/
getAlbumList(id, page = 1, limit = 10) {
if (page === 1) page = 0
return eapiRequest(`/api/artist/albums/${id}`, {
limit,
offset: limit * page,
}).then(({ body }) => {
if (!body.hotAlbums || body.code != 200) throw new Error('get singer album list faild.')
const list = this.filterAlbumList(body.hotAlbums)
return {
source: 'wy',
list,
limit,
page,
total: body.artist.albumSize,
}
})
},
filterAlbumList(raw) {
const list = []
raw.forEach(item => {
if (!item.id) return
list.push({
id: item.id,
count: item.size,
info: {
name: item.name,
author: formatSingerName(item.artists),
img: item.picUrl,
desc: null,
},
})
})
return list
},
filterSongList(raw) {
const list = []
raw.forEach(item => {
if (!item.id) return
const types = []
const _types = {}
let size
item.privilege.chargeInfoList.forEach(i => {
switch (i.rate) {
case 128000:
size = item.lMusic ? sizeFormate(item.lMusic.size) : null
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
case 320000:
size = item.hMusic ? sizeFormate(item.hMusic.size) : null
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
case 999000:
size = item.sqMusic ? sizeFormate(item.sqMusic.size) : null
types.push({ type: 'flac', size })
_types.flac = {
size,
}
case 1999000:
size = item.hrMusic ? sizeFormate(item.hrMusic.size) : null
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
}
})
list.push({
singer: formatSingerName(item.artists),
name: item.name,
albumName: item.album.name,
albumId: item.album.id,
songmid: item.id,
source: 'wy',
interval: formatPlayTime(item.duration),
img: null,
lrc: null,
otherSource: null,
types,
_types,
typeUrl: {},
})
})
return list
},
}
+339
View File
@@ -0,0 +1,339 @@
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/playlist_catlist.js
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/playlist_hot.js
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/top_playlist.js
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/playlist_detail.js
import { weapi, linuxapi } from './utils/crypto.js'
import { httpFetch } from '../request.js'
import { formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../index.js'
import musicDetailApi from './musicDetail.js'
import { eapiRequest } from './utils/index.js'
import { formatSingerName } from '../utils.js'
export default {
_requestObj_tags: null,
_requestObj_hotTags: null,
_requestObj_list: null,
limit_list: 30,
limit_song: 100000,
successCode: 200,
cookie: 'MUSIC_U=',
sortList: [
{
name: '最热',
id: 'hot',
},
// {
// name: '最新',
// id: 'new',
// },
],
regExps: {
listDetailLink: /^.+(?:\?|&)id=(\d+)(?:&.*$|#.*$|$)/,
listDetailLink2: /^.+\/playlist\/(\d+)\/\d+\/.+$/,
},
async handleParseId(link, retryNum = 0) {
if (retryNum > 2) throw 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)
const url = location == null ? link : location
return this.regExps.listDetailLink.test(url)
? url.replace(this.regExps.listDetailLink, '$1')
: url.replace(this.regExps.listDetailLink2, '$1')
},
async getListId(id) {
let cookie
if (/###/.test(id)) {
const [url, token] = id.split('###')
id = url
cookie = `MUSIC_U=${token}`
}
if ((/[?&:/]/.test(id))) {
if (this.regExps.listDetailLink.test(id)) {
id = id.replace(this.regExps.listDetailLink, '$1')
} else if (this.regExps.listDetailLink2.test(id)) {
id = id.replace(this.regExps.listDetailLink2, '$1')
} else {
id = await this.handleParseId(id)
}
// console.log(id)
}
return { id, cookie }
},
async getListDetail(rawId, page, tryNum = 0) { // 获取歌曲列表内的音乐
if (tryNum > 2) return Promise.reject(new Error('try max num'))
const { id, cookie } = await this.getListId(rawId)
if (cookie) this.cookie = cookie
const requestObj_listDetail = httpFetch('https://music.163.com/api/linux/forward', {
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',
Cookie: this.cookie,
},
form: linuxapi({
method: 'POST',
url: 'https://music.163.com/api/v3/playlist/detail',
params: {
id,
n: this.limit_song,
s: 8,
},
}),
})
const { statusCode, body } = await requestObj_listDetail.promise
if (statusCode !== 200 || body.code !== this.successCode) return this.getListDetail(id, page, ++tryNum)
let limit = 1000
let rangeStart = (page - 1) * limit
// console.log(body)
let list
if (body.playlist.trackIds.length == body.privileges.length) {
list = this.filterListDetail(body)
} else {
try {
list = (await musicDetailApi.getList(body.playlist.trackIds.slice(rangeStart, limit * page).map(trackId => trackId.id))).list
} catch (err) {
console.log(err)
if (err.message == 'try max num') {
throw err
} else {
return this.getListDetail(id, page, ++tryNum)
}
}
}
// console.log(list)
return {
list,
page,
limit,
total: body.playlist.trackIds.length,
source: 'wy',
info: {
play_count: formatPlayCount(body.playlist.playCount),
name: body.playlist.name,
img: body.playlist.coverImgUrl,
desc: body.playlist.description,
author: body.playlist.creator.nickname,
},
}
},
filterListDetail({ playlist: { tracks }, privileges }) {
// console.log(tracks, privileges)
const list = []
tracks.forEach((item, index) => {
const types = []
const _types = {}
let size
let privilege = privileges[index]
if (privilege.id !== item.id) privilege = privileges.find(p => p.id === item.id)
if (!privilege) return
if (privilege.maxBrLevel == 'hires') {
size = item.hr ? sizeFormate(item.hr.size) : null
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
}
switch (privilege.maxbr) {
case 999000:
size = null
types.push({ type: 'flac', size })
_types.flac = {
size,
}
case 320000:
size = item.h ? sizeFormate(item.h.size) : null
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
case 192000:
case 128000:
size = item.l ? sizeFormate(item.l.size) : null
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
}
types.reverse()
if (item.pc) {
list.push({
singer: item.pc.ar ?? '',
name: item.pc.sn ?? '',
albumName: item.pc.alb ?? '',
albumId: item.al?.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al?.picUrl ?? '',
lrc: null,
otherSource: null,
types,
_types,
typeUrl: {},
})
} else {
list.push({
singer: formatSingerName(item.ar, 'name'),
name: item.name ?? '',
albumName: item.al?.name,
albumId: item.al?.id,
source: 'wy',
interval: formatPlayTime(item.dt / 1000),
songmid: item.id,
img: item.al?.picUrl,
lrc: null,
otherSource: null,
types,
_types,
typeUrl: {},
})
}
})
return list
},
// 获取列表数据
getList(sortId, tagId, page, tryNum = 0) {
if (tryNum > 2) return Promise.reject(new Error('try max num'))
if (this._requestObj_list) this._requestObj_list.cancelHttp()
this._requestObj_list = httpFetch('https://music.163.com/weapi/playlist/list', {
method: 'post',
form: weapi({
cat: tagId || '全部', // 全部,华语,欧美,日语,韩语,粤语,小语种,流行,摇滚,民谣,电子,舞曲,说唱,轻音乐,爵士,乡村,R&B/Soul,古典,民族,英伦,金属,朋克,蓝调,雷鬼,世界音乐,拉丁,另类/独立,New Age,古风,后摇,Bossa Nova,清晨,夜晚,学习,工作,午休,下午茶,地铁,驾车,运动,旅行,散步,酒吧,怀旧,清新,浪漫,性感,伤感,治愈,放松,孤独,感动,兴奋,快乐,安静,思念,影视原声,ACG,儿童,校园,游戏,70后,80后,90后,网络歌曲,KTV,经典,翻唱,吉他,钢琴,器乐,榜单,00后
order: sortId, // hot,new
limit: this.limit_list,
offset: this.limit_list * (page - 1),
total: true,
}),
})
return this._requestObj_list.promise.then(({ body }) => {
// console.log(body)
if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)
return {
list: this.filterList(body.playlists),
total: parseInt(body.total),
page,
limit: this.limit_list,
source: 'wy',
}
})
},
filterList(rawData) {
// console.log(rawData)
return rawData.map(item => ({
play_count: formatPlayCount(item.playCount),
id: String(item.id),
author: item.creator.nickname,
name: item.name,
time: item.createTime ? dateFormat(item.createTime, 'Y-M-D') : '',
img: item.coverImgUrl,
grade: item.grade,
total: item.trackCount,
desc: item.description,
source: 'wy',
}))
},
// 获取标签
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('https://music.163.com/weapi/playlist/catalogue', {
method: 'post',
form: weapi({}),
})
return this._requestObj_tags.promise.then(({ body }) => {
// console.log(JSON.stringify(body))
if (body.code !== this.successCode) return this.getTag(++tryNum)
return this.filterTagInfo(body)
})
},
filterTagInfo({ sub, categories }) {
const subList = {}
for (const item of sub) {
if (!subList[item.category]) subList[item.category] = []
subList[item.category].push({
parent_id: categories[item.category],
parent_name: categories[item.category],
id: item.name,
name: item.name,
source: 'wy',
})
}
const list = []
for (const key of Object.keys(categories)) {
list.push({
name: categories[key],
list: subList[key],
source: 'wy',
})
}
return list
},
// 获取热门标签
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('https://music.163.com/weapi/playlist/hottags', {
method: 'post',
form: weapi({}),
})
return this._requestObj_hotTags.promise.then(({ body }) => {
// console.log(JSON.stringify(body))
if (body.code !== this.successCode) return this.getTag(++tryNum)
return this.filterHotTagInfo(body.tags)
})
},
filterHotTagInfo(rawList) {
return rawList.map(item => ({
id: item.playlistTag.name,
name: item.playlistTag.name,
source: 'wy',
}))
},
getTags() {
return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'wy' }))
},
async getDetailPageUrl(rawId) {
const { id } = await this.getListId(rawId)
return `https://music.163.com/#/playlist?id=${id}`
},
search(text, page, limit = 20) {
return eapiRequest('/api/cloudsearch/pc', {
s: text,
type: 1000, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频
limit,
total: page == 1,
offset: limit * (page - 1),
})
.promise.then(({ body }) => {
if (body.code != this.successCode) throw new Error('filed')
// console.log(body)
return {
list: this.filterList(body.result.playlists),
limit,
total: body.result.playlistCount,
source: 'wy',
}
})
},
}
// getList
// getTags
// getListDetail
+33
View File
@@ -0,0 +1,33 @@
import { httpFetch } from '../request.js'
import { weapi } from './utils/crypto.js'
import { formatSingerName } from '../utils.js'
export default {
requestObj: null,
cancelTipSearch() {
if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()
},
tipSearchBySong(str) {
this.cancelTipSearch()
this.requestObj = httpFetch('https://music.163.com/weapi/search/suggest/web', {
method: 'POST',
headers: {
referer: 'https://music.163.com/',
origin: 'https://music.163.com/',
},
form: weapi({
s: str,
}),
})
return this.requestObj.promise.then(({ statusCode, body }) => {
if (statusCode != 200 || body.code != 200) return Promise.reject(new Error('请求失败'))
return body.result.songs
})
},
handleResult(rawData) {
return rawData.map(info => `${info.name} - ${formatSingerName(info.artists, 'name')}`)
},
async search(str) {
return this.tipSearchBySong(str).then(result => this.handleResult(result))
},
}
+54
View File
@@ -0,0 +1,54 @@
// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js
import { createCipheriv, createDecipheriv, publicEncrypt, randomBytes, createHash, constants } from 'crypto'
const iv = Buffer.from('0102030405060708')
const presetKey = Buffer.from('0CoJUm6Qyw8W8jud')
const linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q')
const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
const publicKey = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----'
const eapiKey = 'e82ckenh8dichen8'
const aesEncrypt = (buffer, mode, key, iv) => {
const cipher = createCipheriv(mode, key, iv)
return Buffer.concat([cipher.update(buffer), cipher.final()])
}
const aesDecrypt = function(cipherBuffer, mode, key, iv) {
let decipher = createDecipheriv(mode, key, iv)
return Buffer.concat([decipher.update(cipherBuffer), decipher.final()])
}
const rsaEncrypt = (buffer, key) => {
buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer])
return publicEncrypt({ key, padding: constants.RSA_NO_PADDING }, buffer)
}
export const weapi = object => {
const text = JSON.stringify(object)
const secretKey = randomBytes(16).map(n => (base62.charAt(n % 62).charCodeAt()))
return {
params: aesEncrypt(Buffer.from(aesEncrypt(Buffer.from(text), 'aes-128-cbc', presetKey, iv).toString('base64')), 'aes-128-cbc', secretKey, iv).toString('base64'),
encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex'),
}
}
export const linuxapi = object => {
const text = JSON.stringify(object)
return {
eparams: aesEncrypt(Buffer.from(text), 'aes-128-ecb', linuxapiKey, '').toString('hex').toUpperCase(),
}
}
export const eapi = (url, object) => {
const text = typeof object === 'object' ? JSON.stringify(object) : object
const message = `nobody${url}use${text}md5forencrypt`
const digest = createHash('md5').update(message).digest('hex')
const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`
return {
params: aesEncrypt(Buffer.from(data), 'aes-128-ecb', eapiKey, '').toString('hex').toUpperCase(),
}
}
export const eapiDecrypt = cipherBuffer => {
return aesDecrypt(cipherBuffer, 'aes-128-ecb', eapiKey, '').toString()
}
+22
View File
@@ -0,0 +1,22 @@
import { httpFetch } from '../../request.js'
import { eapi } from './crypto.js'
export const eapiRequest = (url, data) => {
return httpFetch('http://interface.music.163.com/eapi/batch', {
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',
// cookie: 'os=pc; deviceId=A9C064BB4584D038B1565B58CB05F95290998EE8B025AA2D07AE; osver=Microsoft-Windows-10-Home-China-build-19043-64bit; appver=2.5.2.197409; channel=netease; MUSIC_A=37a11f2eb9de9930cad479b2ad495b0e4c982367fb6f909d9a3f18f876c6b49faddb3081250c4980dd7e19d4bd9bf384e004602712cf2b2b8efaafaab164268a00b47359f85f22705cc95cb6180f3aee40f5be1ebf3148d888aa2d90636647d0c3061cd18d77b7a0; __csrf=05b50d54082694f945d7de75c210ef94; mode=Z7M-KP5(7)GZ; NMTID=00OZLp2VVgq9QdwokUgq3XNfOddQyIAAAF_6i8eJg; ntes_kaola_ad=1',
},
form: eapi(url, data),
})
// requestObj.promise = requestObj.promise.then(({ body }) => {
// // console.log(raw)
// console.log(body)
// // console.log(eapiDecrypt(raw))
// // return eapiDecrypt(raw)
// return body
// })
// return requestObj
}
+55
View File
@@ -0,0 +1,55 @@
// 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 musicInfo from './musicInfo.js'
// import { closeVerifyModal } from './util.js'
const xm = {
// songList,
// musicSearch,
// leaderboard,
// hotSearch,
// closeVerifyModal,
comment: {
getComment() {
return Promise.reject(new Error('fail'))
},
getHotComment() {
return Promise.reject(new Error('fail'))
},
},
getMusicUrl(songInfo, type) {
return {
promise: Promise.reject(new Error('fail')),
}
// return apis('xm').getMusicUrl(songInfo, type)
},
getLyric(songInfo) {
return {
promise: Promise.reject(new Error('fail')),
}
// return lyric.getLyric(songInfo)
},
getPic(songInfo) {
return Promise.reject(new Error('fail'))
// return pic.getPic(songInfo)
},
// getMusicDetailPageUrl(songInfo) {
// if (songInfo.songStringId) return `https://www.xiami.com/song/${songInfo.songStringId}`
// musicInfo.getMusicInfo(songInfo).then(({ data }) => {
// songInfo.songStringId = data.songStringId
// })
// return `https://www.xiami.com/song/${songInfo.songmid}`
// },
// init() {
// getToken()
// },
}
export default xm
+27
View File
@@ -0,0 +1,27 @@
{
"name": "music-server",
"version": "1.0.0",
"description": "多平台音乐聚合服务器(基于 lx-music 聚合 SDK)",
"type": "module",
"main": "src/index.js",
"scripts": {
"build": "tsc",
"start": "npm run build && node src/server.js",
"test": "npm run build && node src/smoke-test.js"
},
"imports": {
"#common/*": "./src/common/*.js",
"#renderer/*": "./src/renderer/*.js"
},
"dependencies": {
"crypto-js": "^4.2.0",
"iconv-lite": "^0.6.3",
"isolated-vm": "^7.0.1",
"pako": "^2.1.0"
},
"devDependencies": {
"@types/node": "^26.4.0",
"playwright": "^1.62.1",
"typescript": "^7.0.2"
}
}
+37
View File
@@ -0,0 +1,37 @@
// 百度音乐 URL 获取(直连)
// 接口: https://musicapi.qianqian.com/v1/restserver/ting?method=baidu.ting.song.getSongLink
// 返回: result.songinfo.song_link
// 统一接口约定:apis('bd').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// promise resolve → { type, url }
import { httpFetch } from '../common/request.js'
const getSongLink = (songmid, retryNum = 0) => {
const url = `https://musicapi.qianqian.com/v1/restserver/ting?method=baidu.ting.song.getSongLink&format=json&from=bmpc&version=1.0.0&version_d=11.1.6.0&songid=${songmid}&type=1&res=1&s_protocol=1&aac=2&project=tpass`
return httpFetch(url, {
method: 'get',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
},
timeout: 15000,
}).promise.then(({ statusCode, body }) => {
if (statusCode != 200 || body.error_code != 22000 || !body.result?.songinfo) {
if (retryNum < 2) return getSongLink(songmid, ++retryNum)
return Promise.reject(new Error('获取失败'))
}
return body.result.songinfo
})
}
export const getMusicUrl = (songInfo, type) => {
const requestObj = { cancelHttp() {}, promise: null }
requestObj.promise = getSongLink(songInfo.songmid).then(info => {
// song_link 为试听地址(128k),按音质选择字段
const url = info.song_link || info.file_link
if (!url) return Promise.reject(new Error('获取失败'))
return { type, url }
})
return requestObj
}
export default { getMusicUrl }
+19
View File
@@ -0,0 +1,19 @@
// 各平台 URL 直连接口汇总
// 服务器环境固定使用内置直连接口(apiSource.value === 'internal')
// lx 为 lx-music 聚合源:isolated-vm 硬隔离执行原始脚本(签名方案已放弃,见 docs/lx-source-ivm.md)
import kw from './kw.js';
import kg from './kg.js';
import tx from './tx.js';
import wy from './wy.js';
import mg from './mg.js';
import bd from './bd.js';
import lx from './lx-source.js';
export default {
kw: kw,
kg: kg,
tx: tx,
wy: wy,
mg: mg,
bd: bd,
lx: lx,
};
+38
View File
@@ -0,0 +1,38 @@
// 各平台 URL 直连接口汇总
// 服务器环境固定使用内置直连接口(apiSource.value === 'internal')
// lx 为 lx-music 聚合源:isolated-vm 硬隔离执行原始脚本(签名方案已放弃,见 docs/lx-source-ivm.md)
import kw from './kw.js'
import kg from './kg.js'
import tx from './tx.js'
import wy from './wy.js'
import mg from './mg.js'
import bd from './bd.js'
import lx from './lx-source.js'
interface SongInfo {
songmid?: string
hash?: string
copyrightId?: string
[key: string]: any
}
interface MusicUrlResult {
type: string
url: string
}
interface ApiModule {
getMusicUrl?(songInfo: SongInfo, type: string): { promise: Promise<MusicUrlResult>; cancelHttp: () => void }
[key: string]: any
}
export default {
kw: kw as ApiModule,
kg: kg as ApiModule,
tx: tx as ApiModule,
wy: wy as ApiModule,
mg: mg as ApiModule,
bd: bd as ApiModule,
lx: lx as unknown as Record<string, ApiModule>,
}
+50
View File
@@ -0,0 +1,50 @@
// 酷狗音乐 URL 获取(直连)
// 接口: https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash={hash}&album_id={albumId}&mid={mid}
// 统一接口约定:apis('kg').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// promise resolve → { type, url }
import { httpFetch } from '../common/request.js'
let mid = ''
const getMid = () => {
if (mid) return Promise.resolve(mid)
// 随机生成 26 位小写字母数字混合的 mid
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
mid = Array.from({ length: 26 }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
return Promise.resolve(mid)
}
const getPlayUrl = (hash, albumId) => getMid().then(m => {
const url = `https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash=${hash}${albumId ? `&album_id=${albumId}` : ''}&mid=${m}`
return httpFetch(url, {
method: 'get',
headers: {
Referer: 'https://www.kugou.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
},
timeout: 15000,
}).promise.then(({ statusCode, body }) => {
if (statusCode != 200 || body.err_code != 0 || !body.data) {
return Promise.reject(new Error('获取失败'))
}
return body.data
})
})
export const getMusicUrl = (songInfo, type) => {
const requestObj = { cancelHttp() {}, promise: null }
const hash = songInfo._types?.[type]?.hash || songInfo.hash
if (!hash) {
requestObj.promise = Promise.reject(new Error('缺少 hash'))
return requestObj
}
requestObj.promise = getPlayUrl(hash, songInfo.albumId).then(data => {
const url = data.play_url || data.play_backup_url
if (!url) return Promise.reject(new Error('获取失败'))
return { type, url }
})
return requestObj
}
export default { getMusicUrl }
+108
View File
@@ -0,0 +1,108 @@
// 酷我音乐 URL 获取(直连)
// 统一接口约定:apis('kw').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// promise resolve → { type, url }
//
// 取流方案:
// 首选 antiserver.kuwo.cn/anti.s(convert_url3 老接口,无需 token,128k/320k 有效)
// flac 及以上音质该接口返回 refuse,回退 www.kuwo.cn/api/v1/www/music/playUrl
// (需要 csrf token,token 失效时自动重新获取重试一次)
import { httpFetch } from '../common/request.js'
// 音质映射:128k → 128kmp3, 320k → 320kmp3, flac → flac(antiserver 用 mp3/flac 格式)
const qualityMap = {
'128k': { br: '128kmp3', format: 'mp3' },
'320k': { br: '320kmp3', format: 'mp3' },
flac: { br: 'flac', format: 'flac' },
flac24bit: { br: 'flac', format: 'flac' },
}
let token = ''
let tokenPromise = null
/**
* 获取 kw csrf token(访问首页拿 cookie)
*/
const getToken = (retryNum = 0) => {
if (token) return Promise.resolve(token)
if (tokenPromise) return tokenPromise
tokenPromise = httpFetch('http://www.kuwo.cn/', {
method: 'get',
timeout: 15000,
}).promise.then(({ headers }) => {
tokenPromise = null
const match = (headers['set-cookie'] || []).join(';').match(/kw_token=(\w+)/)
if (!match) throw new Error('获取token失败')
token = match[1]
return token
}).catch(err => {
tokenPromise = null
if (retryNum > 2) throw err
return getToken(++retryNum)
})
return tokenPromise
}
/**
* 通过 antiserver 老接口获取(无需 token)
* @param {string} rid MUSIC_xxx
* @param {string} br 128kmp3 / 320kmp3 / flac
* @param {string} format mp3 / flac
*/
const getUrlByAntiServer = (rid, br, format) => {
return httpFetch(`http://antiserver.kuwo.cn/anti.s?type=convert_url3&rid=${rid}&format=${format}&response=url&br=${br}`, {
method: 'get',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
},
timeout: 15000,
}).promise.then(({ statusCode, body }) => {
if (statusCode != 200 || typeof body != 'object' || body.code != 200 || !body.url) {
return Promise.reject(new Error(body?.msg || '获取失败'))
}
return body.url
})
}
/**
* 通过官方 playUrl 接口获取(需要 csrf token)
*/
const getUrlByPlayUrl = (songmid, br, retryNum = 0) => {
const requestObj = httpFetch(`http://www.kuwo.cn/api/v1/www/music/playUrl?mid=${songmid}&type=music&br=${br}`, {
method: 'get',
headers: {
csrf: token,
cookie: `kw_token=${token}`,
Referer: 'http://www.kuwo.cn/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
},
timeout: 15000,
})
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode != 200 || body.code != 200) {
// token 失效时重新获取再试一次
if (token && /token|403|401/.test(body?.msg || '') && retryNum == 0) {
token = ''
return getUrlByPlayUrl(songmid, br, ++retryNum).promise
}
return Promise.reject(new Error(body?.msg || '获取失败'))
}
if (!body.data?.url) return Promise.reject(new Error('获取失败'))
return body.data.url
})
return requestObj
}
export const getMusicUrl = (songInfo, type) => {
const requestObj = { cancelHttp() {}, promise: null }
const { br, format } = qualityMap[type] || qualityMap['128k']
const rid = `MUSIC_${songInfo.songmid}`
requestObj.promise = getUrlByAntiServer(rid, br, format).catch(() => {
// antiserver 无此音质(flac 常被拒)时回退官方接口
return getToken().then(() => getUrlByPlayUrl(songInfo.songmid, br).promise)
}).then(url => ({ type, url }))
return requestObj
}
export default { getMusicUrl }
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff. Show more