refactor: 架构整固——分层单向化、全量 TypeScript、直接运行

- 移除 bd/mg 两平台及磁盘残留产物;清理 api-test 死脚本、musicSdk/message+options 死模块、infSign vendor
- musicSdk/ 改名 sdk/;src/common、src/auth、src/apis 整体移入 sdk/,依赖方向定为 src → sdk 严格单向
- src/ 手写 JS 全量转 .ts,删除 JS 类型垫片 modules.d.ts;源码 import 统一 .ts 后缀
- Node 原生类型剥离直接运行 .ts(engines >=22.18),取消 tsc 发射产物,tsc 仅类型检查(pnpm typecheck)
- 删除 kwdes 备用实现与 src/index.ts 转发垫片;测试移至 test/smoke-test.ts
- common/request.ts 补显式导出类型;修复 set-cookie 单值形态、ApiError 类字段等隐患

验证:tsc 零错误,smoke-test 41/41
This commit is contained in:
snowy-yang committed 2026-09-25 23:06:23 +08:00
1 parent c8c939aa32
commit 354df2e739
126 files changed
+413 -2942

No files matched your search

+4 -6
View File
@@ -35,9 +35,7 @@ dist/
coverage/ coverage/
.nyc_output/ .nyc_output/
# TypeScript in-place emit (musicSdk source is .ts) # TypeScript:源码全部 .ts、由 Node 直接运行(类型剥离),不再生成 .js 产物。
musicSdk/**/*.js # 以下规则仅作保险,防止旧的发射产物被误生成后提交。
!musicSdk/**/api-test.js src/**/*.js
!musicSdk/**/api-temp.js sdk/**/*.js
!musicSdk/**/temp/*.js
!musicSdk/**/vendors/*.js
+31 -32
View File
@@ -4,7 +4,7 @@
## 功能 ## 功能
- **多平台聚合**:酷我(kw)、酷狗(kg)、QQ音乐(tx)、网易云(wy)、咪咕(mg) - **多平台聚合**:酷我(kw)、酷狗(kg)、QQ音乐(tx)、网易云(wy)
- **直连取流**:各平台播放地址获取(不依赖第三方 API 服务) - **直连取流**:各平台播放地址获取(不依赖第三方 API 服务)
- **完整链路**:搜索、播放地址、歌词(含各平台加密歌词解码)、封面、榜单、热搜、歌单 - **完整链路**:搜索、播放地址、歌词(含各平台加密歌词解码)、封面、榜单、热搜、歌单
@@ -12,8 +12,9 @@
```bash ```bash
pnpm install pnpm install
pnpm start # 默认端口 16688,可用 PORT 环境变量修改 pnpm start # 默认端口 16688,可用 PORT 环境变量修改(需 Node 22.18+,直接运行 .ts)
pnpm test # 运行冒烟测试(自动启动/关闭本地服务器) pnpm test # 类型检查(tsc)+ 冒烟测试(自动启动/关闭本地服务器)
pnpm typecheck # 仅类型检查,不运行
``` ```
## HTTP 接口 ## HTTP 接口
@@ -34,7 +35,7 @@ pnpm test # 运行冒烟测试(自动启动/关闭本地服务器)
| `GET /singer/:source/:id` | 歌手信息(kg/tx/wy) | | `GET /singer/:source/:id` | 歌手信息(kg/tx/wy) |
| `GET /singer/:source/:id/songs?page=1&limit=30` | 歌手歌曲列表(kg/tx/wy) | | `GET /singer/:source/:id/songs?page=1&limit=30` | 歌手歌曲列表(kg/tx/wy) |
| `GET /singer/:source/:id/albums?page=1&limit=10` | 歌手专辑列表(kg/tx/wy) | | `GET /singer/:source/:id/albums?page=1&limit=10` | 歌手专辑列表(kg/tx/wy) |
| `GET /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/mg/tx) | | `GET /album/:source/:id?page=1` | 专辑详情与曲目(kg/kw/tx) |
| `POST /login/wy/qr` → `POST /login/wy/qr/check` | 网易云扫码登录(扫码后轮询完成登录) | | `POST /login/wy/qr` → `POST /login/wy/qr/check` | 网易云扫码登录(扫码后轮询完成登录) |
| `POST /login/wy/cellphone` | 网易云手机号登录(password 或 captcha) | | `POST /login/wy/cellphone` | 网易云手机号登录(password 或 captcha) |
| `POST /login/kg` + `userid` | 绑定酷狗 token(App 抓包获取,服务端验证) | | `POST /login/kg` + `userid` | 绑定酷狗 token(App 抓包获取,服务端验证) |
@@ -51,40 +52,38 @@ pnpm test # 运行冒烟测试(自动启动/关闭本地服务器)
- **kw / tx / wy**:`songmid`(如 `228908`、`0039MnYb0qxYhV`) - **kw / tx / wy**:`songmid`(如 `228908`、`0039MnYb0qxYhV`)
- **kg**:`hash`(搜索结果的 `hash` 字段;当前环境 kg 取流接口被服务端限制,URL 获取可能失败) - **kg**:`hash`(搜索结果的 `hash` 字段;当前环境 kg 取流接口被服务端限制,URL 获取可能失败)
- **mg**:`songmid`(搜索结果的 `songmid` 字段,内部自动换取 contentId)
## 项目结构 ## 项目结构
``` ```
src/ src/ HTTP 应用层(Fastify 服务器,全部 TypeScript 源码)
server.js HTTP 服务器入口(pnpm start 实际启动文件,Fastify) server.ts HTTP 服务器入口(pnpm start 实际启动文件)
routes.js 业务路由(JSON Schema 校验 + OpenAPI 元数据) routes.ts 业务路由(JSON Schema 校验 + OpenAPI 元数据)
smoke-test.js 冒烟测试(自动启动本地服务器) routes-login.ts 登录路由(wy 扫码/手机号、kg token、tx cookie)
index.js SDK 根模块(工具函数聚合导出) http/
http/ HTTP 层 api-error.ts 业务错误类型(404/502 等 → 统一 JSON 响应)
api-error.js 业务错误类型(404/502 等 → 统一 JSON 响应) openapi/
openapi/ 文档层 components.ts OpenAPI 共享 schema(与路由校验同源)
components.js OpenAPI 共享 schema(与路由校验同源) ui.ts ReDoc 页面模板
ui.js ReDoc 页面模板 sdk/ 音乐聚合 SDK(自包含:平台模块 + 取流 + 凭据 + 基础设施;kw/kg/tx/wy 四平台)
apis/ 各平台 URL 直连实现(kw/kg/tx/wy/mg/bd) index.ts 平台聚合入口(通用工具转发,实现在 common/utils.ts)
source.js 外部音源懒加载入口(sources/ 目录脚本,不入库) api-source.ts 接口选择层(internal 内置直连 / sources/ 外部音源脚本,SOURCE_ENABLED 切换)
source-ivm.js isolated-vm 隔离执行器 apis/ 各平台 URL 直连取流实现(kw/kg/tx/wy)+ 外部音源加载器(source.ts / source-ivm.ts)
common/ 公共模块 auth/ 登录态与登录协议(store.ts 持久化凭据到 data/auth.json;wy.ts 扫码/手机号、kg.ts token)
request.js HTTP 请求(可取消、自动 JSON 解析、gzip/deflate 解压) common/ SDK 基础设施
utils.js 工具函数(sizeFormate/formatPlayTime/dateFormat 等) request.ts HTTP 请求(可取消、自动 JSON 解析、gzip/deflate 解压)
message.js 请求错误消息 utils.ts 工具函数(sizeFormate/formatPlayTime/dateFormat 等)
ipcNames.js IPC 事件名(兼容 SDK) message.ts 请求错误消息
rendererIpc.js IPC 本地实现(kw/tx 歌词解码) ipcNames.ts IPC 事件名(兼容 lx-music 结构)
lyricDecode.js 酷我/QQ 歌词解码(替代原 Electron 主进程处理器) rendererIpc.ts IPC 本地实现(kw/tx 歌词解码)
qrcDecode.js QQ 歌词 3DES 解密(纯 JS 移植) lyricDecode.ts 酷我/QQ 歌词解码
lyricUtils/kg.js 酷狗 krc 歌词解码 qrcDecode.ts QQ 歌词 3DES 解密(纯 JS 移植)
musicSdk/ 音乐聚合 SDK(搜索/歌词/榜单等核心逻辑,TypeScript 源码) lyricUtils/kg.ts 酷狗 krc 歌词解码
index.js 平台聚合入口 test/
request.js SDK 请求模块(转接 src/common/request.js) smoke-test.ts 冒烟测试(pnpm test 运行,自动启动/关闭本地服务器)
message.js SDK 消息模块(转接 src/common/message.js)
``` ```
> TypeScript 源码(`src/apis/index.ts`、`musicSdk/**/*.ts`)经 `pnpm run build`(tsc)就地发射为 `.js` 产物后运行。 > 分层依赖严格单向:`src` → `sdk`,`sdk` 内部自包含(不反向依赖 src)。源码统一为 TypeScript(import 一律写 `.ts` 后缀),由 Node ≥22.18 的原生类型剥离直接运行——**无编译产物**,改完即跑;tsc 仅用于类型检查(`pnpm typecheck`)。代码只使用可剥离的 TS 语法(无 enum/namespace/参数属性),新增代码也须遵守。
## 已知限制 ## 已知限制
+3 -3
View File
@@ -1,6 +1,6 @@
# MusicServer 聚合音乐 API # MusicServer 聚合音乐 API
多平台音乐聚合接口:搜索、播放地址、歌词、封面、榜单、歌单。支持平台:**kw**(酷我)/ **kg**(酷狗)/ **tx**(QQ音乐)/ **wy**(网易云)/ **mg**(咪咕)。 多平台音乐聚合接口:搜索、播放地址、歌词、封面、榜单、歌单。支持平台:**kw**(酷我)/ **kg**(酷狗)/ **tx**(QQ音乐)/ **wy**(网易云)。
> 本文档为静态速查版。交互式文档以服务器实际输出为准: > 本文档为静态速查版。交互式文档以服务器实际输出为准:
> >
@@ -20,7 +20,7 @@
- `502` 上游平台请求失败 - `502` 上游平台请求失败
- `500` 服务器内部错误 - `500` 服务器内部错误
- 资源类接口统一为**路径参数风格**;`/search`、`/find` 搜索类接口使用 query 参数。 - 资源类接口统一为**路径参数风格**;`/search`、`/find` 搜索类接口使用 query 参数。
- 平台歌曲 id 形态:kw/tx/wy/mg 用 `songmid`,kg 用 `hash`(mg 内部自动换取 contentId)。 - 平台歌曲 id 形态:kw/tx/wy 用 `songmid`,kg 用 `hash`。
## 接口速查 ## 接口速查
@@ -71,7 +71,7 @@
| GET | `/singer/:source/:id` | 歌手信息(name/desc/avatar 与歌曲/专辑数量)。支持 kg/tx/wy | | GET | `/singer/:source/:id` | 歌手信息(name/desc/avatar 与歌曲/专辑数量)。支持 kg/tx/wy |
| GET | `/singer/:source/:id/songs` | 歌手歌曲列表;可带 `page`/`limit`(默认 20)。支持 kg/tx/wy | | GET | `/singer/:source/:id/songs` | 歌手歌曲列表;可带 `page`/`limit`(默认 20)。支持 kg/tx/wy |
| GET | `/singer/:source/:id/albums` | 歌手专辑列表;可带 `page`/`limit`。支持 kg/tx/wy | | GET | `/singer/:source/:id/albums` | 歌手专辑列表;可带 `page`/`limit`。支持 kg/tx/wy |
| GET | `/album/:source/:id` | 专辑详情与曲目列表;可带 `page`/`limit`(limit 仅 kg 生效,kw 单页固定 1000 条)。支持 kg/kw/mg/tx;tx 的专辑名/作者由曲目数据合成(desc 为空) | | GET | `/album/:source/:id` | 专辑详情与曲目列表;可带 `page`/`limit`(limit 仅 kg 生效,kw 单页固定 1000 条)。支持 kg/kw/tx;tx 的专辑名/作者由曲目数据合成(desc 为空) |
歌手/专辑 id 来源:搜索结果条目包含 `albumId`;歌手 id 需从平台方获取(不在搜索结果中)。 歌手/专辑 id 来源:搜索结果条目包含 `albumId`;歌手 id 需从平台方获取(不在搜索结果中)。
-26
View File
@@ -1,26 +0,0 @@
import { httpFetch } from '../request.js'
import { requestMsg } from '../message.js'
import { headers, timeout } from '../options.js'
import { dnsLookup } from '../utils.js'
const api_test = {
getMusicUrl(songInfo, type) {
const requestObj = httpFetch(`http://ts.tempmusics.tk/url/bd/${songInfo.songmid}/${type}`, {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
default: return Promise.reject(new Error(requestMsg.fail))
}
})
return requestObj
},
}
export default api_test
-24
View File
@@ -1,24 +0,0 @@
import { httpFetch } from '../request.ts'
import type { HttpRequestObject } from '../types.ts'
export default {
_requestObj: null as HttpRequestObject | null,
async getList(retryNum = 0) {
if (this._requestObj) this._requestObj.cancelHttp()
if (retryNum > 2) return Promise.reject(new Error('try max num'))
const _requestObj = httpFetch('http://musicapi.qianqian.com/v1/restserver/ting?from=android&version=7.0.2.0&channel=ppzs&operator=0&method=baidu.ting.search.hot', {
method: 'get',
headers: {
'User-Agent': 'android_7.0.2.0;baiduyinyue',
},
})
const { body, statusCode } = await _requestObj.promise
if (statusCode != 200 || body.error_code !== 22000) throw new Error('获取热搜词失败')
// console.log(body, statusCode)
return { source: 'bd', list: this.filterList(body.result) }
},
filterList(rawList) {
return rawList.map(item => item.word)
},
}
-40
View File
@@ -1,40 +0,0 @@
import leaderboard from './leaderboard.ts'
import { apis } from '../api-source.ts'
import musicInfo from './musicInfo.ts'
import songList from './songList.ts'
import { httpFetch } from '../request.ts'
import musicSearch from './musicSearch.ts'
import hotSearch from './hotSearch.ts'
const bd = {
leaderboard,
songList,
musicSearch,
hotSearch,
getMusicUrl(songInfo, type) {
return apis('bd').getMusicUrl!(songInfo, type)
},
getPic(songInfo) {
const requestObj = this.getMusicInfo(songInfo)
return requestObj.promise.then(info => info.pic_premium)
},
getLyric(songInfo) {
const requestObj = this.getMusicInfo(songInfo)
requestObj.promise = requestObj.promise.then(info => httpFetch(info.lrclink).promise.then(resp => ({ lyric: resp.body, tlyric: '' })))
return requestObj
},
// getLyric(songInfo) {
// return apis('bd').getLyric(songInfo)
// },
// getPic(songInfo) {
// return apis('bd').getPic(songInfo)
// },
getMusicInfo(songInfo) {
return musicInfo.getMusicInfo(songInfo.songmid)
},
getMusicDetailPageUrl(songInfo) {
return `http://music.taihe.com/song/${songInfo.songmid}`
},
}
export default bd
-153
View File
@@ -1,153 +0,0 @@
import { httpFetch } from '../request.ts'
// import { formatPlayTime } from '../index.js'
const boardList = [
// { id: 'bd__601', name: '歌单榜', bangid: '601' },
{ id: 'bd__2', name: '热歌榜', bangid: '2' },
{ id: 'bd__20', name: '华语金曲榜', bangid: '20' },
{ id: 'bd__25', name: '网络歌曲榜', bangid: '25' },
{ id: 'bd__1', name: '新歌榜', bangid: '1' },
{ id: 'bd__21', name: '欧美金曲榜', bangid: '21' },
{ id: 'bd__200', name: '原创音乐榜', bangid: '200' },
{ id: 'bd__22', name: '经典老歌榜', bangid: '22' },
{ id: 'bd__24', name: '影视金曲榜', bangid: '24' },
{ id: 'bd__23', name: '情歌对唱榜', bangid: '23' },
{ id: 'bd__11', name: '摇滚榜', bangid: '11' },
{ id: 'bd__105', name: '好童星榜', bangid: '105' },
{ id: 'bd__106', name: '雅克•藏羌彝原创音乐榜', bangid: '106' },
]
export default {
limit: 20,
list: [
{
id: 'bdrgb',
name: '热歌榜',
bangid: '2',
},
{
id: 'bdxgb',
name: '新歌榜',
bangid: '1',
},
{
id: 'bdycb',
name: '原创榜',
bangid: '200',
},
{
id: 'bdhyjqb',
name: '华语榜',
bangid: '20',
},
{
id: 'bdomjqb',
name: '欧美榜',
bangid: '21',
},
{
id: 'bdwugqb',
name: '网络榜',
bangid: '25',
},
{
id: 'bdjdlgb',
name: '老歌榜',
bangid: '22',
},
{
id: 'bdysjqb',
name: '影视金曲榜',
bangid: '24',
},
{
id: 'bdqgdcb',
name: '情歌对唱榜',
bangid: '23',
},
{
id: 'bdygb',
name: '摇滚榜',
bangid: '11',
},
],
getUrl(id, p) {
return `http://musicmini.qianqian.com/2018/static/bangdan/bangdanList_${id}_${p}.html`
},
regExps: {
item: /data-song="({.+?})"/g,
info: /{total[\s:]+"(\d+)", size[\s:]+"(\d+)", page[\s:]+"(\d+)"}/,
},
getData(url) {
const requestObj = httpFetch(url)
return requestObj.promise
},
filterData(rawList) {
// console.log(rawList)
return rawList.map(item => {
const types: any[] = []
const _types: Record<string, any> = {}
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
@@ -1,20 +0,0 @@
import { httpFetch } from '../request.ts'
export default {
cache: {},
getMusicInfo(songmid) {
if (this.cache[songmid]) {
return { promise: Promise.resolve(this.cache[songmid]) }
}
const requestObj = httpFetch(`https://musicapi.qianqian.com/v1/restserver/ting?method=baidu.ting.song.getSongLink&format=json&from=bmpc&version=1.0.0&version_d=11.1.6.0&songid=${songmid}&type=1&res=1&s_protocol=1&aac=2&project=tpass`)
requestObj.promise = requestObj.promise.then(({ body }) => {
// console.log(body)
if (body.error_code == 22000) {
this.cache[songmid] = body.result.songinfo
return body.result.songinfo
}
return Promise.reject(new Error('获取音乐信息失败'))
})
return requestObj
},
}
-88
View File
@@ -1,88 +0,0 @@
// import '../../polyfill/array.find.js'
import { httpFetch } from '../request.ts'
import { formatPlayTime } from '../index.ts'
// import { debug } from '../../utils/env.js'
// import { formatSinger } from './util.js'
export default {
limit: 30,
total: 0,
page: 0,
allPage: 1,
musicSearch(str, page, limit) {
const searchRequest = httpFetch(`http://tingapi.ting.baidu.com/v1/restserver/ting?from=android&version=5.6.5.6&method=baidu.ting.search.merge&format=json&query=${encodeURIComponent(str)}&page_no=${page}&page_size=${limit}&type=0&data_source=0&use_cluster=1`)
return searchRequest.promise.then(({ body }) => body)
},
handleResult(rawData) {
let ids = new Set()
const list: any[] = []
if (!rawData) return list
rawData.forEach(item => {
if (ids.has(item.song_id)) return
ids.add(item.song_id)
const types: any[] = []
const _types: Record<string, any> = {}
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',
})
})
},
}
-264
View File
@@ -1,264 +0,0 @@
import { httpFetch } from '../request.ts'
import { formatPlayTime } from '../index.ts'
import { toMD5 } from '../utils.ts'
import CryptoJS from 'crypto-js'
import type { HttpRequestObject } from '../types.ts'
export default {
_requestObj_tags: null as HttpRequestObject | null,
_requestObj_list: null as HttpRequestObject | null,
_requestObj_listRecommend: null as HttpRequestObject | null,
limit_list: 30,
limit_song: 10000,
successCode: 22000,
sortList: [
{
name: '最热',
id: '1',
},
{
name: '最新',
id: '0',
},
],
regExps: {
// http://music.taihe.com/songlist/566347741
listDetailLink: /^.+\/songlist\/(\d+)(?:\?.*|&.*$|#.*$|$)/,
},
aesPassEncod(jsonData) {
let timestamp = Math.floor(Date.now() / 1000)
let privateKey = toMD5('baidu_taihe_music_secret_key' + timestamp).substr(8, 16)
let key = CryptoJS.enc.Utf8.parse(privateKey)
let iv = CryptoJS.enc.Utf8.parse(privateKey)
let arrData: string[] = []
let strData = ''
for (let key in jsonData) arrData.push(key)
arrData.sort()
for (let i = 0; i < arrData.length; i++) {
let key = arrData[i]
strData +=
(i === 0 ? '' : '&') + key + '=' + encodeURIComponent(jsonData[key])
}
let JsonFormatter = {
stringify(cipherParams) {
let jsonObj: Record<string, any> = {
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) as any) / 10 + '亿'
if (num > 10000) return parseInt((num / 1000) as any) / 10 + '万'
return num
},
filterList(rawData) {
return rawData.map(item => ({
play_count: this.formatPlayCount(item.listen_num),
id: String(item.list_id),
author: item.username,
name: item.title,
// time: item.publish_time,
img: item.list_pic_large || item.list_pic,
grade: item.grade,
desc: item.desc || item.tag,
source: 'bd',
}))
},
// 获取歌曲列表内的音乐
getListDetail(id, page, tryNum = 0) {
if (tryNum > 2) return Promise.reject(new Error('try max num'))
if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')
const requestObj_listDetail = httpFetch(this.getListDetailUrl(id, page))
return requestObj_listDetail.promise.then(({ body }) => {
if (body.error_code !== this.successCode) return this.getListDetail(id, page, ++tryNum)
let listData = this.filterData(body.result.songlist)
return {
list: listData,
page,
limit: this.limit_song,
total: body.result.song_num,
source: 'bd',
info: {
name: body.result.info.list_title,
img: body.result.info.list_pic,
desc: body.result.info.list_desc,
author: body.result.info.userinfo.username,
play_count: this.formatPlayCount(body.result.listen_num),
},
}
})
},
filterData(rawList) {
// console.log(rawList)
return rawList.map(item => {
const types: any[] = []
const _types: Record<string, any> = {}
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
-50
View File
@@ -1,50 +0,0 @@
import { httpFetch } from '../request.js'
import { requestMsg } from '../message.js'
import { headers, timeout } from '../options.js'
import { dnsLookup } from '../utils.js'
const api_test = {
getMusicUrl(songInfo, type) {
const requestObj = httpFetch(`http://ts.tempmusics.tk/url/kg/${songInfo._types[type].hash}/${type}`, {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
default: return Promise.reject(new Error(requestMsg.fail))
}
})
return requestObj
},
getPic(songInfo) {
const requestObj = httpFetch(`http://ts.tempmusics.tk/pic/kg/${songInfo.hash}`, {
method: 'get',
timeout,
headers,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail))
})
return requestObj
},
getLyric(songInfo) {
const requestObj = httpFetch(`http://ts.tempmusics.tk/lrc/kg/${songInfo.hash}`, {
method: 'get',
timeout,
headers,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail))
})
return requestObj
},
}
export default api_test
-2
View File
@@ -1,2 +0,0 @@
!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self,t.infSign=n())}(this,function(){"use strict";function t(t,n,r){return n in t?Object.defineProperty(t,n,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[n]=r,t}function n(t,n){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var e=Object.getOwnPropertySymbols(t);n&&(e=e.filter(function(n){return Object.getOwnPropertyDescriptor(t,n).enumerable})),r.push.apply(r,e)}return r}function r(r){for(var e=1;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
-26
View File
@@ -1,26 +0,0 @@
import { httpFetch } from '../request.js'
import { requestMsg } from '../message.js'
import { headers, timeout } from '../options.js'
import { dnsLookup } from '../utils.js'
const api_temp = {
getMusicUrl(songInfo, type) {
const requestObj = httpFetch(`http://tm.tempmusics.tk/url/kw/${songInfo.songmid}/${type}`, {
method: 'get',
headers,
timeout,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
default: return Promise.reject(new Error(body.msg))
}
})
return requestObj
},
}
export default api_temp
-37
View File
@@ -1,37 +0,0 @@
import { httpFetch } from '../request.js'
import { requestMsg } from '../message.js'
import { headers, timeout } from '../options.js'
import { dnsLookup } from '../utils.js'
const api_test = {
// getMusicUrl(songInfo, type) {
// const requestObj = httpFetch(`http://45.32.53.128:3002/m/kw/u/${songInfo.songmid}/${type}`, {
// method: 'get',
// headers,
// timeout,
// })
// requestObj.promise = requestObj.promise.then(({ body }) => {
// return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(body.msg))
// })
// return requestObj
// },
getMusicUrl(songInfo, type) {
const requestObj = httpFetch(`http://ts.tempmusics.tk/url/kw/${songInfo.songmid}/${type}`, {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
default: return Promise.reject(new Error(requestMsg.fail))
}
})
return requestObj
},
}
export default api_test
-4
View File
@@ -1,4 +0,0 @@
// SDK 根消息模块(转接至 src/common/message.js)
// musicSdk 内 `import { requestMsg } from '../message.js'` 的解析目标
export { requestMsg } from '../src/common/message.js'
-49
View File
@@ -1,49 +0,0 @@
import { createHttpFetch } from './utils/index.js'
import { filterMusicInfoList } from './musicInfo.js'
import { formatPlayCount } from '../index.js'
export default {
/**
* 通过AlbumId获取专辑
* @param {*} id
* @param {*} page
*/
async getAlbumDetail(id, page = 1) {
const list = await createHttpFetch(`http://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/queryAlbumSong?albumId=${id}&pageNo=${page}`)
if (!list.songList) return Promise.reject(new Error('Get album list error.'))
const songList = filterMusicInfoList(list.songList)
const listInfo = await this.getAlbumInfo(id)
return {
list: songList || [],
page,
limit: listInfo.total,
total: listInfo.total,
source: 'mg',
info: {
name: listInfo.name,
img: listInfo.image,
desc: listInfo.desc,
author: listInfo.author,
play_count: listInfo.play_count,
},
}
}, /**
* 通过AlbumId获取专辑信息
* @param {*} id
*/
async getAlbumInfo(id) {
const info = await createHttpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/resource/album/v2.0?albumId=${id}`)
if (!info) return Promise.reject(new Error('Get album info error.'))
return {
name: info.title ?? null,
image: info.imgItems?.length ? info.imgItems[0].img : null,
desc: info.summary ?? null,
author: info.singer ?? null,
play_count: formatPlayCount(info.opNumItem?.playNum ?? 0),
total: parseInt(info.totalCount) || null,
}
},
}
-26
View File
@@ -1,26 +0,0 @@
import { httpFetch } from '../request.js'
import { requestMsg } from '../message.js'
import { headers, timeout } from '../options.js'
import { dnsLookup } from '../utils.js'
const api_test = {
getMusicUrl(songInfo, type) {
const requestObj = httpFetch(`http://ts.tempmusics.tk/url/mg/${songInfo.copyrightId}/${type}`, {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
default: return Promise.reject(new Error(requestMsg.fail))
}
})
return requestObj
},
}
export default api_test
-97
View File
@@ -1,97 +0,0 @@
import { httpFetch } from '../request.ts'
import getSongId from './songId.ts'
import { dateFormat2 } from '../index.ts'
import type { HttpRequestObject } from '../types.ts'
export default {
_requestObj: null as HttpRequestObject | null,
_requestObj2: null as HttpRequestObject | null,
_requestObj3: null as HttpRequestObject | null,
lastCommentIds: new Map(),
async getComment(musicInfo, page = 1, limit = 20) {
if (this._requestObj) this._requestObj.cancelHttp()
if (!musicInfo.songId) {
let id = await getSongId(musicInfo)
if (!id) throw new Error('获取评论失败')
musicInfo.songId = id
}
if (page === 1) this.lastCommentIds.clear()
const lastCommentId = this.lastCommentIds.get(String(page)) || ''
if (!lastCommentId && page > 1) throw new Error('获取评论失败')
// const _requestObj = httpFetch(`https://music.migu.cn/v3/api/comment/listComments?targetId=${musicInfo.songId}&pageSize=${limit}&pageNo=${page}`, {
const _requestObj = httpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/user/comment/stack/v1.0?pageSize=${limit}&queryType=1&resourceId=${musicInfo.songId}&resourceType=2&commentId=${lastCommentId}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
// Referer: 'https://music.migu.cn',
},
})
const { body, statusCode } = await _requestObj.promise
// console.log(body)
if (statusCode != 200 || body.code !== '000000') throw new Error('获取评论失败')
const total = parseInt(body.data.commentNums)
const list = this.filterComment(body.data.comments)
this.lastCommentIds.set(String(page + 1), list.length ? list[list.length - 1].id : '')
return { source: 'mg', comments: list, total, page, limit, maxPage: Math.ceil(total / limit) || 1 }
},
async getHotComment(musicInfo, page = 1, limit = 20) {
if (this._requestObj2) this._requestObj2.cancelHttp()
if (!musicInfo.songId) {
let id = await getSongId(musicInfo)
if (!id) throw new Error('获取评论失败')
musicInfo.songId = id
}
// const _requestObj2 = httpFetch(`https://music.migu.cn/v3/api/comment/listTopComments?targetId=${musicInfo.songId}&pageSize=${limit}&pageNo=${page}`, {
const _requestObj2 = httpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/user/comment/stack/v1.0?pageSize=${limit}&queryType=2&resourceId=${musicInfo.songId}&resourceType=2&hotCommentStart=${(page - 1) * limit}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
// Referer: 'https://music.migu.cn',
},
})
const { body, statusCode } = await _requestObj2.promise
// console.log(body)
if (statusCode != 200 || body.code !== '000000') throw new Error('获取热门评论失败')
const total = parseInt(body.data.cfgHotCount)
return { source: 'mg', comments: this.filterComment(body.data.hotComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 }
},
async getReplyComment(musicInfo, replyId, page = 1, limit = 10) {
if (this._requestObj2) this._requestObj2.cancelHttp()
// const _requestObj2 = httpFetch(`https://music.migu.cn/v3/api/comment/listCommentsById?commentId=${replyId}&pageSize=${limit}&pageNo=${page}`, {
const _requestObj2 = httpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/user/comment/stack/${replyId}/v1.0?pageSize=${limit}&queryType=2&resourceId=${musicInfo.songId}&resourceType=2&start=${(page - 1) * limit}`, {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
},
})
const { body, statusCode } = await _requestObj2.promise
// console.log(body)
if (statusCode != 200 || body.code !== '000000') throw new Error('获取回复评论失败')
const total = parseInt(body.data.replyTotalCount)
return { source: 'mg', comments: this.filterComment(body.data.mainCommentItem.replyComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 }
},
filterComment(rawList) {
return rawList.map(item => ({
id: item.commentId,
text: item.commentInfo,
time: item.commentTime,
timeStr: dateFormat2(new Date(item.commentTime).getTime()),
userName: item.user.nickName,
avatar: item.user.middleIcon || item.user.bigIcon || item.user.smallIcon,
userId: item.user.userId,
likedCount: item.opNumItem.thumbNum,
replyNum: item.replyTotalCount,
reply: item.replyComments.map(c => ({
id: c.replyId,
text: c.replyInfo,
time: c.replyTime,
timeStr: dateFormat2(new Date(c.replyTime).getTime()),
userName: c.user.nickName,
avatar: c.user.middleIcon || c.user.bigIcon || c.user.smallIcon,
userId: c.user.userId,
likedCount: null,
replyNum: null,
})),
}))
},
}
-19
View File
@@ -1,19 +0,0 @@
import { httpFetch } from '../request.ts'
import type { HttpRequestObject } from '../types.ts'
export default {
_requestObj: null as HttpRequestObject | null,
async getList(retryNum = 0) {
if (this._requestObj) this._requestObj.cancelHttp()
if (retryNum > 2) return Promise.reject(new Error('try max num'))
const _requestObj = httpFetch('http://jadeite.migu.cn:7090/music_search/v3/search/hotword')
const { body, statusCode } = await _requestObj.promise
if (statusCode != 200 || body.code !== '000000') throw new Error('获取热搜词失败')
// console.log(body, statusCode)
return { source: 'mg', list: this.filterList(body.data.hotwords[0].hotwordList) }
},
filterList(rawList) {
return rawList.filter(item => item.resourceType == 'song').map(item => item.word)
},
}
-34
View File
@@ -1,34 +0,0 @@
import { apis } from '../api-source.ts'
import leaderboard from './leaderboard.ts'
import songList from './songList.ts'
import musicSearch from './musicSearch.ts'
import pic from './pic.ts'
import lyric from './lyric.ts'
import hotSearch from './hotSearch.ts'
import comment from './comment.ts'
import album from './album.js'
// import tipSearch from './tipSearch.ts'
const mg = {
// tipSearch,
songList,
musicSearch,
leaderboard,
hotSearch,
comment,
album,
getMusicUrl(songInfo, type) {
return apis('mg').getMusicUrl!(songInfo, type)
},
getLyric(songInfo) {
return lyric.getLyric(songInfo)
},
getPic(songInfo) {
return pic.getPic(songInfo)
},
getMusicDetailPageUrl(songInfo) {
return `http://music.migu.cn/v3/music/song/${songInfo.copyrightId}`
},
}
export default mg
-252
View File
@@ -1,252 +0,0 @@
import { httpFetch } from '../request.ts'
import { filterMusicInfoList } from './musicInfo.ts'
import type { Source, HttpRequestObject } from '../types.ts'
// const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]
// const boardList = [
// { id: 'mg__27553319', name: '尖叫新歌榜', bangid: '27553319', webId: 'jianjiao_newsong' },
// { id: 'mg__27186466', name: '尖叫热歌榜', bangid: '27186466', webId: 'jianjiao_hotsong' },
// { id: 'mg__27553408', name: '尖叫原创榜', bangid: '27553408', webId: 'jianjiao_original' },
// { id: 'mg__23189800', name: '港台榜', bangid: '23189800', webId: 'hktw' },
// { id: 'mg__23189399', name: '内地榜', bangid: '23189399', webId: 'mainland' },
// { id: 'mg__19190036', name: '欧美榜', bangid: '19190036', webId: 'eur_usa' },
// { id: 'mg__23189813', name: '日韩榜', bangid: '23189813', webId: 'jpn_kor' },
// { id: 'mg__23190126', name: '彩铃榜', bangid: '23190126', webId: 'coloring' },
// { id: 'mg__15140045', name: 'KTV榜', bangid: '15140045', webId: 'ktv' },
// { id: 'mg__15140034', name: '网络榜', bangid: '15140034', webId: 'network' },
// // { id: 'mg__21958042', name: '美国iTunes榜', bangid: '21958042', webId: 'itunes' },
// // { id: 'mg__21975570', name: '美国billboard榜', bangid: '21975570', webId: 'billboard' },
// // { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815', webId: 'hito' },
// // { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943', webId: 'mnet' },
// // { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437', webId: 'uk' },
// ]
interface BoardItem {
id: string
name: string
bangid: string
source: Source
// 旧数据字段,部分条目缺失(缺失时 getDetailPageUrl 拼出 undefined,与原实现一致)
webId?: string
}
const boardList: BoardItem[] = [
{
id: 'mg__27553319',
name: '新歌榜',
bangid: '27553319',
source: 'mg',
},
{
id: 'mg__27186466',
name: '热歌榜',
bangid: '27186466',
source: 'mg',
},
{
id: 'mg__27553408',
name: '原创榜',
bangid: '27553408',
source: 'mg',
},
{
id: 'mg__75959118',
name: '音乐风向榜',
bangid: '75959118',
source: 'mg',
},
{
id: 'mg__76557036',
name: '彩铃分贝榜',
bangid: '76557036',
source: 'mg',
},
{
id: 'mg__76557745',
name: '会员臻爱榜',
bangid: '76557745',
source: 'mg',
},
{
id: 'mg__23189800',
name: '港台榜',
bangid: '23189800',
source: 'mg',
},
{
id: 'mg__23189399',
name: '内地榜',
bangid: '23189399',
source: 'mg',
},
{
id: 'mg__19190036',
name: '欧美榜',
bangid: '19190036',
source: 'mg',
},
{
id: 'mg__83176390',
name: '国风金曲榜',
bangid: '83176390',
source: 'mg',
},
]
export default {
limit: 200,
list: [
{
id: 'mgyyb',
name: '音乐榜',
bangid: '27553319',
},
{
id: 'mgysb',
name: '影视榜',
bangid: '23603721',
},
{
id: 'mghybnd',
name: '华语内地榜',
bangid: '23603926',
},
{
id: 'mghyjqbgt',
name: '华语港台榜',
bangid: '23603954',
},
{
id: 'mgomb',
name: '欧美榜',
bangid: '23603974',
},
{
id: 'mgrhb',
name: '日韩榜',
bangid: '23603982',
},
{
id: 'mgwlb',
name: '网络榜',
bangid: '23604058',
},
{
id: 'mgclb',
name: '彩铃榜',
bangid: '23604023',
},
{
id: 'mgktvb',
name: 'KTV榜',
bangid: '23604040',
},
{
id: 'mgrcb',
name: '原创榜',
bangid: '23604032',
},
],
getUrl(id, page) {
return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/querycontentbyId.do?columnId=${id}&needAll=0`
// return `http://m.music.migu.cn/migu/remoting/cms_list_tag?nid=${id}&pageSize=${this.limit}&pageNo=${page - 1}`
},
successCode: '000000',
requestBoardsObj: null as HttpRequestObject | null,
getBoardsData() {
if (this.requestBoardsObj) (this as any)._requestBoardsObj.cancelHttp()
this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/pc/bmw/rank/rank-index/v1.0', {
// this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/rank-list/release', {
// this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/indexrank.do?templateVersion=8', {
headers: {
Referer: 'https://app.c.nf.migu.cn/',
'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36',
channel: '0146921',
},
})
return this.requestBoardsObj!.promise
},
getData(url) {
const requestObj = httpFetch(url)
return requestObj.promise
},
// filterBoardsData(listData, list = [], ids = new Set()) {
// for (const item of listData) {
// if (item.rankId && !ids.has(item.rankId)) {
// ids.add(item.rankId)
// list.push({
// id: 'mg__' + item.rankId,
// name: item.rankName,
// bangid: String(item.rankId),
// source: 'mg',
// })
// } else if (item.contents) this.filterBoardsData(item.contents, list, ids)
// }
// return list
// },
// filterBoardsData(rawList) {
// // console.log(rawList)
// let list = []
// for (const board of rawList) {
// if (board.template != 'group1') continue
// for (const item of board.itemList) {
// if ((item.template != 'row1' && item.template != 'grid1' && !item.actionUrl) || !item.actionUrl.includes('rank-info')) continue
// let data = item.displayLogId.param
// list.push({
// id: 'mg__' + data.rankId,
// name: data.rankName,
// bangid: String(data.rankId),
// })
// }
// }
// return list
// },
async getBoards(retryNum = 0) {
// if (++retryNum > 3) return Promise.reject(new Error('try max num'))
// let response
// try {
// response = await this.getBoardsData()
// } catch (error) {
// return this.getBoards(retryNum)
// }
// // console.log(response.body.data.contentItemList)
// if (response.statusCode !== 200 || response.body.code !== this.successCode) return this.getBoards(retryNum)
// const list = this.filterBoardsData(response.body.data.contents)
// console.log(list)
// // console.log(JSON.stringify(list))
// this.list = list
// return {
// list,
// source: 'mg',
// }
this.list = boardList
return {
list: boardList,
source: 'mg',
}
},
getList(bangid, page, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
return this.getData(this.getUrl(bangid, page)).then(({ statusCode, body }) => {
// console.log(body)
if (statusCode !== 200 || body.code !== this.successCode) return this.getList(bangid, page, retryNum)
const list = filterMusicInfoList(body.columnInfo.contents.map(m => m.objectInfo))
return {
total: list.length,
list,
limit: this.limit,
page,
source: 'mg',
}
})
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('mg__', '')
for (const item of boardList) {
if (item.bangid == id) {
return `https://music.migu.cn/v3/music/top/${item.webId}`
}
}
return null
},
}
-104
View File
@@ -1,104 +0,0 @@
import { httpFetch } from '../request.ts'
import { getMusicInfo } from './musicInfo.ts'
import { decrypt } from './utils/mrc.ts'
const mrcTools = {
rxps: {
lineTime: /^\s*\[(\d+),\d+\]/,
wordTime: /\(\d+,\d+\)/,
wordTimeAll: /(\(\d+,\d+\))/g,
},
parseLyric(str) {
str = str.replace(/\r/g, '')
const lines = str.split('\n')
const yrcLines: string[] = []
const lrcLines: string[] = []
for (const line of lines) {
if (line.length < 6) continue
let result = this.rxps.lineTime.exec(line)
if (!result) continue
const startTime = parseInt(result[1])
let time: any = startTime
let ms = time % 1000
time /= 1000
let m = parseInt((time / 60) as unknown as string).toString().padStart(2, '0')
time %= 60
let s = parseInt(time as unknown as string).toString().padStart(2, '0')
time = `${m}:${s}.${ms}`
let words = line.replace(this.rxps.lineTime, '')
lrcLines.push(`[${time}]${words.replace(this.rxps.wordTimeAll, '')}`)
let times = words.match(this.rxps.wordTimeAll)
if (!times) continue
times = times.map(time => {
const result = /\((\d+),(\d+)\)/.exec(time)
return `<${parseInt(result![1]) - startTime},${result![2]}>`
})
const wordArr = words.split(this.rxps.wordTime)
const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('')
yrcLines.push(`[${time}]${newWords}`)
}
return {
lyric: lrcLines.join('\n'),
yrc: yrcLines.join('\n'),
}
},
getText(url, tryNum = 0) {
const requestObj = httpFetch(url, {
headers: {
Referer: 'https://app.c.nf.migu.cn/',
'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36',
channel: '0146921',
},
})
return requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 200) return body
if (tryNum > 5 || statusCode == 404) return Promise.reject(new Error('歌词获取失败'))
return this.getText(url, ++tryNum)
})
},
getMrc(url) {
return this.getText(url).then(text => {
return this.parseLyric(decrypt(text))
})
},
getLrc(url) {
return this.getText(url).then(text => ({ yrc: '', lyric: text }))
},
getTrc(url) {
if (!url) return Promise.resolve('')
return this.getText(url)
},
async getMusicInfo(songInfo) {
return songInfo.mrcUrl == null
? getMusicInfo(songInfo.songmid || songInfo.copyrightId)
: songInfo
},
getLyric(songInfo) {
return {
promise: this.getMusicInfo(songInfo).then(info => {
let p
if (info.mrcUrl) p = this.getMrc(info.mrcUrl)
else if (info.lrcUrl) p = this.getLrc(info.lrcUrl)
else if (info.lyricUrl) p = this.getLrc(info.lyricUrl)
if (p == null) return Promise.reject(new Error('获取歌词失败'))
return Promise.all([p, this.getTrc(info.trcUrl)]).then(([lrcInfo, tlyric]) => {
lrcInfo.tlyric = tlyric
return lrcInfo
})
}),
cancelHttp() {},
}
},
}
export default {
getLyric(songInfo) {
let requestObj = mrcTools.getLyric(songInfo)
return requestObj
},
}
-163
View File
@@ -1,163 +0,0 @@
import { sizeFormate, formatPlayTime } from '../index.ts'
import { createHttpFetch } from './utils/index.ts'
import { formatSingerName } from '../utils.ts'
import type { SongInfo, SongType, QualityInfo, Quality } from '../types.ts'
const createGetMusicInfosTask = (ids: Array<string | undefined>) => {
let list = ids
const tasks: Array<Array<string | undefined>> = []
while (list.length) {
tasks.push(list.slice(0, 100))
if (list.length < 100) break
list = list.slice(100)
}
let url = 'https://c.musicapp.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceType=2'
return Promise.all(tasks.map(task => createHttpFetch(url, {
method: 'POST',
form: {
resourceId: task.join('|'),
},
}).then(data => data.resource)))
}
export const filterMusicInfoList = (rawList: any[]): SongInfo[] => {
// console.log(rawList)
let ids = new Set()
const list: any[] = []
rawList.forEach(item => {
if (!item.songId || ids.has(item.songId)) return
ids.add(item.songId)
const types: SongType[] = []
const _types: Partial<Record<Quality, QualityInfo>> = {}
item.newRateFormats?.forEach(type => {
let size
switch (type.formatType) {
case 'PQ':
size = sizeFormate(type.size ?? type.androidSize)
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
break
case 'HQ':
size = sizeFormate(type.size ?? type.androidSize)
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
break
case 'SQ':
size = sizeFormate(type.size ?? type.androidSize)
types.push({ type: 'flac', size })
_types.flac = {
size,
}
break
case 'ZQ':
size = sizeFormate(type.size ?? type.androidSize)
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
break
}
})
const intervalTest = /(\d\d:\d\d)$/.test(item.length)
list.push({
singer: formatSingerName(item.artists, 'name'),
name: item.songName,
albumName: item.album,
albumId: item.albumId,
songmid: item.songId,
copyrightId: item.copyrightId,
source: 'mg',
interval: intervalTest ? RegExp.$1 : null,
img: item.albumImgs?.length ? item.albumImgs[0].img : null,
lrc: null,
lrcUrl: item.lrcUrl,
mrcUrl: item.mrcUrl,
trcUrl: item.trcUrl,
otherSource: null,
types,
_types,
typeUrl: {},
})
})
return list
}
export const filterMusicInfoListV5 = (rawList) => {
// console.log(rawList)
let ids = new Set()
const list: any[] = []
rawList.forEach(item => {
if (!item.songId || ids.has(item.songId)) return
ids.add(item.songId)
const types: SongType[] = []
const _types: Partial<Record<Quality, QualityInfo>> = {}
item.audioFormats?.forEach(type => {
let size
switch (type.formatType) {
case 'PQ':
size = sizeFormate(type.size ?? type.androidSize)
types.push({ type: '128k', size })
_types['128k'] = {
size,
}
break
case 'HQ':
size = sizeFormate(type.size ?? type.androidSize)
types.push({ type: '320k', size })
_types['320k'] = {
size,
}
break
case 'SQ':
size = sizeFormate(type.size ?? type.androidSize)
types.push({ type: 'flac', size })
_types.flac = {
size,
}
break
case 'ZQ':
size = sizeFormate(type.size ?? type.androidSize)
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size,
}
break
}
})
list.push({
singer: formatSingerName(item.singerList, 'name'),
name: item.songName,
albumName: item.album,
albumId: item.albumId,
songmid: item.songId,
copyrightId: item.copyrightId,
source: 'mg',
interval: formatPlayTime(item.duration),
img: item.img3 || item.img2 || item.img1 || null,
lrc: null,
lrcUrl: item.lrcUrl,
mrcUrl: item.mrcUrl,
trcUrl: item.trcUrl,
otherSource: null,
types,
_types,
typeUrl: {},
})
})
return list
}
export const getMusicInfo = async(copyrightId?: string): Promise<SongInfo | undefined> => {
return getMusicInfos([copyrightId]).then(data => data[0])
}
export const getMusicInfos = async(copyrightIds: Array<string | undefined>): Promise<SongInfo[]> => {
return filterMusicInfoList((await createGetMusicInfosTask(copyrightIds)).flat())
}
-224
View File
@@ -1,224 +0,0 @@
import { httpFetch } from '../request.ts'
import { sizeFormate, formatPlayTime } from '../index.ts'
import { toMD5, formatSingerName } from '../utils.ts'
import type { SongType, QualityInfo, Quality } from '../types.ts'
export const createSignature = (time: string, str: string): { sign: string, deviceId: string } => {
const deviceId = '963B7AA0D21511ED807EE5846EC87D20'
const signatureMd5 = '6cdc72a439cef99a3418d2a78aa28c73'
const sign = toMD5(`${str}${signatureMd5}yyapp2d16148780a1dcc7408e06336b98cfd50${deviceId}${time}`)
return { sign, deviceId }
}
export default {
limit: 20,
total: 0,
page: 0,
allPage: 1,
// 旧版API
// musicSearch(str, page, limit) {
// const searchRequest = httpFetch(`http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=${encodeURIComponent(str)}&pageNo=${page}&pageSize=${limit}&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22songlist%22%3A0%2C%22bestShow%22%3A1%7D`, {
// searchRequest = httpFetch(`http://pd.musicapp.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=${encodeURIComponent(str)}&pageNo=${page}&pageSize=${limit}&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22songlist%22%3A0%2C%22bestShow%22%3A1%7D`, {
// searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, {
// searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`)
// // searchRequest = httpFetch(`http://jadeite.migu.cn:7090/music_search/v2/search/searchAll?sid=4f87090d01c84984a11976b828e2b02c18946be88a6b4c47bcdc92fbd40762db&isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0`, {
// headers: {
// // sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
// // timestamp: 1578225871982,
// // appId: 'yyapp2',
// // mode: 'android',
// // ua: 'Android_migu',
// // version: '6.9.4',
// osVersion: 'android 7.0',
// 'User-Agent': 'okhttp/3.9.1',
// },
// })
// // searchRequest = httpFetch(`https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?isCopyright=1&isCorrect=1&pageNo=${page}&pageSize=${limit}&searchSwitch={%22song%22:1,%22album%22:0,%22singer%22:0,%22tagSong%22:0,%22mvSong%22:0,%22songlist%22:0,%22bestShow%22:0}&sort=0&text=${encodeURIComponent(str)}`)
// return searchRequest.promise.then(({ body }) => body)
// },
// handleResult(rawData) {
// // console.log(rawData)
// let ids = new Set()
// const list = []
// rawData.forEach(item => {
// if (ids.has(item.id)) return
// ids.add(item.id)
// const types = []
// const _types = {}
// item.newRateFormats && item.newRateFormats.forEach(type => {
// let size
// switch (type.formatType) {
// case 'PQ':
// size = sizeFormate(type.size ?? type.androidSize)
// types.push({ type: '128k', size })
// _types['128k'] = {
// size,
// }
// break
// case 'HQ':
// size = sizeFormate(type.size ?? type.androidSize)
// types.push({ type: '320k', size })
// _types['320k'] = {
// size,
// }
// break
// case 'SQ':
// size = sizeFormate(type.size ?? type.androidSize)
// types.push({ type: 'flac', size })
// _types.flac = {
// size,
// }
// break
// case 'ZQ':
// size = sizeFormate(type.size ?? type.androidSize)
// types.push({ type: 'flac24bit', size })
// _types.flac24bit = {
// size,
// }
// break
// }
// })
// const albumNInfo = item.albums && item.albums.length
// ? {
// id: item.albums[0].id,
// name: item.albums[0].name,
// }
// : {}
// list.push({
// singer: this.getSinger(item.singers),
// name: item.name,
// albumName: albumNInfo.name,
// albumId: albumNInfo.id,
// songmid: item.songId,
// copyrightId: item.copyrightId,
// source: 'mg',
// interval: null,
// img: item.imgItems && item.imgItems.length ? item.imgItems[0].img : null,
// lrc: null,
// lrcUrl: item.lyricUrl,
// mrcUrl: item.mrcurl,
// trcUrl: item.trcUrl,
// otherSource: null,
// types,
// _types,
// typeUrl: {},
// })
// })
// return list
// },
musicSearch(str, page, limit) {
const time = Date.now().toString()
const signData = createSignature(time, str)
const searchRequest = httpFetch(`https://jadeite.migu.cn/music_search/v3/search/searchAll?isCorrect=0&isCopyright=1&searchSwitch=%7B%22song%22%3A1%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A1%2C%22mvSong%22%3A0%2C%22bestShow%22%3A1%2C%22songlist%22%3A0%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(str)}&pageNo=${page}&sort=0&sid=USS`, {
headers: {
uiVersion: 'A_music_3.6.1',
deviceId: signData.deviceId,
timestamp: time,
sign: signData.sign,
channel: '0146921',
'User-Agent': 'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
},
})
return searchRequest.promise.then(({ body }) => body)
},
filterData(rawData) {
// console.log(rawData)
const list: any[] = []
const ids = new Set()
rawData.forEach(item => {
item.forEach(data => {
if (!data.songId || !data.copyrightId || ids.has(data.copyrightId)) return
ids.add(data.copyrightId)
const types: SongType[] = []
const _types: Partial<Record<Quality, QualityInfo>> = {}
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
@@ -1,28 +0,0 @@
import { httpFetch } from '../request.ts'
import getSongId from './songId.ts'
export default {
async getPicUrl(songId, tryNum = 0) {
let requestObj = httpFetch(`http://music.migu.cn/v3/api/music/audioPlayer/getSongPic?songId=${songId}`, {
headers: {
Referer: 'http://music.migu.cn/v3/music/player/audio?from=migu',
},
})
requestObj.promise.then(({ body }) => {
if (body.returnCode !== '000000') {
if (tryNum > 5) return Promise.reject(new Error('图片获取失败'))
let tryRequestObj = (this as any).getPic(songId, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise
}
let url = body.largePic || body.mediumPic || body.smallPic
if (!/https?:/.test(url)) url = 'http:' + url
return url
})
return requestObj
},
async getPic(songInfo) {
const songId = await getSongId(songInfo)
return this.getPicUrl(songId)
},
}
-32
View File
@@ -1,32 +0,0 @@
// import { httpFetch } from '../request.js'
import { getMusicInfo } from './musicInfo.ts'
import type { SongInfo } from '../types.ts'
const getSongId = async(mInfo: SongInfo): Promise<string | undefined> => {
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
-363
View File
@@ -1,363 +0,0 @@
import { httpFetch } from '../request.ts'
import { formatPlayCount } from '../index.ts'
import { filterMusicInfoListV5 } from './musicInfo.ts'
import { createSignature } from './musicSearch.ts'
import { createHttpFetch } from './utils/index.ts'
import type { HttpRequestObject } from '../types.ts'
// const tagData = { code: '000000', info: 'SUCCESS', columnInfo: { columnTitle: '分类', columnId: '15244430', columnPid: '15031270', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 6, columnStatus: 1, columnCreateTime: '2016-11-10 10:53:05.077', columntype: 2011, contents: [{ contentId: '18464615', relationType: 2011, objectInfo: { columnTitle: '热门', columnId: '18464615', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 8, columnStatus: 1, columnCreateTime: '2017-02-20 16:09:13.400', columntype: 2011, contents: [{ contentId: '1000001672', relationType: 4034, objectInfo: { tagId: '1000001672', tagName: '流行', resourceType: '2034' }, relationSort: 9 }, { contentId: '1003449727', relationType: 4034, objectInfo: { tagId: '1003449727', tagName: '厂牌', resourceType: '2034' }, relationSort: 8 }, { contentId: '1000001795', relationType: 4034, objectInfo: { tagId: '1000001795', tagName: '伤感', resourceType: '2034' }, relationSort: 7 }, { contentId: '1001076080', relationType: 4034, objectInfo: { tagId: '1001076080', tagName: '电影', resourceType: '2034' }, relationSort: 6 }, { contentId: '1000001675', relationType: 4034, objectInfo: { tagId: '1000001675', tagName: '中国风', resourceType: '2034' }, relationSort: 5 }, { contentId: '1000001635', relationType: 4034, objectInfo: { tagId: '1000001635', tagName: '经典老歌', resourceType: '2034' }, relationSort: 4 }, { contentId: '1000001831', relationType: 4034, objectInfo: { tagId: '1000001831', tagName: '翻唱', resourceType: '2034' }, relationSort: 3 }, { contentId: '1000001762', relationType: 4034, objectInfo: { tagId: '1000001762', tagName: '国语', resourceType: '2034' }, relationSort: 1 }], dataVersion: '1620410266029', customizedPicUrls: [] }, relationSort: 6 }, { contentId: '15244503', relationType: 2011, objectInfo: { columnTitle: '主题', columnId: '15244503', columnPid: '15244430', opNumItem: { playNum: 0, playNumDesc: '0', keepNum: 0, keepNumDesc: '0', commentNum: 0, commentNumDesc: '0', shareNum: 0, shareNumDesc: '0', orderNumByWeek: 0, orderNumByWeekDesc: '0', orderNumByTotal: 0, orderNumByTotalDesc: '0', thumbNum: 0, thumbNumDesc: '0', followNum: 0, followNumDesc: '0', subscribeNum: 0, subscribeNumDesc: '0', livePlayNum: 0, livePlayNumDesc: '0', popularNum: 0, popularNumDesc: '0', bookingNum: 0, bookingNumDesc: '0' }, contentsCount: 23, columnStatus: 1, columnCreateTime: '2016-11-10 10:54:10.261', columntype: 2011, contents: [{ contentId: '1003449727', relationType: 4034, objectInfo: { tagId: '1003449727', tagName: '厂牌', resourceType: '2034' }, relationSort: 29 }, { contentId: '1001076080', relationType: 4034, objectInfo: { tagId: '1001076080', tagName: '电影', resourceType: '2034' }, relationSort: 28 }, { contentId: '1001076078', relationType: 4034, objectInfo: { tagId: '1001076078', tagName: '电视剧', resourceType: '2034' }, relationSort: 27 }, { contentId: '1001076083', relationType: 4034, objectInfo: { tagId: '1001076083', tagName: '综艺', resourceType: '2034' }, relationSort: 26 }, { contentId: '1000001827', relationType: 4034, objectInfo: { tagId: '1000001827', tagName: 'KTV', resourceType: '2034' }, relationSort: 23 }, { contentId: '1000001698', relationType: 4034, objectInfo: { tagId: '1000001698', tagName: '爱情', resourceType: '2034' }, relationSort: 22 }, { contentId: '1000001635', relationType: 4034, objectInfo: { tagId: '1000001635', tagName: '经典老歌', resourceType: '2034' }, relationSort: 21 }, { contentId: '1001076096', relationType: 4034, objectInfo: { tagId: '1001076096', tagName: '网络热歌', resourceType: '2034' }, relationSort: 20 }, { contentId: '1000001780', relationType: 4034, objectInfo: { tagId: '1000001780', tagName: '儿童歌曲', resourceType: '2034' }, relationSort: 19 }, { contentId: '1000587702', relationType: 4034, objectInfo: { tagId: '1000587702', tagName: '广场舞', resourceType: '2034' }, relationSort: 18 }, { contentId: '1000587717', relationType: 4034, objectInfo: { tagId: '1000587717', tagName: '70后', resourceType: '2034' }, relationSort: 17 }, { contentId: '1000587718', relationType: 4034, objectInfo: { tagId: '1000587718', t Line truncated
export default {
_requestObj_tags: null as HttpRequestObject | null,
_requestObj_list: null as HttpRequestObject | null,
limit_list: 30,
limit_song: 50,
successCode: '000000',
cachedDetailInfo: {},
cachedUrl: {},
sortList: [
{
name: '推荐',
id: '15127315',
// id: '1',
},
// {
// name: '最新',
// id: '15127272',
// // id: '2',
// },
],
regExps: {
list: /<li><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: any[] = [], ids = new Set()) {
for (const item of listData) {
if (item.contents) this.filterList2(item.contents, list, ids)
else if (item.resType == '2021' && !ids.has(item.resId)) {
ids.add(item.resId)
list.push({
id: String(item.resId),
author: '',
name: item.txt,
// time: dateFormat(item.createTime, 'Y-M-D'),
img: item.img,
// grade: item.grade,
// total: item.contentCount,
desc: item.txt2,
source: 'mg',
})
}
}
return list
},
filterList(rawData) {
// console.log(rawData)
return rawData.map(item => ({
play_count: item.barList[0]?.title,
id: String(item.logEvent.contentId),
author: '',
name: item.title,
// time: dateFormat(item.createTime, 'Y-M-D'),
img: item.imageUrl,
// grade: item.grade,
// total: item.contentCount,
desc: '',
source: 'mg',
}))
},
// 获取标签
getTag(tryNum = 0) {
if (this._requestObj_tags) this._requestObj_tags.cancelHttp()
if (tryNum > 2) return Promise.reject(new Error('try max num'))
this._requestObj_tags = httpFetch(this.tagsUrl, { headers: this.defaultHeaders })
return this._requestObj_tags!.promise.then(({ body }) => {
if (body.code !== this.successCode) return this.getTag(++tryNum)
// console.log(body)
return this.filterTagInfo(body.data)
})
// return Promise.resolve(this.filterTagInfo(tagData.columnInfo.contents))
},
filterTagInfo(rawList) {
return {
hotTag: rawList[0].content.map(({ texts: [name, id] }) => ({
id,
name,
source: 'mg',
})),
tags: rawList.slice(1).map(({ header, content }) => ({
name: header.title,
list: content.map(({ texts: [name, id] }) => ({
// parent_id: objectInfo.columnId,
// parent_name: objectInfo.columnTitle,
id,
name,
source: 'mg',
})),
})),
source: 'mg',
}
// return {
// hotTag: rawList[0].objectInfo.contents.map(item => ({
// id: item.objectInfo.tagId,
// name: item.objectInfo.tagName,
// source: 'mg',
// })),
// tags: rawList.slice(1).map(({ objectInfo }) => ({
// name: objectInfo.columnTitle,
// list: objectInfo.contents.map(item => ({
// parent_id: objectInfo.columnId,
// parent_name: objectInfo.columnTitle,
// id: item.objectInfo.tagId,
// name: item.objectInfo.tagName,
// source: 'mg',
// })),
// })),
// source: 'mg',
// }
},
getTags() {
return this.getTag()
},
getDetailPageUrl(id) {
if (/playlist\/index\.html\?/.test(id)) {
id = id.replace(/.*(?:\?|&)id=(\d+)(?:&.*|$)/, '$1')
} else if (this.regExps.listDetailLink.test(id)) {
id = id.replace(this.regExps.listDetailLink, '$1')
}
return `https://music.migu.cn/v3/music/playlist/${id}`
},
filterSongListResult(raw) {
const list: any[] = []
raw.forEach(item => {
if (!item.id) return
const playCount = parseInt(item.playNum)
list.push({
play_count: isNaN(playCount) ? 0 : formatPlayCount(playCount),
id: item.id,
author: item.userName,
name: item.name,
img: item.musicListPicUrl,
total: item.musicNum,
source: 'mg',
})
})
return list
},
search(text, page, limit = 20) {
const timeStr = Date.now().toString()
const signResult = createSignature(timeStr, text)
return createHttpFetch(`https://jadeite.migu.cn/music_search/v3/search/searchAll?isCorrect=1&isCopyright=1&searchSwitch=%7B%22song%22%3A0%2C%22album%22%3A0%2C%22singer%22%3A0%2C%22tagSong%22%3A0%2C%22mvSong%22%3A0%2C%22bestShow%22%3A0%2C%22songlist%22%3A1%2C%22lyricSong%22%3A0%7D&pageSize=${limit}&text=${encodeURIComponent(text)}&pageNo=${page}&sort=0&sid=USS`, {
headers: {
uiVersion: 'A_music_3.6.1',
deviceId: signResult.deviceId,
timestamp: timeStr,
sign: signResult.sign,
channel: '0146921',
'User-Agent': 'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
},
}).then(body => {
if (!body.songListResultData) throw new Error('get song list faild.')
const list = this.filterSongListResult(body.songListResultData.result)
return {
list,
limit,
total: parseInt(body.songListResultData.totalCount),
source: 'mg',
}
})
},
}
// getList
// getTags
// getListDetail
-25
View File
@@ -1,25 +0,0 @@
import { createHttpFetch } from './utils/index.ts'
export default {
requestObj: null as any,
cancelTipSearch() {
if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()
},
tipSearchBySong(str) {
this.cancelTipSearch()
this.requestObj = createHttpFetch(`https://music.migu.cn/v3/api/search/suggest?keyword=${encodeURIComponent(str)}`, {
headers: {
referer: 'https://music.migu.cn/v3',
},
})
return this.requestObj.then(body => {
return body.songs
})
},
handleResult(rawData) {
return rawData.map(info => `${info.name} - ${info.singerName}`)
},
async search(str) {
return this.tipSearchBySong(str).then(result => this.handleResult(result))
},
}
-30
View File
@@ -1,30 +0,0 @@
import { httpFetch } from '../../request.ts'
import type { HttpOptions } from '../../types.ts'
/**
* 创建一个适用于MG的Http请求
* @param {*} url
* @param {*} options
* @param {*} retryNum
*/
export const createHttpFetch = async(url: string, options?: HttpOptions, retryNum = 0): Promise<any> => {
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
@@ -1,104 +0,0 @@
// const key = 'karakal@123Qcomyidongtiantianhaoting'
const DELTA = 2654435769n
const MIN_LENGTH = 32
// const SPECIAL_CHAR = '0'
const keyArr = [
27303562373562475n,
18014862372307051n,
22799692160172081n,
34058940340699235n,
30962724186095721n,
27303523720101991n,
27303523720101998n,
31244139033526382n,
28992395054481524n,
]
const teaDecrypt = (data: bigint[], key: bigint[]): bigint[] => {
const length = data.length
const lengthBitint = BigInt(length)
if (length >= 1) {
// let j = data[data.length - 1];
let j2 = data[0]
let j3 = toLong((6n + (52n / lengthBitint)) * DELTA)
while (true) {
let j4 = j3
if (j4 == 0n) break
let j5 = toLong(3n & toLong(j4 >> 2n))
let j6 = lengthBitint
while (true) {
j6--
if (j6 > 0n) {
let j7 = data[(j6 - 1n) as unknown as number]
let i = j6 as unknown as number
j2 = toLong(data[i] - (toLong(toLong(j2 ^ j4) + toLong(j7 ^ key[toLong(toLong(3n & j6) ^ j5) as unknown as number])) ^ toLong(toLong(toLong(j7 >> 5n) ^ toLong(j2 << 2n)) + toLong(toLong(j2 >> 3n) ^ toLong(j7 << 4n)))))
data[i] = j2
} else break
}
let j8 = data[lengthBitint - 1n as unknown as number]
j2 = toLong(data[0n as unknown as number] - toLong(toLong(toLong(key[toLong(toLong(j6 & 3n) ^ j5) as unknown as number] ^ j8) + toLong(j2 ^ j4)) ^ toLong(toLong(toLong(j8 >> 5n) ^ toLong(j2 << 2n)) + toLong(toLong(j2 >> 3n) ^ toLong(j8 << 4n)))))
data[0] = j2
j3 = toLong(j4 - DELTA)
}
}
return data
}
const longArrToString = (data: bigint[]): string => {
const arrayList: string[] = []
for (const j of data) arrayList.push(longToBytes(j).toString('utf16le'))
return arrayList.join('')
}
// https://stackoverflow.com/a/29132118
const longToBytes = (l: bigint): Buffer => {
const result = Buffer.alloc(8)
for (let i = 0; i < 8; i++) {
result[i] = parseInt((l & 0xFFn) as unknown as string)
l >>= 8n
}
return result
}
const toBigintArray = (data: string): bigint[] => {
const length = Math.floor(data.length / 16)
const jArr: bigint[] = Array(length)
for (let i = 0; i < length; i++) {
jArr[i] = toLong(data.substring(i * 16, (i * 16) + 16))
}
return jArr
}
// 基于社区通用的 mrc 逐字歌词解析思路
const MAX = 9223372036854775807n
const MIN = -9223372036854775808n
const toLong = (str: string | bigint): bigint => {
const num = typeof str == 'string' ? BigInt('0x' + str) : str
if (num > MAX) return toLong(num - (1n << 64n))
else if (num < MIN) return toLong(num + (1n << 64n))
return num
}
export const decrypt = (data: string | null): string | null => {
// console.log(data.length)
// -3551594764563790630
// console.log(toLongArrayFromArr(Buffer.from(key)))
// console.log(teaDecrypt(toBigintArray(data), keyArr))
// console.log(longArrToString(teaDecrypt(toBigintArray(data), keyArr)))
// console.log(toByteArray(teaDecrypt(toBigintArray(data), keyArr)))
return (data == null || data.length < MIN_LENGTH)
? data
: longArrToString(teaDecrypt(toBigintArray(data), keyArr))
}
// console.log(14895149309145760986n - )
// console.log(toLong('14895149309145760986'))
// console.log(decrypt(str))
// console.log(decrypt(str))
// console.log(toByteArray([6048138644744000495n]))
// console.log(toByteArray([16325999628386395n]))
// console.log(toLong(90994076459972177136n))
-9
View File
@@ -1,9 +0,0 @@
// SDK 公共请求常量
export const bHh = '624868746c'
export const headers: Record<string, string | string[]> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
[bHh]: [bHh],
}
export const timeout = 15000
-8
View File
@@ -1,8 +0,0 @@
// SDK 根请求模块(转接至 src/common/request.js)
// musicSdk 内 `import { ... } from '../request.js'` 的解析目标
import { httpFetch as rawHttpFetch } from '../src/common/request.js'
import type { HttpRequestObject, HttpOptions } from './types.ts'
export const httpFetch: (url: string, options?: HttpOptions) => HttpRequestObject =
rawHttpFetch as (url: string, options?: HttpOptions) => HttpRequestObject
-29
View File
@@ -1,29 +0,0 @@
import { httpFetch } from '../request.js'
import { requestMsg } from '../message.js'
import { headers, timeout } from '../options.js'
import { dnsLookup } from '../utils.js'
const api_messoer = {
getMusicUrl(songInfo, type) {
const requestObj = httpFetch(`http://ts.tempmusics.tk/url/tx/${songInfo.songmid}/${type}`, {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
default: return Promise.reject(new Error(requestMsg.fail))
}
})
return requestObj
},
getPic(songInfo) {
return Promise.resolve(`https://y.gtimg.cn/music/photo_new/T002R500x500M000${songInfo.albumId}.jpg`)
},
}
export default api_messoer
-50
View File
@@ -1,50 +0,0 @@
import { httpFetch } from '../request.js'
import { requestMsg } from '../message.js'
import { headers, timeout } from '../options.js'
import { dnsLookup } from '../utils.js'
const api_test = {
getMusicUrl(songInfo, type) {
const requestObj = httpFetch(`http://ts.tempmusics.tk/url/wy/${songInfo.songmid}/${type}`, {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {
if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))
switch (body.code) {
case 0: return Promise.resolve({ type, url: body.data })
default: return Promise.reject(new Error(requestMsg.fail))
}
})
return requestObj
},
/* getPic(songInfo) {
const requestObj = httpFetch(`http://localhost:3100/pic/wy/${songInfo.songmid}`, {
method: 'get',
timeout,
headers,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail))
})
return requestObj
},
getLyric(songInfo) {
const requestObj = httpFetch(`http://localhost:3100/lrc/wy/${songInfo.songmid}`, {
method: 'get',
timeout,
headers,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {
return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail))
})
return requestObj
}, */
}
export default api_test
+7 -4
View File
@@ -3,12 +3,12 @@
"version": "1.0.0", "version": "1.0.0",
"description": "多平台音乐聚合服务器", "description": "多平台音乐聚合服务器",
"type": "module", "type": "module",
"main": "src/index.js", "main": "sdk/index.ts",
"packageManager": "pnpm@12.4.1", "packageManager": "pnpm@12.4.1",
"scripts": { "scripts": {
"build": "tsc", "typecheck": "tsc",
"start": "pnpm run build && node src/server.js", "start": "node src/server.ts",
"test": "pnpm run build && node src/smoke-test.js" "test": "pnpm run typecheck && node test/smoke-test.ts"
}, },
"dependencies": { "dependencies": {
"@fastify/cors": "^11.3.0", "@fastify/cors": "^11.3.0",
@@ -25,5 +25,8 @@
"@types/node": "^26.4.0", "@types/node": "^26.4.0",
"playwright": "^1.62.1", "playwright": "^1.62.1",
"typescript": "^7.0.2" "typescript": "^7.0.2"
},
"engines": {
"node": ">=22.18"
} }
} }
@@ -5,7 +5,7 @@ interface ApiSourceInfo {
id: string id: string
name: string name: string
disabled: boolean disabled: boolean
supportQualitys: Partial<Record<'kw' | 'kg' | 'tx' | 'wy' | 'mg' | 'bd', string[]>> supportQualitys: Partial<Record<'kw' | 'kg' | 'tx' | 'wy', string[]>>
} }
const sources: ApiSourceInfo[] = [ const sources: ApiSourceInfo[] = [
@@ -18,8 +18,6 @@ const sources: ApiSourceInfo[] = [
// kg: ['128k'], // kg: ['128k'],
// tx: ['128k'], // tx: ['128k'],
// wy: ['128k'], // wy: ['128k'],
// mg: ['128k'],
// // bd: ['128k'],
// }, // },
// }, // },
{ {
@@ -31,8 +29,6 @@ const sources: ApiSourceInfo[] = [
kg: ['128k', '320k', 'flac'], kg: ['128k', '320k', 'flac'],
tx: ['128k', '320k', 'flac'], tx: ['128k', '320k', 'flac'],
wy: ['128k', '320k', 'flac'], wy: ['128k', '320k', 'flac'],
mg: ['128k', '320k', 'flac'],
// bd: ['128k', '320k', 'flac'],
}, },
}, },
] ]
+13 -4
View File
@@ -1,8 +1,18 @@
// 接口选择层:按 apiSource 配置把请求路由到外部聚合音源或服务器内置直连接口 // 接口选择层:按 apiSource 配置把请求路由到外部聚合音源或服务器内置直连接口
import apiSourceInfo from './api-source-info.ts' import apiSourceInfo from './api-source-info.ts'
import { apiSource, userApi } from '../src/renderer/store.js' // 接口模式开关(替代原 lx-music 渲染进程 store 的服务器端替身):
import internalApi from '../src/apis/index.js' // 默认 internal 使用内置直连接口;设置 SOURCE_ENABLED(任意非 0/false 值)启用外部聚合音源
import { createApi as createSourceApi } from '../src/apis/source.js' const enabled = process.env.SOURCE_ENABLED
export const apiSource = {
value: enabled && enabled !== '0' && enabled !== 'false' ? 'source' : 'internal',
}
export const userApi = {
apis: {},
}
import internalApi from './apis/index.ts'
import { createApi as createSourceApi } from './apis/source.ts'
interface SourceApi { interface SourceApi {
getMusicUrl(songInfo: any, type: string): { promise: Promise<any>; cancelHttp(): void } getMusicUrl(songInfo: any, type: string): { promise: Promise<any>; cancelHttp(): void }
@@ -14,7 +24,6 @@ const allApi: Record<string, SourceApi> = {
source_kg: createSourceApi('kg'), source_kg: createSourceApi('kg'),
source_tx: createSourceApi('tx'), source_tx: createSourceApi('tx'),
source_wy: createSourceApi('wy'), source_wy: createSourceApi('wy'),
source_mg: createSourceApi('mg'),
} }
const apiList: Record<string, SourceApi | undefined> = {} const apiList: Record<string, SourceApi | undefined> = {}
+6 -9
View File
@@ -1,13 +1,12 @@
// 各平台 URL 直连接口汇总 // 各平台 URL 直连接口汇总
// 服务器环境固定使用内置直连接口(apiSource.value === 'internal') // 服务器环境固定使用内置直连接口(apiSource.value === 'internal')
// 外部聚合音源(sources/ 目录脚本)不走这里,由 musicSdk/api-source.ts 直接接入 source.js // 外部聚合音源(sources/ 目录脚本)不走这里,由 sdk/api-source.ts 直接接入 source.ts
// 注意:index.js 为 tsc 就地发射产物(不入库);改逻辑只改本文件,然后 pnpm build
import kw from './kw.js' import kw from './kw.ts'
import kg from './kg.js' import kg from './kg.ts'
import tx from './tx.js' import tx from './tx.ts'
import wy from './wy.js' import wy from './wy.ts'
import mg from './mg.js'
import bd from './bd.js'
interface SongInfo { interface SongInfo {
songmid?: string songmid?: string
@@ -31,6 +30,4 @@ export default {
kg: kg as ApiModule, kg: kg as ApiModule,
tx: tx as ApiModule, tx: tx as ApiModule,
wy: wy as ApiModule, wy: wy as ApiModule,
mg: mg as ApiModule,
bd: bd as ApiModule,
} }
+5 -5
View File
@@ -6,9 +6,9 @@
// promise resolve → { type, url } // promise resolve → { type, url }
import crypto from 'node:crypto' import crypto from 'node:crypto'
import { httpFetch } from '../common/request.js' import { httpFetch } from '../common/request.ts'
import { getKgAuth } from '../auth/store.js' import { getKgAuth } from '../auth/store.ts'
import { androidSign, getDevice } from '../auth/kg.js' import { androidSign, getDevice } from '../auth/kg.ts'
const md5 = s => crypto.createHash('md5').update(s).digest('hex') const md5 = s => crypto.createHash('md5').update(s).digest('hex')
const SALT_KEY = '57ae12eb6890223e355ccfcb74edf70d' const SALT_KEY = '57ae12eb6890223e355ccfcb74edf70d'
@@ -18,7 +18,7 @@ const CLIENTVER = 11430
const qualityMap = { '128k': '128', '320k': '320', flac: 'flac', flac24bit: 'high' } const qualityMap = { '128k': '128', '320k': '320', flac: 'flac', flac24bit: 'high' }
export const getMusicUrl = (songInfo, type) => { export const getMusicUrl = (songInfo, type) => {
const requestObj = { cancelHttp() {}, promise: null } const requestObj = { cancelHttp() {}, promise: null as any }
const hash = (songInfo._types?.[type]?.hash || songInfo.hash || '').toLowerCase() const hash = (songInfo._types?.[type]?.hash || songInfo.hash || '').toLowerCase()
if (!hash) { if (!hash) {
requestObj.promise = Promise.reject(new Error('缺少 hash')) requestObj.promise = Promise.reject(new Error('缺少 hash'))
@@ -32,7 +32,7 @@ export const getMusicUrl = (songInfo, type) => {
const auth = getKgAuth() const auth = getKgAuth()
const userid = Number(auth?.userid || 0) const userid = Number(auth?.userid || 0)
const params = { const params: Record<string, any> = {
dfid: dev.dfid, mid: dev.mid, uuid: '-', appid: APPID, clientver: CLIENTVER, dfid: dev.dfid, mid: dev.mid, uuid: '-', appid: APPID, clientver: CLIENTVER,
clienttime: Math.floor(Date.now() / 1000), clienttime: Math.floor(Date.now() / 1000),
album_id: Number(songInfo.albumId ?? 0), album_id: Number(songInfo.albumId ?? 0),
+17 -9
View File
@@ -2,14 +2,22 @@
// 统一接口约定:apis('kw').getMusicUrl(songInfo, type) → { promise, cancelHttp } // 统一接口约定:apis('kw').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// promise resolve → { type, url } // promise resolve → { type, url }
// //
// 取流方案: // 取流方案(按序回退):
// 首选 antiserver.kuwo.cn/anti.s(convert_url3 老接口,无需 token,128k/320k 有效) // 1. antiserver.kuwo.cn/anti.s(convert_url3 老接口,无需 token,128k/320k 有效,
// flac 及以上音质该接口返回 refuse,回退 www.kuwo.cn/api/v1/www/music/playUrl // 未登录时部分资源只返回试听片段)
// (需要 csrf token,token 失效时自动重新获取重试一次) // 2. www.kuwo.cn/api/v1/www/music/playUrl(需 csrf token,token 失效自动重取重试一次)
//
// 2026-09-19 实测结论(勿轻易改链):
// - mobi.kuwo.cn/mobi.s(DES 加密 query)已进防盗链蜜罐:
// 任意 rid(含不存在的)都返回 200 与 MP3 地址,但内容是《防盗链打击音频》假文件,
// 且响应无法与真实响应区分,故不接入默认链路;DES 实现可参考 UnblockNeteaseMusic 上游 kwDES。
// - playUrl 对新 token(Hm_Iuvt_* cookie)返回 "The request is illegal!"(要求签名),
// 首页也不再下发 kw_token,getToken 会失败——通道名存实亡,仅作最后回退。
// - flac 及以上音质各匿名通道均要求 VIP 登录态;接入酷我登录 token 后可重估 mobi.s。
import { httpFetch } from '../common/request.js' import { httpFetch } from '../common/request.ts'
// 音质映射:128k → 128kmp3, 320k → 320kmp3, flac → flac(antiserver 用 mp3/flac 格式) // 音质映射:128k → 128kmp3, 320k → 320kmp3, flac → flac(antiserver/mobi.s 用 mp3/flac 格式)
const qualityMap = { const qualityMap = {
'128k': { br: '128kmp3', format: 'mp3' }, '128k': { br: '128kmp3', format: 'mp3' },
'320k': { br: '320kmp3', format: 'mp3' }, '320k': { br: '320kmp3', format: 'mp3' },
@@ -18,7 +26,7 @@ const qualityMap = {
} }
let token = '' let token = ''
let tokenPromise = null let tokenPromise: Promise<string> | null = null
/** /**
* 获取 kw csrf token(访问首页拿 cookie) * 获取 kw csrf token(访问首页拿 cookie)
@@ -31,7 +39,7 @@ const getToken = (retryNum = 0) => {
timeout: 15000, timeout: 15000,
}).promise.then(({ headers }) => { }).promise.then(({ headers }) => {
tokenPromise = null tokenPromise = null
const match = (headers['set-cookie'] || []).join(';').match(/kw_token=(\w+)/) const match = ((headers['set-cookie'] as string[]) || []).join(';').match(/kw_token=(\w+)/)
if (!match) throw new Error('获取token失败') if (!match) throw new Error('获取token失败')
token = match[1] token = match[1]
return token return token
@@ -94,7 +102,7 @@ const getUrlByPlayUrl = (songmid, br, retryNum = 0) => {
} }
export const getMusicUrl = (songInfo, type) => { export const getMusicUrl = (songInfo, type) => {
const requestObj = { cancelHttp() {}, promise: null } const requestObj = { cancelHttp() {}, promise: null as any }
const { br, format } = qualityMap[type] || qualityMap['128k'] const { br, format } = qualityMap[type] || qualityMap['128k']
const rid = `MUSIC_${songInfo.songmid}` const rid = `MUSIC_${songInfo.songmid}`
@@ -15,7 +15,7 @@
// //
// 依赖:isolated-vm(未安装时工厂函数抛出明确错误) // 依赖:isolated-vm(未安装时工厂函数抛出明确错误)
// 用法: // 用法:
// import { createIvmSource } from './source-ivm.js' // import { createIvmSource } from './source-ivm.ts'
// const source = await createIvmSource({ scriptPath }) // scriptPath 必填(sources/ 扫描逻辑见 source.js) // const source = await createIvmSource({ scriptPath }) // scriptPath 必填(sources/ 扫描逻辑见 source.js)
// const source = await createIvmSource({ scriptPath, http }) // http 可注入 mock 做离线测试 // const source = await createIvmSource({ scriptPath, http }) // http 可注入 mock 做离线测试
// await source.kw.getMusicUrl({ songmid: '123' }, '320k').promise // await source.kw.getMusicUrl({ songmid: '123' }, '320k').promise
@@ -29,12 +29,12 @@
import fs from 'node:fs' import fs from 'node:fs'
import crypto from 'node:crypto' import crypto from 'node:crypto'
import zlib from 'node:zlib' import zlib from 'node:zlib'
import { httpFetch } from '../common/request.js' import { httpFetch } from '../common/request.ts'
// 从脚本头部的 SERVER_SCRIPT_CONFIG 提取 apiUrl 声明,作为 HTTP 白名单缺省值。 // 从脚本头部的 SERVER_SCRIPT_CONFIG 提取 apiUrl 声明,作为 HTTP 白名单缺省值。
// 兼容裸键(apiUrl:)与 JSON 风格("apiUrl":),并处理 \/ 转义 // 兼容裸键(apiUrl:)与 JSON 风格("apiUrl":),并处理 \/ 转义
const extractAllowedOrigins = (script) => { const extractAllowedOrigins = (script) => {
const origins = [] const origins: string[] = []
const re = /["']?apiUrl["']?\s*:\s*["'`]([^"'`\s]+)["'`]/g const re = /["']?apiUrl["']?\s*:\s*["'`]([^"'`\s]+)["'`]/g
let m let m
while ((m = re.exec(script))) { while ((m = re.exec(script))) {
@@ -365,7 +365,7 @@ export const createIvmSource = async({
allowedOrigins, allowedOrigins,
httpFilter, httpFilter,
allowAllHttp = false, allowAllHttp = false,
} = {}) => { }: Record<string, any> = {}) => {
let ivm let ivm
try { try {
ivm = (await import('isolated-vm')).default ivm = (await import('isolated-vm')).default
@@ -398,7 +398,7 @@ export const createIvmSource = async({
const jail = context.global const jail = context.global
// ---- Node 侧状态 ---- // ---- Node 侧状态 ----
const state = { const state: Record<string, any> = {
inited: null, inited: null,
updateAlerts: [], updateAlerts: [],
onRegistered: null, onRegistered: null,
@@ -425,7 +425,7 @@ export const createIvmSource = async({
try { try {
fnRef.apply(undefined, args) fnRef.apply(undefined, args)
} catch (err) { } catch (err) {
log(`isolate 调用失败: ${err?.message}`) log(`isolate 调用失败: ${(err as any)?.message}`)
} }
} }
@@ -494,7 +494,7 @@ export const createIvmSource = async({
} }
log(`→ source.request ${method} ${brief(url, 120)}`) log(`→ source.request ${method} ${brief(url, 120)}`)
const startedAt = Date.now() const startedAt = Date.now()
const options = { const options: Record<string, any> = {
method: opts.method || 'get', method: opts.method || 'get',
timeout: typeof opts.timeout === 'number' && opts.timeout > 0 ? opts.timeout : 15000, timeout: typeof opts.timeout === 'number' && opts.timeout > 0 ? opts.timeout : 15000,
} }
@@ -544,8 +544,8 @@ export const createIvmSource = async({
await context.eval(script, { async: true, timeout: 15000 }) await context.eval(script, { async: true, timeout: 15000 })
// ---- 对外 dispatch ---- // ---- 对外 dispatch ----
const dispatch = (payload) => { const dispatch = (payload: any): { id: number; promise: Promise<any> } => {
if (disposed) return Promise.reject(new Error('isolate 已销毁')) if (disposed) return Promise.reject(new Error('isolate 已销毁')) as any
const id = ++dispatchSeq const id = ++dispatchSeq
return { return {
id, id,
@@ -561,7 +561,7 @@ export const createIvmSource = async({
} }
const getMusicUrlFor = (source) => (songInfo, type) => { const getMusicUrlFor = (source) => (songInfo, type) => {
const requestObj = { cancelHttp() {}, promise: null } const requestObj = { cancelHttp() {}, promise: null as any }
const id = songInfo?.hash || songInfo?.songmid || '?' const id = songInfo?.hash || songInfo?.songmid || '?'
const dispatched = dispatch({ source, action: 'musicUrl', info: { type, musicInfo: songInfo } }) const dispatched = dispatch({ source, action: 'musicUrl', info: { type, musicInfo: songInfo } })
const startedAt = Date.now() const startedAt = Date.now()
@@ -592,7 +592,7 @@ export const createIvmSource = async({
} }
// ---- 等待脚本 inited ---- // ---- 等待脚本 inited ----
const inited = await new Promise((resolve, reject) => { const inited = await new Promise<any>((resolve, reject) => {
if (state.inited) return resolve(state.inited) if (state.inited) return resolve(state.inited)
const waiter = { resolve, reject } const waiter = { resolve, reject }
state.initWaiters.push(waiter) state.initWaiters.push(waiter)
@@ -609,7 +609,7 @@ export const createIvmSource = async({
// 按脚本上报的 sources 生成分平台接口(过滤 local 这类服务器侧无意义条目仍保留键,但 local 无 qualitys) // 按脚本上报的 sources 生成分平台接口(过滤 local 这类服务器侧无意义条目仍保留键,但 local 无 qualitys)
const reportedSources = inited?.sources && typeof inited.sources === 'object' ? Object.keys(inited.sources) : [] const reportedSources = inited?.sources && typeof inited.sources === 'object' ? Object.keys(inited.sources) : []
const sources = reportedSources.length ? reportedSources : ['kw', 'kg', 'tx', 'wy', 'mg'] const sources = reportedSources.length ? reportedSources : ['kw', 'kg', 'tx', 'wy']
const apis = {} const apis = {}
for (const s of sources) apis[s] = { getMusicUrl: getMusicUrlFor(s) } for (const s of sources) apis[s] = { getMusicUrl: getMusicUrlFor(s) }
+8 -8
View File
@@ -1,5 +1,5 @@
// 外部聚合音源统一入口(isolated-vm 懒加载封装) // 外部聚合音源统一入口(isolated-vm 懒加载封装)
// 内部委托 source-ivm.js 在硬隔离沙箱中执行音源脚本。 // 内部委托 source-ivm.ts 在硬隔离沙箱中执行音源脚本。
// 首次调用 getMusicUrl 时才加载并初始化隔离环境,后续复用同一实例。 // 首次调用 getMusicUrl 时才加载并初始化隔离环境,后续复用同一实例。
// 统一接口约定:source.kw.getMusicUrl(songInfo, type) → { promise, cancelHttp },resolve → { type, url } // 统一接口约定:source.kw.getMusicUrl(songInfo, type) → { promise, cancelHttp },resolve → { type, url }
// //
@@ -10,9 +10,9 @@
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { createIvmSource } from './source-ivm.js' import { createIvmSource } from './source-ivm.ts'
export const SOURCES = ['kw', 'kg', 'tx', 'wy', 'mg'] export const SOURCES = ['kw', 'kg', 'tx', 'wy']
const SOURCES_DIR = fileURLToPath(new URL('../../sources/', import.meta.url)) const SOURCES_DIR = fileURLToPath(new URL('../../sources/', import.meta.url))
@@ -32,7 +32,7 @@ const resolveScriptPath = () => {
return path.join(SOURCES_DIR, files[0]) return path.join(SOURCES_DIR, files[0])
} }
let sharedPromise = null let sharedPromise: Promise<any> | null = null
const getShared = () => { const getShared = () => {
if (!sharedPromise) { if (!sharedPromise) {
@@ -51,8 +51,8 @@ const getShared = () => {
const apiFor = (source) => ({ const apiFor = (source) => ({
getMusicUrl(songInfo, type) { getMusicUrl(songInfo, type) {
const requestObj = { cancelHttp() {}, promise: null } const requestObj = { cancelHttp() {}, promise: null as any }
let inner = null let inner: any = null
requestObj.cancelHttp = () => inner?.cancelHttp() requestObj.cancelHttp = () => inner?.cancelHttp()
requestObj.promise = (async () => { requestObj.promise = (async () => {
const api = await getShared() const api = await getShared()
@@ -64,10 +64,10 @@ const apiFor = (source) => ({
}, },
}) })
// 兼容导出:musicSdk/api-source.js 以 createApi(source) 方式接入聚合音源 // 兼容导出:sdk/api-source.ts 以 createApi(source) 方式接入聚合音源
export const createApi = apiFor export const createApi = apiFor
const sourceApi = {} const sourceApi: Record<string, any> = {}
for (const s of SOURCES) sourceApi[s] = apiFor(s) for (const s of SOURCES) sourceApi[s] = apiFor(s)
// 预热:提前触发脚本加载与初始化(可选调用) // 预热:提前触发脚本加载与初始化(可选调用)
+87
View File
@@ -0,0 +1,87 @@
// QQ音乐 URL 获取(直连)
// 接口: https://u.y.qq.com/cgi-bin/musicu.fcg
// 方法: vkey.GetVkeyServer.CgiGetVkey
// 取流方案:
// 按请求音质组 filename 链一次请求(M500{media_mid}.mp3 / M800{media_mid}.mp3 / F000{media_mid}.flac),
// 响应 midurlinfo 与 filename 一一对应,取第一个有 purl 的(即最高可用音质);
// 始终附带 C400{songmid}.m4a 保底(匿名可取的 128k)。
// 登录 cookie(uin + qm_keyst,见 /login/tx)可解锁 320k/无损。
// 统一接口约定:apis('tx').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// promise resolve → { type, url }(type 为实际命中的音质,可能低于请求值)
import { httpFetch } from '../common/request.ts'
import { getTxAuth } from '../auth/store.ts'
// 音质 → [文件前缀, 扩展名](flac24bit 暂走 flac 通道,命中即返回)
const prefixMap = {
'128k': ['M500', 'mp3'],
'320k': ['M800', 'mp3'],
flac: ['F000', 'flac'],
flac24bit: ['F000', 'flac'],
}
// 降级链:高音质无版权/VIP 受限时逐级回退
const chainMap = {
'128k': ['128k'],
'320k': ['320k', '128k'],
flac: ['flac', '320k', '128k'],
flac24bit: ['flac', '320k', '128k'],
}
export const getMusicUrl = (songInfo, type) => {
const requestObj = { cancelHttp() {}, promise: null as any }
const mediaMid = songInfo.strMediaMid || songInfo.songmid
const entries: any[] = []
for (const t of chainMap[type] || chainMap['128k']) {
const [prefix, ext] = prefixMap[t]
entries.push({ filename: `${prefix}${mediaMid}.${ext}`, type: t })
}
entries.push({ filename: `C400${songInfo.songmid}.m4a`, type: '128k' })
const auth = getTxAuth()
const uin = auth?.uin || '0'
const data = {
comm: { ct: '19', cv: '1859', uin },
req: {
module: 'vkey.GetVkeyServer',
method: 'CgiGetVkey',
param: {
guid: '7342827269',
songmid: [songInfo.songmid],
filename: entries.map(e => e.filename),
songtype: [0],
uin,
loginflag: 1,
platform: '20',
needNewCode: 1,
},
},
}
requestObj.promise = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {
method: 'post',
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
Referer: 'https://y.qq.com/',
...(auth ? { cookie: auth.cookie } : {}),
},
body: data,
timeout: 15000,
}).promise.then(({ statusCode, body }) => {
if (statusCode != 200 || body.code != 0 || body.req?.code != 0) {
return Promise.reject(new Error('获取失败'))
}
const data = body.req.data
const sip = data.sip?.[0] || 'http://aqqmusic.tc.qq.com/'
const infos = data.midurlinfo || []
const idx = infos.findIndex(info => info.purl)
if (idx >= 0) {
return { type: entries[idx]?.type || '128k', url: `${sip}${infos[idx].purl}` }
}
// 全部受限(VIP 歌匿名)时回退试听地址(服务器返回的 128k m4a 试听文件)
if (data.testfile2g) return { type: '128k', url: `${sip}${data.testfile2g}` }
return Promise.reject(new Error('获取失败'))
})
return requestObj
}
export default { getMusicUrl }
+5 -4
View File
@@ -4,9 +4,10 @@
// 统一接口约定:apis('wy').getMusicUrl(songInfo, type) → { promise, cancelHttp } // 统一接口约定:apis('wy').getMusicUrl(songInfo, type) → { promise, cancelHttp }
// promise resolve → { type, url } // promise resolve → { type, url }
import { httpFetch } from '../common/request.js' import { httpFetch } from '../common/request.ts'
import { eapi } from '../../musicSdk/wy/utils/crypto.ts' // crypto.js 为 tsc 就地发射产物(源 sdk/wy/utils/crypto.ts);统一走 .js,避免同进程出现 .ts/.js 双实例
import { getWyCookie } from '../auth/store.js' import { eapi } from '../wy/utils/crypto.ts'
import { getWyCookie } from '../auth/store.ts'
const levelMap = { const levelMap = {
'128k': 'standard', '128k': 'standard',
@@ -16,7 +17,7 @@ const levelMap = {
} }
export const getMusicUrl = (songInfo, type) => { export const getMusicUrl = (songInfo, type) => {
const requestObj = { cancelHttp() {}, promise: null } const requestObj = { cancelHttp() {}, promise: null as any }
const data = { const data = {
ids: `[${songInfo.songmid}]`, ids: `[${songInfo.songmid}]`,
level: levelMap[type] || 'standard', level: levelMap[type] || 'standard',
+3 -3
View File
@@ -3,8 +3,8 @@
// token 可从酷狗 App 抓包获取(login.token) // token 可从酷狗 App 抓包获取(login.token)
import crypto from 'node:crypto' import crypto from 'node:crypto'
import { httpFetch } from '../common/request.js' import { httpFetch } from '../common/request.ts'
import { getCredentials, setCredentials } from './store.js' import { getCredentials, setCredentials } from './store.ts'
const md5 = s => crypto.createHash('md5').update(s).digest('hex') const md5 = s => crypto.createHash('md5').update(s).digest('hex')
const SALT_KEY = '57ae12eb6890223e355ccfcb74edf70d' const SALT_KEY = '57ae12eb6890223e355ccfcb74edf70d'
@@ -79,7 +79,7 @@ export const verifyToken = async({ token, userid }) => {
}) })
if (body.status !== 1 || !body.data) { if (body.status !== 1 || !body.data) {
const err = new Error(body.error ?? 'token 无效或已过期') const err = new Error(body.error ?? 'token 无效或已过期')
err.status = 401 ;(err as any).status = 401
throw err throw err
} }
return { return {
+15 -2
View File
@@ -1,6 +1,6 @@
// 登录态存储 —— 各平台凭据持久化(data/auth.json,不入库) // 登录态存储 —— 各平台凭据持久化(data/auth.json,不入库)
// 结构:{ wy: {...}, kg: {...}, _kg_device: {...} } // 结构:{ wy: {...}, kg: {...}, tx: {...}, _kg_device: {...} }
// 注意:本文件经 tsc 就地发射 store.js 供纯 JS 模块引用(与 src/apis/index.ts 同约定) // 注意:store.js 为 tsc 就地发射产物(不入库);改逻辑只改本文件,然后 pnpm build
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path' import { dirname, join } from 'node:path'
@@ -32,6 +32,11 @@ export interface KgDevice {
updatedAt?: string updatedAt?: string
} }
export interface TxCredentials {
cookie: string
updatedAt?: string
}
let cache: Record<string, any> = {} let cache: Record<string, any> = {}
let loaded = false let loaded = false
@@ -75,3 +80,11 @@ export const getKgAuth = (): { token: string; userid: number | string } | null =
const cred = getCredentials('kg') as KgCredentials | null const cred = getCredentials('kg') as KgCredentials | null
return cred?.token ? { token: cred.token, userid: cred.userid || 0 } : null return cred?.token ? { token: cred.token, userid: cred.userid || 0 } : null
} }
/** tx(QQ) 登录态:cookie 与其中的 uin(未登录返回 null) */
export const getTxAuth = (): { cookie: string; uin: string } | null => {
const cred = getCredentials('tx') as TxCredentials | null
if (!cred?.cookie) return null
const uin = cred.cookie.match(/uin=(\d+)/)?.[1] ?? '0'
return { cookie: cred.cookie, uin }
}
+4 -4
View File
@@ -2,9 +2,9 @@
// 参考实现:NeteaseCloudMusicApi(login_qr_key / login_qr_check / login_cellphone / login_status) // 参考实现:NeteaseCloudMusicApi(login_qr_key / login_qr_check / login_cellphone / login_status)
import crypto from 'node:crypto' import crypto from 'node:crypto'
import { httpFetch } from '../common/request.js' import { httpFetch } from '../common/request.ts'
import { weapi } from '../../musicSdk/wy/utils/crypto.ts' import { weapi } from '../wy/utils/crypto.ts'
import { getWyCookie, setCredentials } from './store.js' import { getWyCookie, setCredentials } from './store.ts'
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
@@ -106,7 +106,7 @@ export const cellphoneLogin = async({ phone, password, captcha, countrycode = '8
const { body, setCookie } = await requestWeapi('/api/w/login/cellphone', data) const { body, setCookie } = await requestWeapi('/api/w/login/cellphone', data)
if (body.code !== 200) { if (body.code !== 200) {
const err = new Error(body.message ?? body.msg ?? '登录失败') const err = new Error(body.message ?? body.msg ?? '登录失败')
err.status = 401 ;(err as any).status = 401
throw err throw err
} }
const profile = await applyLogin(setCookie) const profile = await applyLogin(setCookie)
File renamed without changes.
@@ -9,7 +9,7 @@ import iconv from 'iconv-lite'
// ==================== 酷我 ==================== // ==================== 酷我 ====================
const handleInflate = data => new Promise((resolve, reject) => { const handleInflate = (data: Buffer): Promise<Buffer> => new Promise((resolve, reject) => {
zlib.inflate(data, (err, result) => { zlib.inflate(data, (err, result) => {
if (err) return reject(err) if (err) return reject(err)
resolve(result) resolve(result)
@@ -53,7 +53,7 @@ export const handleKwDecodeLyric = async({ lrcBase64, isGetLyricx }) => {
// ==================== QQ音乐(qrc) ==================== // ==================== QQ音乐(qrc) ====================
import { decodeQrc } from './qrcDecode.js' import { decodeQrc } from './qrcDecode.ts'
/** /**
* QQ音乐歌词解码 * QQ音乐歌词解码
@@ -1,10 +1,10 @@
// 酷狗 krc 歌词解码 // 酷狗 krc 歌词解码
import zlib from 'node:zlib' import zlib from 'node:zlib'
import { decodeName } from '../utils.js' import { decodeName } from '../utils.ts'
// 基于社区通用的 krc 解析思路 // 基于社区通用的 krc 解析思路
const enc_key = Buffer.from([0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69], 'binary') const enc_key = Buffer.from([0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69])
const decodeLyric = str => new Promise((resolve, reject) => { const decodeLyric = str => new Promise((resolve, reject) => {
if (!str.length) return if (!str.length) return
@@ -44,12 +44,12 @@ const parseLyric = str => {
let i = 0 let i = 0
let yrc = str.replace(/\[((\d+),\d+)\].*/g, str => { let yrc = str.replace(/\[((\d+),\d+)\].*/g, str => {
let result = str.match(/\[((\d+),\d+)\].*/) let result = str.match(/\[((\d+),\d+)\].*/)
let time = parseInt(result[2]) let time: any = parseInt(result[2])
let ms = time % 1000 let ms = time % 1000
time /= 1000 time /= 1000
let m = parseInt(time / 60).toString().padStart(2, '0') let m = Math.trunc(time / 60).toString().padStart(2, '0')
time %= 60 time %= 60
let s = parseInt(time).toString().padStart(2, '0') let s = Math.trunc(time).toString().padStart(2, '0')
time = `${m}:${s}.${ms}` time = `${m}:${s}.${ms}`
if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}` if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}`
if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}` if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}`
File renamed without changes.
@@ -218,7 +218,7 @@ const QRC_KEY = Buffer.from([
0x58, 0x43, 0x21, 0x40, 0x21, 0x40, 0x23, 0x29, 0x28, 0x4e, 0x48, 0x4c, 0x58, 0x43, 0x21, 0x40, 0x21, 0x40, 0x23, 0x29, 0x28, 0x4e, 0x48, 0x4c,
]) ])
const handleInflate = (data) => new Promise((resolve, reject) => { const handleInflate = (data: Buffer): Promise<Buffer> => new Promise((resolve, reject) => {
// Z_SYNC_FLUSH:容忍尾部不完整的 zlib 流(对应原实现忽略 Z_BUF_ERROR 的行为) // Z_SYNC_FLUSH:容忍尾部不完整的 zlib 流(对应原实现忽略 Z_BUF_ERROR 的行为)
inflate(data, { finishFlush: constants.Z_SYNC_FLUSH }, (err, result) => { inflate(data, { finishFlush: constants.Z_SYNC_FLUSH }, (err, result) => {
if (err) reject(err) if (err) reject(err)
@@ -3,7 +3,7 @@
// handle_kw_decode_lyric → 酷我歌词解码(见 lyricDecode.js) // handle_kw_decode_lyric → 酷我歌词解码(见 lyricDecode.js)
// handle_tx_decode_lyric → QQ音乐歌词解码(见 lyricDecode.js) // handle_tx_decode_lyric → QQ音乐歌词解码(见 lyricDecode.js)
import { handleKwDecodeLyric, handleTxDecodeLyric } from './lyricDecode.js' import { handleKwDecodeLyric, handleTxDecodeLyric } from './lyricDecode.ts'
const localHandlers = { const localHandlers = {
[WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric]: handleKwDecodeLyric, [WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric]: handleKwDecodeLyric,
@@ -28,6 +28,6 @@ export const rendererOff = () => {}
export const rendererOffAll = () => {} export const rendererOffAll = () => {}
export const rendererSendSync = () => {} export const rendererSendSync = () => {}
import { WIN_MAIN_RENDERER_EVENT_NAME } from './ipcNames.js' import { WIN_MAIN_RENDERER_EVENT_NAME } from './ipcNames.ts'
export default { rendererInvoke, rendererSend, rendererOn, rendererOnce, rendererOff, rendererOffAll, rendererSendSync } export default { rendererInvoke, rendererSend, rendererOn, rendererOnce, rendererOff, rendererOffAll, rendererSendSync }
+48 -18
View File
@@ -12,14 +12,43 @@ import http from 'node:http'
import https from 'node:https' import https from 'node:https'
import zlib from 'node:zlib' import zlib from 'node:zlib'
import { URL } from 'node:url' import { URL } from 'node:url'
import { requestMsg } from './message.ts'
const MAX_BUFFER_SIZE = 64 * 1024 * 1024 // 64MB const MAX_BUFFER_SIZE = 64 * 1024 * 1024 // 64MB
export interface HttpOptions {
method?: string
headers?: Record<string, any>
body?: any
form?: Record<string, any>
timeout?: number
lookup?: any
family?: number
[key: string]: any
}
export interface HttpFetchResponse {
statusCode: number
statusMessage?: string
headers: Record<string, any>
body: any
raw: Buffer
}
export interface HttpRequestObject {
promise: Promise<HttpFetchResponse>
cancelHttp(): void
isCancelled: boolean
requestObj?: any
cancelFn?: ((err: any) => void) | null
[key: string]: any
}
const defaultHeaders = { const defaultHeaders = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
} }
const parseBody = (raw) => { const parseBody = (raw: Buffer): any => {
if (!raw || !raw.length) return null if (!raw || !raw.length) return null
const text = raw.toString('utf8') const text = raw.toString('utf8')
// 与原版一致:无论 content-type 都尝试 JSON 解析,失败则返回原始文本 // 与原版一致:无论 content-type 都尝试 JSON 解析,失败则返回原始文本
@@ -33,9 +62,10 @@ const parseBody = (raw) => {
/** /**
* 发起请求(返回带 promise 与 cancelHttp 的对象) * 发起请求(返回带 promise 与 cancelHttp 的对象)
*/ */
const buildHttpPromise = (url, options = {}) => { const buildHttpPromise = (url: string, options: HttpOptions = {}): HttpRequestObject => {
const method = (options.method || 'GET').toUpperCase() const method = (options.method || 'GET').toUpperCase()
const obj = { const obj: HttpRequestObject = {
promise: null as any,
isCancelled: false, isCancelled: false,
cancelHttp: () => { cancelHttp: () => {
if (!obj.requestObj) { if (!obj.requestObj) {
@@ -44,7 +74,8 @@ const buildHttpPromise = (url, options = {}) => {
} }
obj.requestObj.destroy(new Error('Canceled')) obj.requestObj.destroy(new Error('Canceled'))
obj.requestObj = null obj.requestObj = null
obj.promise = obj.cancelHttp = null // 取消后置空是运行时事实,但公开类型保持非空(消费方总是先取 promise 再取消)
;(obj as any).promise = (obj as any).cancelHttp = null
obj.cancelFn?.(new Error('Canceled')) obj.cancelFn?.(new Error('Canceled'))
obj.cancelFn = null obj.cancelFn = null
}, },
@@ -52,7 +83,7 @@ const buildHttpPromise = (url, options = {}) => {
obj.promise = new Promise((resolve, reject) => { obj.promise = new Promise((resolve, reject) => {
obj.cancelFn = reject obj.cancelFn = reject
let parsedUrl let parsedUrl: URL
try { try {
parsedUrl = new URL(url) parsedUrl = new URL(url)
} catch (err) { } catch (err) {
@@ -64,7 +95,7 @@ const buildHttpPromise = (url, options = {}) => {
const client = isHttps ? https : http const client = isHttps ? https : http
// 数据体构造:body(对象/字符串/Buffer)与 form(对象 → urlencoded) // 数据体构造:body(对象/字符串/Buffer)与 form(对象 → urlencoded)
let body = null let body: string | any = null
let isForm = false let isForm = false
if (options.form) { if (options.form) {
const formBody = new URLSearchParams() const formBody = new URLSearchParams()
@@ -79,13 +110,13 @@ const buildHttpPromise = (url, options = {}) => {
body = typeof options.body === 'object' && !Buffer.isBuffer(options.body) ? JSON.stringify(options.body) : options.body body = typeof options.body === 'object' && !Buffer.isBuffer(options.body) ? JSON.stringify(options.body) : options.body
} }
const headers = { ...defaultHeaders, ...(options.headers || {}) } const headers: Record<string, any> = { ...defaultHeaders, ...(options.headers || {}) }
if (body && !headers['content-type'] && !headers['Content-Type']) { if (body && !headers['content-type'] && !headers['Content-Type']) {
headers['content-type'] = isForm ? 'application/x-www-form-urlencoded' : 'application/json' headers['content-type'] = isForm ? 'application/x-www-form-urlencoded' : 'application/json'
} }
if (body) headers['content-length'] = Buffer.byteLength(body) if (body) headers['content-length'] = Buffer.byteLength(body)
const requestOptions = { const requestOptions: Record<string, any> = {
method, method,
headers, headers,
family: options.family || undefined, family: options.family || undefined,
@@ -95,16 +126,16 @@ const buildHttpPromise = (url, options = {}) => {
const req = client.request(parsedUrl, requestOptions, res => { const req = client.request(parsedUrl, requestOptions, res => {
obj.requestObj = null obj.requestObj = null
obj.cancelFn = null obj.cancelFn = null
const statusCode = res.statusCode const statusCode = res.statusCode ?? 0
let stream = res let stream: any = res
switch (res.headers['content-encoding']) { switch (res.headers['content-encoding']) {
case 'gzip': stream = res.pipe(zlib.createGunzip()); break case 'gzip': stream = res.pipe(zlib.createGunzip()); break
case 'deflate': stream = res.pipe(zlib.createInflate()); break case 'deflate': stream = res.pipe(zlib.createInflate()); break
case 'br': stream = res.pipe(zlib.createBrotliDecompress()); break case 'br': stream = res.pipe(zlib.createBrotliDecompress()); break
} }
const chunks = [] const chunks: Buffer[] = []
let size = 0 let size = 0
stream.on('data', chunk => { stream.on('data', chunk => {
size += chunk.length size += chunk.length
@@ -129,13 +160,14 @@ const buildHttpPromise = (url, options = {}) => {
obj.requestObj = req obj.requestObj = req
req.on('error', err => { req.on('error', err => {
if (obj.isCancelled || err.canceled) err.canceled = true const e = err as any
reject(err) if (obj.isCancelled || e.canceled) e.canceled = true
reject(e)
}) })
if (options.timeout) { if (options.timeout) {
req.setTimeout(options.timeout, () => { req.setTimeout(options.timeout, () => {
const err = new Error('Request timeout') const err = new Error('Request timeout') as any
err.timeout = true err.timeout = true
req.destroy(err) req.destroy(err)
}) })
@@ -150,9 +182,9 @@ const buildHttpPromise = (url, options = {}) => {
/** /**
* 请求超时/断网错误统一处理(与原版行为一致) * 请求超时/断网错误统一处理(与原版行为一致)
*/ */
export const httpFetch = (url, options = { method: 'get' }) => { export const httpFetch = (url: string, options: HttpOptions = { method: 'get' }): HttpRequestObject => {
const requestObj = buildHttpPromise(url, options) const requestObj = buildHttpPromise(url, options)
requestObj.promise = requestObj.promise.catch(err => { requestObj.promise = requestObj.promise.catch((err: any) => {
if (err.canceled) return Promise.reject(err) if (err.canceled) return Promise.reject(err)
switch (err.code) { switch (err.code) {
case 'ETIMEDOUT': case 'ETIMEDOUT':
@@ -169,6 +201,4 @@ export const httpFetch = (url, options = { method: 'get' }) => {
return requestObj return requestObj
} }
import { requestMsg } from './message.js'
export default { httpFetch } export default { httpFetch }
File renamed without changes.
+1 -6
View File
@@ -2,8 +2,6 @@ import kw from './kw/index.ts'
import kg from './kg/index.ts' import kg from './kg/index.ts'
import tx from './tx/index.ts' import tx from './tx/index.ts'
import wy from './wy/index.ts' import wy from './wy/index.ts'
import mg from './mg/index.ts'
import bd from './bd/index.ts'
import { supportQuality } from './api-source.ts' import { supportQuality } from './api-source.ts'
import type { PlatformModule, SearchResult, SongInfo } from './types.ts' import type { PlatformModule, SearchResult, SongInfo } from './types.ts'
import { import {
@@ -13,7 +11,7 @@ import {
dateFormat2, dateFormat2,
formatPlayCount, formatPlayCount,
decodeName, decodeName,
} from '../src/index.js' } from './common/utils.ts'
// 通用工具函数(SDK 内 `import { ... } from '../index.ts'` 的解析目标) // 通用工具函数(SDK 内 `import { ... } from '../index.ts'` 的解析目标)
export { sizeFormate, formatPlayTime, dateFormat, dateFormat2, formatPlayCount, decodeName } export { sizeFormate, formatPlayTime, dateFormat, dateFormat2, formatPlayCount, decodeName }
@@ -24,9 +22,6 @@ const SUPPORTED_SOURCES: Array<{ id: string; name: string; module: PlatformModul
{ id: 'kg', name: '酷狗音乐', module: kg }, { id: 'kg', name: '酷狗音乐', module: kg },
{ id: 'tx', name: 'QQ音乐', module: tx }, { id: 'tx', name: 'QQ音乐', module: tx },
{ id: 'wy', name: '网易音乐', module: wy }, { id: 'wy', name: '网易音乐', module: wy },
{ id: 'mg', name: '咪咕音乐', module: mg },
// bd 取流接口可用但暂不列入平台列表(直连路由仍可访问 /url/bd/:id)
// { id: 'bd', name: '百度音乐', module: bd },
] ]
const SOURCE_MODULES = Object.fromEntries(SUPPORTED_SOURCES.map((s) => [s.id, s.module])) const SOURCE_MODULES = Object.fromEntries(SUPPORTED_SOURCES.map((s) => [s.id, s.module]))
File renamed without changes.
+1 -1
View File
@@ -1,7 +1,7 @@
import { httpFetch } from '../request.ts' import { httpFetch } from '../request.ts'
import { decodeName, dateFormat2 } from '../index.ts' import { decodeName, dateFormat2 } from '../index.ts'
import { signatureParams } from './util.ts' import { signatureParams } from './util.ts'
// import { getMusicInfoRaw } from './musicInfo.js' // import { getMusicInfoRaw } from './musicInfo.ts'
import type { HttpRequestObject } from '../types.ts' import type { HttpRequestObject } from '../types.ts'
export default { export default {
File renamed without changes.
+1 -1
View File
@@ -8,7 +8,7 @@ import hotSearch from './hotSearch.ts'
import comment from './comment.ts' import comment from './comment.ts'
import singer from './singer.ts' import singer from './singer.ts'
import album from './album.ts' import album from './album.ts'
// import tipSearch from './tipSearch.js' // import tipSearch from './tipSearch.ts'
const kg = { const kg = {
// tipSearch, // tipSearch,
File renamed without changes.
+1 -1
View File
@@ -1,5 +1,5 @@
import { httpFetch } from '../request.ts' import { httpFetch } from '../request.ts'
import { decodeKrc } from '../../src/common/lyricUtils/kg.js' import { decodeKrc } from '../common/lyricUtils/kg.ts'
export default { export default {
getIntv(interval) { getIntv(interval) {
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -2,7 +2,7 @@
import { httpFetch } from '../request.ts' import { httpFetch } from '../request.ts'
import { formatPlayTime, decodeName } from '../index.ts' import { formatPlayTime, decodeName } from '../index.ts'
// import { debug } from '../../utils/env.js' // import { debug } from '../../utils/env.ts'
import { formatSinger } from './util.ts' import { formatSinger } from './util.ts'
import type { Quality, QualityInfo, SongInfo, SongType } from '../types.ts' import type { Quality, QualityInfo, SongInfo, SongType } from '../types.ts'
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2 -2
View File
@@ -1,6 +1,6 @@
// import { httpGet, httpFetch } from '../request.ts' // import { httpGet, httpFetch } from '../request.ts'
import { WIN_MAIN_RENDERER_EVENT_NAME } from '../../src/common/ipcNames.js' import { WIN_MAIN_RENDERER_EVENT_NAME } from '../common/ipcNames.ts'
import { rendererInvoke } from '../../src/common/rendererIpc.js' import { rendererInvoke } from '../common/rendererIpc.ts'
import { createCipheriv, createDecipheriv } from 'crypto' import { createCipheriv, createDecipheriv } from 'crypto'
import { toMD5 } from '../utils.ts' import { toMD5 } from '../utils.ts'
+5
View File
@@ -0,0 +1,5 @@
// SDK 根请求模块 —— 唯一实现在 common/request.ts,此处仅转发
// sdk 内 `import { ... } from '../request.ts'` 的解析目标
export { httpFetch } from './common/request.ts'
export type { HttpOptions, HttpRequestObject, HttpFetchResponse } from './common/request.ts'
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3 -3
View File
@@ -1,12 +1,12 @@
import type { HttpRequestObject } from '../types.ts' import type { HttpRequestObject } from '../types.ts'
import { httpFetch } from '../request.ts' import { httpFetch } from '../request.ts'
import getMusicInfo from './musicInfo.ts' import getMusicInfo from './musicInfo.ts'
import { rendererInvoke } from '../../src/common/rendererIpc.ts' import { rendererInvoke } from '../common/rendererIpc.ts'
import { WIN_MAIN_RENDERER_EVENT_NAME } from '../../src/common/ipcNames.ts' import { WIN_MAIN_RENDERER_EVENT_NAME } from '../common/ipcNames.ts'
const songIdMap = new Map() const songIdMap = new Map()
const promises = new Map() const promises = new Map()
export const decodeLyric = (lrc, tlrc, rlrc) => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, { lrc, tlrc, rlrc }) export const decodeLyric = (lrc: any, tlrc?: any, rlrc?: any): Promise<any> => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, { lrc, tlrc, rlrc })
const parseTools = { const parseTools = {
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3 -3
View File
@@ -2,7 +2,7 @@
// 所有平台模块与根模块的类型锚点。类型只描述实际返回的数据形状,不改变运行时行为。 // 所有平台模块与根模块的类型锚点。类型只描述实际返回的数据形状,不改变运行时行为。
/** 音乐平台标识 */ /** 音乐平台标识 */
export type Source = 'kw' | 'kg' | 'tx' | 'wy' | 'mg' | 'bd' export type Source = 'kw' | 'kg' | 'tx' | 'wy'
/** 音质标识(wav 仅部分接口声明使用) */ /** 音质标识(wav 仅部分接口声明使用) */
export type Quality = '128k' | '320k' | 'flac' | 'flac24bit' | 'wav' export type Quality = '128k' | '320k' | 'flac' | 'flac24bit' | 'wav'
@@ -39,7 +39,7 @@ export interface SongInfo {
[key: string]: unknown [key: string]: unknown
} }
/** httpFetch 响应(与 src/common/request.js 契约一致) */ /** httpFetch 响应(与 common/request.ts 契约一致) */
export interface HttpResponse { export interface HttpResponse {
statusCode: number statusCode: number
statusMessage?: string statusMessage?: string
@@ -54,7 +54,7 @@ export interface HttpRequestObject {
cancelHttp(): void cancelHttp(): void
} }
/** httpFetch 选项(src/common/request.js 子集) */ /** httpFetch 选项(common/request.ts 子集) */
export interface HttpOptions { export interface HttpOptions {
method?: string method?: string
headers?: Record<string, string> headers?: Record<string, string>
+1 -1
View File
@@ -1,6 +1,6 @@
import crypto from 'crypto' import crypto from 'crypto'
import dns from 'dns' import dns from 'dns'
import { decodeName } from '../src/common/utils.js' import { decodeName } from './common/utils.ts'
export const toMD5 = (str: string): string => crypto.createHash('md5').update(str).digest('hex') export const toMD5 = (str: string): string => crypto.createHash('md5').update(str).digest('hex')
File renamed without changes.
File renamed without changes.
+1 -1
View File
@@ -7,7 +7,7 @@ import songList from './songList.ts'
import hotSearch from './hotSearch.ts' import hotSearch from './hotSearch.ts'
import comment from './comment.ts' import comment from './comment.ts'
import singer from './singer.ts' import singer from './singer.ts'
// import tipSearch from './tipSearch.js' // import tipSearch from './tipSearch.ts'
const wy = { const wy = {
// tipSearch, // tipSearch,
File renamed without changes.
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more