refactor!: 移除品牌字样,音源改为 sources/ 目录自动加载,脚本不入库
This commit is contained in:
1 parent
f548e5990b
commit
5dd0468860
34 files changed
+256
-232
No files matched your search
@@ -3,6 +3,10 @@ node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# 音源脚本(第三方、不可审计,不入库;放入 sources/ 自动加载)
|
||||
sources/
|
||||
lx-music-source-v5.js
|
||||
|
||||
# Runtime
|
||||
*.log
|
||||
logs/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 音乐服务器(MusicServer)
|
||||
|
||||
基于洛雪音乐(lx-music)聚合 SDK 的多平台音乐 API 服务器,纯 Node.js 实现,无需浏览器环境。
|
||||
多平台音乐聚合 API 服务器,纯 Node.js 实现,无需浏览器环境。
|
||||
|
||||
## 功能
|
||||
|
||||
@@ -25,7 +25,7 @@ pnpm test # 运行冒烟测试(自动启动/关闭本地服务器)
|
||||
| `GET /search?name=xxx&singer=xxx&source=kw&page=1&limit=20` | 搜索歌曲(source 省略则聚合所有平台) |
|
||||
| `GET /find?name=xxx&singer=xxx&albumName=xxx&interval=xxx` | 精确匹配搜索(跨平台去重排序) |
|
||||
| `GET /url/:source/:id?quality=320k` | 获取播放地址(quality: 128k/320k/flac/flac24bit) |
|
||||
| `GET /lyric/:source/:id` | 获取歌词(返回 lyric/tlyric/lxlyric) |
|
||||
| `GET /lyric/:source/:id` | 获取歌词(返回 lyric/tlyric/yrc) |
|
||||
| `GET /pic/:source/:id` | 获取封面 |
|
||||
| `GET /boards/:source` | 榜单列表 |
|
||||
| `GET /boards/:source/:boardId` | 榜单详情 |
|
||||
@@ -59,8 +59,8 @@ src/
|
||||
components.js OpenAPI 共享 schema(与路由校验同源)
|
||||
ui.js ReDoc 页面模板
|
||||
apis/ 各平台 URL 直连实现(kw/kg/tx/wy/mg/bd)
|
||||
lx-source.js lx 聚合源懒加载入口
|
||||
lx-source-ivm.js isolated-vm 隔离执行器
|
||||
source.js 外部音源懒加载入口(sources/ 目录脚本,不入库)
|
||||
source-ivm.js isolated-vm 隔离执行器
|
||||
common/ 公共模块
|
||||
request.js HTTP 请求(可取消、自动 JSON 解析、gzip/deflate 解压)
|
||||
utils.js 工具函数(sizeFormate/formatPlayTime/dateFormat 等)
|
||||
@@ -70,7 +70,7 @@ src/
|
||||
lyricDecode.js 酷我/QQ 歌词解码(替代原 Electron 主进程处理器)
|
||||
qrcDecode.js QQ 歌词 3DES 解密(纯 JS 移植)
|
||||
lyricUtils/kg.js 酷狗 krc 歌词解码
|
||||
musicSdk/ 洛雪音乐聚合 SDK(搜索/歌词/榜单等核心逻辑,TypeScript 源码)
|
||||
musicSdk/ 音乐聚合 SDK(搜索/歌词/榜单等核心逻辑,TypeScript 源码)
|
||||
index.js 平台聚合入口
|
||||
request.js SDK 请求模块(转接 src/common/request.js)
|
||||
message.js SDK 消息模块(转接 src/common/message.js)
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@
|
||||
{ "code": 0, "data": { "type": "320k", "url": "https://..." } }
|
||||
|
||||
// GET /lyric/kw/228908
|
||||
{ "code": 0, "data": { "lyric": "[00:00.00]...", "tlyric": "", "lxlyric": "" } }
|
||||
{ "code": 0, "data": { "lyric": "[00:00.00]...", "tlyric": "", "yrc": "" } }
|
||||
|
||||
// 参数缺失
|
||||
// 422
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# lx 聚合源 —— isolated-vm 加载器
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
调用方 ── lx.kw.getMusicUrl(songInfo, type) ──▶ src/apis/lx-source.js(懒加载单例)
|
||||
│
|
||||
▼
|
||||
src/apis/lx-source-ivm.js
|
||||
isolated-vm 硬隔离沙箱
|
||||
┌──────────────────────────┐
|
||||
│ 原始混淆脚本(升版即换文件) │
|
||||
│ ↕ 仅经显式桥接通道 │
|
||||
└──────────────────────────┘
|
||||
http(白名单) crypto zlib 定时器 console
|
||||
```
|
||||
|
||||
- 接口契约与其它平台适配器一致:`getMusicUrl(songInfo, type) → { promise, cancelHttp }`,resolve → `{ type, url }`
|
||||
- 脚本升版:替换项目根目录脚本文件(或设 `LX_SOURCE_SCRIPT_PATH`)即可,无需改代码
|
||||
|
||||
## 安全模型
|
||||
|
||||
| 面 | 措施 |
|
||||
|---|---|
|
||||
| 进程 | isolate 无 process/require/fs,内存上限 256MB(可调) |
|
||||
| HTTP | **默认阻止**;白名单取脚本头部 `SERVER_SCRIPT_CONFIG.apiUrl` 同源,可覆盖 |
|
||||
| 密钥 | crypto 桥接日志只记操作与长度,不落密钥内容 |
|
||||
| 观测 | 每个 lx API 调用(on/send/request/utils/dispatch)均有日志,请求含耗时与状态码 |
|
||||
|
||||
HTTP 白名单选项(`createIvmLxSource`):
|
||||
|
||||
- `allowedOrigins: string[]` —— 显式指定白名单(覆盖脚本声明)
|
||||
- `httpFilter: (url) => boolean` —— 自定义判定,优先于 origin 白名单
|
||||
- `allowAllHttp: true` —— 全部放行(仅调试用)
|
||||
|
||||
被拦截的请求会以错误回调返回脚本(`✋` 日志标记),脚本按请求失败自行处理。
|
||||
|
||||
## 环境变量(经 lx-source.js 封装)
|
||||
|
||||
- `LX_SOURCE_SCRIPT_PATH` —— 脚本文件路径(默认项目根目录 `lx-music-source-v5.js`)
|
||||
- `LX_SOURCE_LOG=0` —— 关闭日志
|
||||
- `LX_SOURCE_ALLOW_ALL_HTTP=1` —— 放行全部 HTTP(调试用)
|
||||
|
||||
## 已知边界
|
||||
|
||||
- `lx.request` 回调的 resp 不含 `raw`/`bytes`(Buffer 无法跨隔离边界,实测脚本未使用)
|
||||
- `formData` 选项不支持(底层 httpFetch 不支持 multipart)
|
||||
- `updateAlert` 事件仅记日志,不弹窗
|
||||
@@ -0,0 +1,57 @@
|
||||
# 外部聚合音源 —— isolated-vm 加载器
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
调用方 ── source.kw.getMusicUrl(songInfo, type) ──▶ src/apis/source.js(懒加载单例)
|
||||
│
|
||||
▼
|
||||
src/apis/source-ivm.js
|
||||
isolated-vm 硬隔离沙箱
|
||||
┌──────────────────────────┐
|
||||
│ 音源脚本(升版即换文件) │
|
||||
│ ↕ 仅经显式桥接通道 │
|
||||
└──────────────────────────┘
|
||||
http(白名单) crypto zlib 定时器 console
|
||||
```
|
||||
|
||||
- 接口契约与其它平台适配器一致:`getMusicUrl(songInfo, type) → { promise, cancelHttp }`,resolve → `{ type, url }`
|
||||
- 脚本来源:默认自动读取 `sources/` 目录下按文件名排序的第一个 `.js`;也可用 `SOURCE_SCRIPT_PATH` 显式指定
|
||||
- 脚本升版:替换 `sources/` 目录下的脚本文件即可,无需改代码
|
||||
- 脚本文件含第三方混淆内容,不入库(`.gitignore` 已排除 `sources/`)
|
||||
|
||||
## 安全模型
|
||||
|
||||
| 面 | 措施 |
|
||||
|---|---|
|
||||
| 进程 | isolate 无 process/require/fs,内存上限 256MB(可调) |
|
||||
| HTTP | **默认阻止**;白名单取脚本头部 `SERVER_SCRIPT_CONFIG.apiUrl` 同源,可覆盖 |
|
||||
| 密钥 | crypto 桥接日志只记操作与长度,不落密钥内容 |
|
||||
| 观测 | 每个音源 API 调用(on/send/request/utils/dispatch)均有日志,请求含耗时与状态码 |
|
||||
|
||||
HTTP 白名单选项(`createIvmSource`):
|
||||
|
||||
- `allowedOrigins: string[]` —— 显式指定白名单(覆盖脚本声明)
|
||||
- `httpFilter: (url) => boolean` —— 自定义判定,优先于 origin 白名单
|
||||
- `allowAllHttp: true` —— 全部放行(仅调试用)
|
||||
|
||||
被拦截的请求会以错误回调返回脚本(`✋` 日志标记),脚本按请求失败自行处理。
|
||||
|
||||
## 启用方式
|
||||
|
||||
设置环境变量 `SOURCE_ENABLED`(任意非 `0`/`false` 值)后启动服务器,取流请求即走音源;
|
||||
不设置或为空则使用内置直连接口。歌词、封面、榜单等其余能力始终走内置实现。
|
||||
|
||||
## 环境变量(经 source.js 封装)
|
||||
|
||||
- `SOURCE_ENABLED=1` —— 启用音源(见上)
|
||||
- `SOURCE_SCRIPT_PATH` —— 脚本文件路径(缺省扫描 `sources/` 目录)
|
||||
- `SOURCE_LOG=0` —— 关闭日志
|
||||
- `SOURCE_ALLOW_ALL_HTTP=1` —— 放行全部 HTTP(调试用)
|
||||
|
||||
## 已知边界
|
||||
|
||||
- 音源请求回调的 resp 不含 `raw`/`bytes`(Buffer 无法跨隔离边界,实测脚本未使用)
|
||||
- `formData` 选项不支持(底层 httpFetch 不支持 multipart)
|
||||
- `updateAlert` 事件仅记日志,不弹窗
|
||||
- 沙箱内全局对象名 `lx` 为第三方脚本依赖的 API 契约名,不可改名(见 `source-ivm.js` GLUE 注释)
|
||||
@@ -1,13 +0,0 @@
|
||||
/*!
|
||||
* @name 独家音源
|
||||
* @description 后续更新,请关注微信公众号: 洛雪科技
|
||||
* @version 5
|
||||
* @author w
|
||||
*/
|
||||
|
||||
// ===== 服务端下发配置(自动生成, 请勿修改) =====
|
||||
globalThis['SERVER_SCRIPT_CONFIG'] = {"apiUrl":"https:\/\/88.lxmusic.xn--fiqs8s","apiKey":"lxmusic","signSalt":"LxSrv@2026#Sig","fingerprint":"74c52ba24b386a2301983d94596246c2"};
|
||||
// ===== 服务端下发配置结束 =====
|
||||
|
||||
|
||||
;(function ﱡﹶ(ﱡﹲ,ﱡﱣ,ﹲ,ﱞﹶ,ﹲﹰ,ﹸﹼ,ﹰﱣ,ﹰﹼ,ﹴﱠ,ﹸﹸ,ءﱟ,ﱟﹸ,ﹰﹲ,ﱠﹴ,ﱞﹲ,ﹼﱣ,ﹶﹲ,ﹶﹰ){return ﹶﹰ=function(ﱟﹺ,ﹼ,ﹰﹺ,ءﱟ){return function(ﹼﹶ,ﱡء,ءﱡ,ﱡﹲ,ﹲ,ﹲﹰ,ﹸﹼ,ﹰﱣ,ﹴﱠ){if(ﹼﹶ=ﹼﹶ^ﱡء,ﹲﹰ=66666-ﹼﹶ,ﹸﹼ=ﹶﹲ[ﹲﹰ],ﹸﹼ!==void 0)return ﹸﹼ;for(ﹲ=ﱡﱣ[ﹼﹶ],ﱡﹲ=[],ﹸﹼ=ﹰﹺ(ﹼﹶ,ﱡء,ﹲ.\u006c\u0065\u006e\u0067\u0074\u0068,ﹼ,ﱟﹺ),ﹰﱣ=0,ﹼﹶ=(ﹸﹼ^ﹸﹼ>>>16^ﱡء^ﹼﹶ)&65535,ءﱡ=0;ءﱡ<ﹲ.\u006c\u0065\u006e\u0067\u0074\u0068;ءﱡ++){(ءﱡ&1)===0&&(ﹸﹼ=ﹸﹼ+2654435769>>>0,ﹰﱣ=ﹼ(ﹸﹼ,ﱟﹺ));ﱡء=ﹰﱣ>>>((ءﱡ&1)<<4)&65535,ﹴﱠ=ﹲ[ءﱡ],ﹰﹼ(ﱡﹲ,(ﹴﱠ^ﱡء^ﹼﹶ)&65535),ﹼﹶ=ﹴﱠ+(ﹰﱣ>>>16&65535)+ءﱡ+40503&65535}return ﹶﹲ[ﹲﹰ]=ﹸﹸ(ﱡﹲ)}}(function(ﹼﹶ,ﱟﹺ,ﱡء,ﹼ){return ﱡء=ﹼﹶ>>>16&65535,ﹼﹶ=ﹼﹶ&65535,ﹼ=ﱟﹺ>>>16&65535,ﱟﹺ=ﱟﹺ&65535,ﹼﹶ*ﱟﹺ+(ﱡء*ﱟﹺ+ﹼﹶ*ﹼ<<16>>>0)|0},function(ﹼﹶ,ﱟﹺ){return ﹼﹶ>>>=0,ﹼﹶ=ﱟﹺ(ﹼﹶ^ﹼﹶ>>>16,569420461),ﹼﹶ=ﱟﹺ(ﹼﹶ^ﹼﹶ>>>15,1935289751),(ﹼﹶ^ﹼﹶ>>>15)>>>0},function(ﹸ,ﱡء,ءﱡ,ﹼ,ﱟﹺ){return ﹼ(1327381188>>>0^ﱟﹺ(ﹸ+1>>>0,2654435761)^ﱟﹺ(ﱡء+1>>>0,2246822507)^ﱟﹺ(ءﱡ+1>>>0,3266489909),ﱟﹺ)}),function ﹰﹺ(ﹼﹶ,ﱟﹺ,ﱡء,ﹼ,ءﱡ){ﱟﹺ=ﱟﹺ||[],ﱡء=ﱡء||[],ﹼ=ﹼ||[];for(var ﱡﹴ=ﱡء.\u006c\u0065\u006e\u0067\u0074\u0068,ءﹶ,ﹰﹸ;ﹰﹸ=ﱡﹲ[ﹼﹶ++];)if(61<ﹰﹸ)ﹰﹸ<68&&((ﹰﹸ>65||(ﹰﹸ<64&&((ﹰﹸ-->62||(ﱡء[++ﱡﹴ-ﹰﹸ+++60]=function(ﱡء,ﱡﹴ){return function(){return ﹶﹰ(ﱡء,ﱡﹴ)}}(ﱡﹲ[ﹼﹶ++]^-++ﹰﹸ+32881+ﹼﹶ,ﱡﹲ[ﹼﹶ++]^-ﹰﹸ--+49974+ﹼﹶ),!true))&&(ﱡء[ﱡﹴ+++ﹰﹸ---62]=function(ﱡء){return function(){return ﱟﹺ[ﱡء]++}}(ﱡﹲ[ﹼﹶ++]^--ﹰﹸ+57119+ﹼﹶ)),!0)||ﹰﹸ>64&&(ﱡء[ﱡﹴ+++ﹰﹸ-67]()||(ﹼﹶ=ﱡء[ﱡﹴ-ﹰﹸ+++63]()),(ﱡﹴ-=3)+ﹰﹸ+++31,1)||(ﱡء[ﱡﹴ+++ﹰﹸ-64]=function(){return function(){return typeof \u0070\u0072\u006f\u0063\u0065\u0073\u0073}}()),""))&&(66<ﹰﹸ||(ﱡء[(ﱡﹴ+=101)- --ﹰﹸ-38]=function(ﱡء,ﱡﹴ){return function(){return new ﹰﹲ(ﱡء(),ﱡﹴ())}}(ﱡء[++ﱡﹴ-104],ﱡء[(ﱡﹴ-=103)+ﹰﹸ++-65]),void 0))&&(ﱡء[--ﱡﹴ+ﹰﹸ-67]=function(ﱡء){return function(){return!ﱡء()}}(ﱡء[++ﱡﹴ- --ﹰﹸ+65])),!false)||73>ﹰﹸ&&(ﹰﹸ<70&&(ﹰﹸ<69&&(ﱡء[++ﱡﹴ+ ++ﹰﹸ-70]=function(){return function(){return \u0070\u0061\u0072\u0073\u0065\u0049\u006e\u0074}}(),!0)||(ﱡء[ﱡﹴ+++ ++ﹰﹸ-70]=function(){return function(){return \u0050\u0072\u006f\u006d\u0069\u0073\u0065}}()),true)||(71<ﹰﹸ||((71<++ﹰﹸ||(ﱡء[(ﱡﹴ+=--ﹰﹸ)-78]=function(ﱡء,ﱡﹴ,ﹰﹸ,ﹼﹶ,ﹰﱞ,ﹰء,ﹲﹼ,ﹼﱡ){return function(){return ﹰﱣ(ﱡء())(ﱡﹴ(),ﹰﹸ(),ﹼﹶ(),ﹰﱞ(),ﹰء(),ﹲﹼ(),ﹼﱡ())()}}(ﱡء[(ﱡﹴ+=43)-++ﹰﹸ-49],ﱡء[--ﱡﹴ+ﹰﹸ++-191],ﱡء[ﱡﹴ++-118],ﱡء[(ﱡﹴ+=8)+ﹰﹸ---198],ﱡء[--ﱡﹴ-ﹰﹸ-53],ﱡء[(ﱡﹴ+=ﹰﹸ--)-194],ﱡء[ﱡﹴ+ﹰﹸ++-263],ﱡء[(ﱡﹴ-=198)-ﹰﹸ+77]),![]))&&(ﱡء[(ﱡﹴ+=3)-ﹰﹸ--+67]()&&(ﹼﹶ=ﱡء[ﱡﹴ-ﹰﹸ--+67]()),(ﱡﹴ-=5)-ﹰﹸ+++115),null))&&(ﱡء[ﱡﹴ+++ ++ﹰﹸ-73]=function(){return function(){return \u0063\u006f\u006e\u0073\u006f\u006c\u0065}}()),!false)||ﹰﹸ>77&&(ﹰﹸ>81&&(83>ﹰﹸ--&&(ﱡء[ﱡﹴ+++ﹰﹸ---81]=function(ﱡء){return function(){return ﱡء}}(ﹼﱣ),!null)||(ﱡء[(ﱡﹴ-=10)-ﹰﹸ+89]=function(ﱡء,ﱡﹴ,ﹰﹸ,ﹼﹶ){return function(){return ﹸﹼ(ﱡء(),(ﱡﹴ()[0]=ﹰﹸ(),ﹼﹶ()))()}}(ﱡء[ﱡﹴ+ﹰﹸ-74],ﱡء[(ﱡﹴ+=36)+ﹰﹸ-109],ﱡء[(ﱡﹴ+=ﹰﹸ)-111],ﱡء[(ﱡﹴ-=110)+ﹰﹸ---81])),!null)||++ﹰﹸ>81&&(ﱡء[ﱡﹴ-ﹰﹸ+80]=function(ﱡء,ﱡﹴ){return function(){return delete ﱡء()[ﱡﹴ()]}}(ﱡء[(ﱡﹴ+=ﹰﹸ++)-84],ﱡء[(ﱡﹴ-=83)+ﹰﹸ---83]),!null)||ﹰﹸ>80&&(ﱡء[ﱡﹴ+++ ++ﹰﹸ-82]=function(){return function(){return \u0053\u0079\u006d\u0062\u006f\u006c}}(),1)||80>ﹰﹸ&&(ﱡء[ﱡﹴ++]=function(){return function(){return \u004f\u0062\u006a\u0065\u0063\u0074}}(),!0)||(ﱡء[ﱡﹴ++]=function(){return function(){return \u0072\u0065\u0071\u0075\u0069\u0072\u0065}}()),!false)||(ﹰﹸ>76||((ﹰﹸ>75||(ﹰﹸ<75&&((ﹰﹸ<74||(ﱡء[(ﱡﹴ+=92)+ ++ﹰﹸ-168]=function(ﱡء){return function(){return-ﱡء()}}(ﱡء[(ﱡﹴ-=92)-++ﹰﹸ+75]),void 0))&&(ﱡء[ﱡﹴ--+--ﹰﹸ-74]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()+ﱡﹴ()}}(ﱡء[(ﱡﹴ-=--ﹰﹸ)+70],ﱡء[(ﱡﹴ+=71)-ﹰﹸ--+71])),1)||(ﱡء[(ﱡﹴ+=ﹰﹸ)-77]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()*ﱡﹴ()}}(ﱡء[ﱡﹴ++-ﹰﹸ++-2],ﱡء[(ﱡﹴ-=77)+--ﹰﹸ-75])),void 0))&&(ﱡء[++ﱡﹴ-ﹰﹸ--+73]=function(ﱡء,ﱡﹴ){return function(){return ﱡء()<=ﱡﹴ()}}(ﱡLine truncated
|
||||
@@ -23,8 +23,8 @@ const sources: ApiSourceInfo[] = [
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: 'v5',
|
||||
name: 'lx-music-source-v5 聚合源',
|
||||
id: 'source',
|
||||
name: '外部聚合音源',
|
||||
disabled: false,
|
||||
supportQualitys: {
|
||||
kw: ['128k', '320k', 'flac'],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// 接口选择层:按 apiSource 配置把请求路由到 v5 聚合源或服务器内置直连接口
|
||||
// 接口选择层:按 apiSource 配置把请求路由到外部聚合音源或服务器内置直连接口
|
||||
import apiSourceInfo from './api-source-info.ts'
|
||||
import { apiSource, userApi } from '../src/renderer/store.js'
|
||||
import internalApi from '../src/apis/index.js'
|
||||
import { createApi as createLxSourceApi } from '../src/apis/lx-source.js'
|
||||
import { createApi as createSourceApi } from '../src/apis/source.js'
|
||||
|
||||
interface SourceApi {
|
||||
getMusicUrl(songInfo: any, type: string): { promise: Promise<any>; cancelHttp(): void }
|
||||
@@ -10,11 +10,11 @@ interface SourceApi {
|
||||
}
|
||||
|
||||
const allApi: Record<string, SourceApi> = {
|
||||
v5_kw: createLxSourceApi('kw'),
|
||||
v5_kg: createLxSourceApi('kg'),
|
||||
v5_tx: createLxSourceApi('tx'),
|
||||
v5_wy: createLxSourceApi('wy'),
|
||||
v5_mg: createLxSourceApi('mg'),
|
||||
source_kw: createSourceApi('kw'),
|
||||
source_kg: createSourceApi('kg'),
|
||||
source_tx: createSourceApi('tx'),
|
||||
source_wy: createSourceApi('wy'),
|
||||
source_mg: createSourceApi('mg'),
|
||||
}
|
||||
|
||||
const apiList: Record<string, SourceApi | undefined> = {}
|
||||
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
lyric: Buffer.from(body.content, 'base64').toString('utf-8'),
|
||||
tlyric: '',
|
||||
rlyric: '',
|
||||
lxlyric: '',
|
||||
yrc: '',
|
||||
}
|
||||
default:
|
||||
return Promise.reject(new Error(`未知歌词格式: ${body.fmt}`))
|
||||
|
||||
@@ -224,10 +224,10 @@ export default {
|
||||
// return Promise.reject(new Error('Get lyric failed'))
|
||||
// }
|
||||
// if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
// lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric)
|
||||
// lrcInfo.yrc = lrcTools.parse(lrcInfo.lyric)
|
||||
// // console.log(lrcInfo.lyric)
|
||||
// // console.log(lrcInfo.tlyric)
|
||||
// // console.log(lrcInfo.lxlyric)
|
||||
// // console.log(lrcInfo.yrc)
|
||||
// // console.log(JSON.stringify(lrcInfo))
|
||||
// })
|
||||
// })
|
||||
@@ -255,9 +255,9 @@ export default {
|
||||
// console.log(lrcInfo)
|
||||
if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
try {
|
||||
lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric)
|
||||
lrcInfo.yrc = lrcTools.parse(lrcInfo.lyric)
|
||||
} catch {
|
||||
lrcInfo.lxlyric = ''
|
||||
lrcInfo.yrc = ''
|
||||
}
|
||||
lrcInfo.lyric = lrcInfo.lyric.replace(lrcTools.rxps.wordTimeAll, '')
|
||||
if (!existTimeExp.test(lrcInfo.lyric)) return Promise.reject(new Error('Get lyric failed'))
|
||||
|
||||
@@ -11,7 +11,7 @@ const mrcTools = {
|
||||
parseLyric(str) {
|
||||
str = str.replace(/\r/g, '')
|
||||
const lines = str.split('\n')
|
||||
const lxlrcLines: string[] = []
|
||||
const yrcLines: string[] = []
|
||||
const lrcLines: string[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -40,11 +40,11 @@ const mrcTools = {
|
||||
})
|
||||
const wordArr = words.split(this.rxps.wordTime)
|
||||
const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('')
|
||||
lxlrcLines.push(`[${time}]${newWords}`)
|
||||
yrcLines.push(`[${time}]${newWords}`)
|
||||
}
|
||||
return {
|
||||
lyric: lrcLines.join('\n'),
|
||||
lxlyric: lxlrcLines.join('\n'),
|
||||
yrc: yrcLines.join('\n'),
|
||||
}
|
||||
},
|
||||
getText(url, tryNum = 0) {
|
||||
@@ -67,7 +67,7 @@ const mrcTools = {
|
||||
})
|
||||
},
|
||||
getLrc(url) {
|
||||
return this.getText(url).then(text => ({ lxlyric: '', lyric: text }))
|
||||
return this.getText(url).then(text => ({ yrc: '', lyric: text }))
|
||||
},
|
||||
getTrc(url) {
|
||||
if (!url) return Promise.resolve('')
|
||||
|
||||
@@ -72,7 +72,7 @@ const toBigintArray = (data: string): bigint[] => {
|
||||
return jArr
|
||||
}
|
||||
|
||||
// https://github.com/lyswhut/lx-music-desktop/issues/445#issuecomment-1139338682
|
||||
// 基于社区通用的 mrc 逐字歌词解析思路
|
||||
const MAX = 9223372036854775807n
|
||||
const MIN = -9223372036854775808n
|
||||
const toLong = (str: string | bigint): bigint => {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
export const bHh = '624868746c'
|
||||
|
||||
export const headers: Record<string, string | string[]> = {
|
||||
'User-Agent': 'lx-music request',
|
||||
'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],
|
||||
}
|
||||
|
||||
|
||||
+11
-11
@@ -30,10 +30,10 @@ const parseTools = {
|
||||
parseLyric(lrc) {
|
||||
lrc = lrc.trim()
|
||||
lrc = lrc.replace(/\r/g, '')
|
||||
if (!lrc) return { lyric: '', lxlyric: '' }
|
||||
if (!lrc) return { lyric: '', yrc: '' }
|
||||
const lines = lrc.split('\n')
|
||||
|
||||
const lxlrcLines: string[] = []
|
||||
const yrcLines: string[] = []
|
||||
const lrcLines: string[] = []
|
||||
|
||||
for (let line of lines) {
|
||||
@@ -41,11 +41,11 @@ const parseTools = {
|
||||
let result = this.rxps.lineTime.exec(line)
|
||||
if (!result) {
|
||||
if (line.startsWith('[offset')) {
|
||||
lxlrcLines.push(line)
|
||||
yrcLines.push(line)
|
||||
lrcLines.push(line)
|
||||
}
|
||||
if (this.rxps.lineTime2.test(line)) {
|
||||
// lxlrcLines.push(line)
|
||||
// yrcLines.push(line)
|
||||
lrcLines.push(line)
|
||||
}
|
||||
continue
|
||||
@@ -67,17 +67,17 @@ const parseTools = {
|
||||
})
|
||||
const wordArr = words.split(this.rxps.wordTime)
|
||||
const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('')
|
||||
lxlrcLines.push(`${startTimeStr}${newWords}`)
|
||||
yrcLines.push(`${startTimeStr}${newWords}`)
|
||||
}
|
||||
return {
|
||||
lyric: lrcLines.join('\n'),
|
||||
lxlyric: lxlrcLines.join('\n'),
|
||||
yrc: yrcLines.join('\n'),
|
||||
}
|
||||
},
|
||||
parseRlyric(lrc) {
|
||||
lrc = lrc.trim()
|
||||
lrc = lrc.replace(/\r/g, '')
|
||||
if (!lrc) return { lyric: '', lxlyric: '' }
|
||||
if (!lrc) return { lyric: '', yrc: '' }
|
||||
const lines = lrc.split('\n')
|
||||
|
||||
const lrcLines: string[] = []
|
||||
@@ -177,14 +177,14 @@ const parseTools = {
|
||||
lyric: '',
|
||||
tlyric: '',
|
||||
rlyric: '',
|
||||
lxlyric: '',
|
||||
yrc: '',
|
||||
}
|
||||
if (lrc) {
|
||||
let { lyric, lxlyric } = this.parseLyric(this.removeTag(lrc))
|
||||
let { lyric, yrc } = this.parseLyric(this.removeTag(lrc))
|
||||
info.lyric = lyric
|
||||
info.lxlyric = lxlyric
|
||||
info.yrc = yrc
|
||||
// console.log(lyric)
|
||||
// console.log(lxlyric)
|
||||
// console.log(yrc)
|
||||
}
|
||||
if (rlrc) info.rlyric = this.fixRlrcTimeTag(this.parseRlyric(this.removeTag(rlrc)), info.lyric)
|
||||
if (tlrc) info.tlyric = this.fixTlrcTimeTag(tlrc, info.lyric)
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ export interface LyricInfo {
|
||||
lyric: string
|
||||
tlyric: string
|
||||
rlyric?: string
|
||||
lxlyric?: string
|
||||
yrc?: string
|
||||
}
|
||||
|
||||
/** 搜索结果 */
|
||||
|
||||
+11
-11
@@ -8,7 +8,7 @@ import type { LyricInfo, HttpRequestObject } from '../types.ts'
|
||||
|
||||
// str = str.replace(/\r/g, '')
|
||||
|
||||
// let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
|
||||
// let yrc = str.replace(/\[((\d+),\d+)\].*/g, str => {
|
||||
// let result = str.match(/\[((\d+),\d+)\].*/)
|
||||
// let time = parseInt(result[2])
|
||||
// let ms = time % 1000
|
||||
@@ -31,8 +31,8 @@ import type { LyricInfo, HttpRequestObject } from '../types.ts'
|
||||
// return str
|
||||
// })
|
||||
|
||||
// lxlyric = decodeName(lxlyric)
|
||||
// return lxlyric.trim()
|
||||
// yrc = decodeName(yrc)
|
||||
// return yrc.trim()
|
||||
// }
|
||||
|
||||
const eapiRequest = (url: string, data: object) => {
|
||||
@@ -72,7 +72,7 @@ const parseTools = {
|
||||
return `[${m}:${s}.${ms}]`
|
||||
},
|
||||
parseLyric(lines) {
|
||||
const lxlrcLines: string[] = []
|
||||
const yrcLines: string[] = []
|
||||
const lrcLines: string[] = []
|
||||
|
||||
for (let line of lines) {
|
||||
@@ -80,7 +80,7 @@ const parseTools = {
|
||||
let result = this.rxps.lineTime.exec(line)
|
||||
if (!result) {
|
||||
if (line.startsWith('[offset')) {
|
||||
lxlrcLines.push(line)
|
||||
yrcLines.push(line)
|
||||
lrcLines.push(line)
|
||||
}
|
||||
continue
|
||||
@@ -103,11 +103,11 @@ const parseTools = {
|
||||
const wordArr = words.split(this.rxps.wordTime)
|
||||
wordArr.shift()
|
||||
const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('')
|
||||
lxlrcLines.push(`${startTimeStr}${newWords}`)
|
||||
yrcLines.push(`${startTimeStr}${newWords}`)
|
||||
}
|
||||
return {
|
||||
lyric: lrcLines.join('\n'),
|
||||
lxlyric: lxlrcLines.join('\n'),
|
||||
yrc: yrcLines.join('\n'),
|
||||
}
|
||||
},
|
||||
parseHeaderInfo(str) {
|
||||
@@ -170,7 +170,7 @@ const parseTools = {
|
||||
lyric: '',
|
||||
tlyric: '',
|
||||
rlyric: '',
|
||||
lxlyric: '',
|
||||
yrc: '',
|
||||
}
|
||||
if (ylrc) {
|
||||
let lines = this.parseHeaderInfo(ylrc)
|
||||
@@ -196,7 +196,7 @@ const parseTools = {
|
||||
const timeRxp = /^\[[\d:.]+\]/
|
||||
const headers = lines.filter(l => timeRxp.test(l)).join('\n')
|
||||
info.lyric = `${headers}\n${result.lyric}`
|
||||
info.lxlyric = result.lxlyric
|
||||
info.yrc = result.yrc
|
||||
return info
|
||||
}
|
||||
}
|
||||
@@ -242,13 +242,13 @@ const parseTools = {
|
||||
// lyric: body.lrc.lyric,
|
||||
// tlyric: body.tlyric?.lyric ?? '',
|
||||
// rlyric: body.romalrc?.lyric ?? '',
|
||||
// // lxlyric: parseLyric(body.klyric.lyric),
|
||||
// // yrc: parseLyric(body.klyric.lyric),
|
||||
// }
|
||||
// })
|
||||
// return requestObj
|
||||
// }
|
||||
|
||||
// https://github.com/lyswhut/lx-music-mobile/issues/370
|
||||
// 参考社区通用的歌词时间标签修正思路
|
||||
const fixTimeLabel = (lrc: string, tlrc?: string, romalrc?: string) => {
|
||||
if (lrc) {
|
||||
let newLrc = lrc.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]')
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "music-server",
|
||||
"version": "1.0.0",
|
||||
"description": "多平台音乐聚合服务器(基于 lx-music 聚合 SDK)",
|
||||
"description": "多平台音乐聚合服务器",
|
||||
"type": "module",
|
||||
"main": "src/index.js",
|
||||
"packageManager": "pnpm@12.4.1",
|
||||
|
||||
+1
-3
@@ -1,13 +1,12 @@
|
||||
// 各平台 URL 直连接口汇总
|
||||
// 服务器环境固定使用内置直连接口(apiSource.value === 'internal')
|
||||
// lx 为 lx-music 聚合源:isolated-vm 硬隔离执行原始脚本(签名方案已放弃,见 docs/lx-source-ivm.md)
|
||||
// 外部聚合音源(sources/ 目录脚本)不走这里,由 musicSdk/api-source.ts 直接接入 source.js
|
||||
import kw from './kw.js';
|
||||
import kg from './kg.js';
|
||||
import tx from './tx.js';
|
||||
import wy from './wy.js';
|
||||
import mg from './mg.js';
|
||||
import bd from './bd.js';
|
||||
import lx from './lx-source.js';
|
||||
export default {
|
||||
kw: kw,
|
||||
kg: kg,
|
||||
@@ -15,5 +14,4 @@ export default {
|
||||
wy: wy,
|
||||
mg: mg,
|
||||
bd: bd,
|
||||
lx: lx,
|
||||
};
|
||||
+1
-3
@@ -1,6 +1,6 @@
|
||||
// 各平台 URL 直连接口汇总
|
||||
// 服务器环境固定使用内置直连接口(apiSource.value === 'internal')
|
||||
// lx 为 lx-music 聚合源:isolated-vm 硬隔离执行原始脚本(签名方案已放弃,见 docs/lx-source-ivm.md)
|
||||
// 外部聚合音源(sources/ 目录脚本)不走这里,由 musicSdk/api-source.ts 直接接入 source.js
|
||||
|
||||
import kw from './kw.js'
|
||||
import kg from './kg.js'
|
||||
@@ -8,7 +8,6 @@ import tx from './tx.js'
|
||||
import wy from './wy.js'
|
||||
import mg from './mg.js'
|
||||
import bd from './bd.js'
|
||||
import lx from './lx-source.js'
|
||||
|
||||
interface SongInfo {
|
||||
songmid?: string
|
||||
@@ -34,5 +33,4 @@ export default {
|
||||
wy: wy as ApiModule,
|
||||
mg: mg as ApiModule,
|
||||
bd: bd as ApiModule,
|
||||
lx: lx as unknown as Record<string, ApiModule>,
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// lx 聚合源统一入口(isolated-vm 懒加载封装)
|
||||
// 内部委托 lx-source-ivm.js 在硬隔离沙箱中执行原始脚本(签名方案已放弃:v5 的
|
||||
// to/si 为脚本内部哈希、服务端独立校验,直连复刻不可行,详见 docs/lx-source-ivm.md)。
|
||||
// 首次调用 getMusicUrl 时才加载并初始化隔离环境,后续复用同一实例。
|
||||
// 统一接口约定:lx.kw.getMusicUrl(songInfo, type) → { promise, cancelHttp },resolve → { type, url }
|
||||
|
||||
import { createIvmLxSource } from './lx-source-ivm.js'
|
||||
|
||||
export const SOURCES = ['kw', 'kg', 'tx', 'wy', 'mg']
|
||||
|
||||
let sharedPromise = null
|
||||
|
||||
const getShared = () => {
|
||||
if (!sharedPromise) {
|
||||
// 配置可经环境变量调整,与项目其它适配器保持一致风格
|
||||
sharedPromise = createIvmLxSource({
|
||||
scriptPath: process.env.LX_SOURCE_SCRIPT_PATH || undefined,
|
||||
log: process.env.LX_SOURCE_LOG !== '0',
|
||||
allowAllHttp: process.env.LX_SOURCE_ALLOW_ALL_HTTP === '1',
|
||||
}).catch(err => {
|
||||
sharedPromise = null // 失败允许下次重试
|
||||
throw err
|
||||
})
|
||||
}
|
||||
return sharedPromise
|
||||
}
|
||||
|
||||
const apiFor = (source) => ({
|
||||
getMusicUrl(songInfo, type) {
|
||||
const requestObj = { cancelHttp() {}, promise: null }
|
||||
let inner = null
|
||||
requestObj.cancelHttp = () => inner?.cancelHttp()
|
||||
requestObj.promise = (async () => {
|
||||
const lx = await getShared()
|
||||
if (!lx[source]) throw new Error(`脚本不支持的平台: ${source}`)
|
||||
inner = lx[source].getMusicUrl(songInfo, type)
|
||||
return inner.promise
|
||||
})()
|
||||
return requestObj
|
||||
},
|
||||
})
|
||||
|
||||
// 兼容导出:musicSdk/api-source.js 以 createApi(source) 方式接入 v5_* 聚合源
|
||||
export const createApi = apiFor
|
||||
|
||||
const lxSource = {}
|
||||
for (const s of SOURCES) lxSource[s] = apiFor(s)
|
||||
|
||||
// 预热:提前触发脚本加载与初始化(可选调用)
|
||||
lxSource.init = getShared
|
||||
|
||||
export default lxSource
|
||||
@@ -1,39 +1,36 @@
|
||||
// lx 聚合源加载器(isolated-vm 硬隔离执行第三方脚本)
|
||||
// 外部聚合音源加载器(isolated-vm 硬隔离执行第三方脚本)
|
||||
//
|
||||
// 这是 lx 聚合源的唯一接入方式:在 isolated-vm 硬隔离沙箱中运行原始混淆脚本
|
||||
// (如 lx-music-source-v5.js),脚本的所有能力都经由本模块显式桥接的通道:
|
||||
// 这是外部音源的唯一接入方式:在 isolated-vm 硬隔离沙箱中运行原始混淆脚本,
|
||||
// 脚本的所有能力都经由本模块显式桥接的通道:
|
||||
// - http:默认只放行脚本头部 SERVER_SCRIPT_CONFIG.apiUrl 同源请求,其余一律阻止
|
||||
// - crypto / zlib / buffer:Node 侧实现,日志只记操作与长度,不记录密钥内容
|
||||
// - 定时器 / console:桥接到 Node,随 isolate 销毁回收
|
||||
// 对外接口:getMusicUrl(songInfo, type) → { promise, cancelHttp },与其它平台适配器一致。
|
||||
// 脚本升版时只需替换脚本文件,无需改代码。
|
||||
// 脚本升版时只需替换 sources/ 目录下的脚本文件,无需改代码。
|
||||
//
|
||||
// 安全模型:
|
||||
// - isolate 内存上限(memoryLimit),无 process/require/fs/网络直连能力
|
||||
// - HTTP 出站默认拒绝(deny-all),白名单来自脚本自身声明的 apiUrl,可被 options 收紧或放宽
|
||||
// - 每个 lx API 调用(on/send/request/utils/dispatch)都有日志,请求含耗时与状态
|
||||
// - 每个音源 API 调用(on/send/request/utils/dispatch)都有日志,请求含耗时与状态
|
||||
//
|
||||
// 依赖:isolated-vm(未安装时工厂函数抛出明确错误)
|
||||
// 用法:
|
||||
// import { createIvmLxSource } from './lx-source-ivm.js'
|
||||
// const lx = await createIvmLxSource() // 默认加载项目根目录的 lx-music-source-v5.js
|
||||
// const lx = await createIvmLxSource({ scriptPath, http }) // http 可注入 mock 做离线测试
|
||||
// await lx.kw.getMusicUrl({ songmid: '123' }, '320k').promise
|
||||
// await lx.destroy()
|
||||
// import { createIvmSource } from './source-ivm.js'
|
||||
// const source = await createIvmSource({ scriptPath }) // scriptPath 必填(sources/ 扫描逻辑见 source.js)
|
||||
// const source = await createIvmSource({ scriptPath, http }) // http 可注入 mock 做离线测试
|
||||
// await source.kw.getMusicUrl({ songmid: '123' }, '320k').promise
|
||||
// await source.destroy()
|
||||
//
|
||||
// 已知边界(相对桌面端 preload 的差异):
|
||||
// - lx.request 回调的 resp 不含 raw/bytes(Buffer 无法跨隔离边界,实测脚本未使用)
|
||||
// - source.request 回调的 resp 不含 raw/bytes(Buffer 无法跨隔离边界,实测脚本未使用)
|
||||
// - formData 选项不支持(httpFetch 不支持 multipart)
|
||||
// - updateAlert 事件仅记录到日志,不弹窗
|
||||
|
||||
import fs from 'node:fs'
|
||||
import crypto from 'node:crypto'
|
||||
import zlib from 'node:zlib'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { httpFetch } from '../common/request.js'
|
||||
|
||||
const DEFAULT_SCRIPT_PATH = fileURLToPath(new URL('../../lx-music-source-v5.js', import.meta.url))
|
||||
|
||||
// 从脚本头部的 SERVER_SCRIPT_CONFIG 提取 apiUrl 声明,作为 HTTP 白名单缺省值。
|
||||
// 兼容裸键(apiUrl:)与 JSON 风格("apiUrl":),并处理 \/ 转义
|
||||
const extractAllowedOrigins = (script) => {
|
||||
@@ -51,7 +48,7 @@ const brief = (v, max = 72) => {
|
||||
return s.length > max ? `${s.slice(0, max)}…(${s.length})` : s
|
||||
}
|
||||
|
||||
// 从脚本头部注释解析 @name/@version 等(对齐 lx.currentScriptInfo)
|
||||
// 从脚本头部注释解析 @name/@version 等(对齐音源脚本 currentScriptInfo)
|
||||
const parseScriptInfo = (script, scriptPath) => {
|
||||
const head = script.slice(0, 2000)
|
||||
const pick = (key) => {
|
||||
@@ -69,7 +66,7 @@ const parseScriptInfo = (script, scriptPath) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 注入隔离环境的胶水层:提供 lx API、定时器、console、Bytes 包装。
|
||||
// 注入隔离环境的胶水层:提供音源脚本 API(globalThis.lx 为脚本契约名)、定时器、console、Bytes 包装。
|
||||
// 与 Node 的数据交换一律走 JSON 字符串 / hex 字符串,避免跨隔离传输复杂对象。
|
||||
const GLUE = `
|
||||
'use strict'
|
||||
@@ -188,10 +185,10 @@ function __fromAny(data, enc) {
|
||||
|
||||
// ---- console / 定时器桥接 ----
|
||||
globalThis.console = {
|
||||
log: function () { __log('[lx] ' + Array.prototype.map.call(arguments, String).join(' ')) },
|
||||
info: function () { __log('[lx] ' + Array.prototype.map.call(arguments, String).join(' ')) },
|
||||
warn: function () { __log('[lx] ' + Array.prototype.map.call(arguments, String).join(' ')) },
|
||||
error: function () { __log('[lx] ' + Array.prototype.map.call(arguments, String).join(' ')) },
|
||||
log: function () { __log('[source] ' + Array.prototype.map.call(arguments, String).join(' ')) },
|
||||
info: function () { __log('[source] ' + Array.prototype.map.call(arguments, String).join(' ')) },
|
||||
warn: function () { __log('[source] ' + Array.prototype.map.call(arguments, String).join(' ')) },
|
||||
error: function () { __log('[source] ' + Array.prototype.map.call(arguments, String).join(' ')) },
|
||||
debug: function () {},
|
||||
}
|
||||
globalThis.setTimeout = function (fn, ms) {
|
||||
@@ -221,7 +218,9 @@ globalThis.__fireTimer = function (id) {
|
||||
try { t.fn.apply(undefined, t.args) } catch (e) { __log('timer error: ' + (e && e.message)) }
|
||||
}
|
||||
|
||||
// ---- lx API(对齐 lx-music-desktop preload.js)----
|
||||
// ---- 音源脚本 preload API ----
|
||||
// 注意:globalThis.lx 是第三方脚本依赖的 API 契约名(脚本内部调用 lx.on/source.request 等),
|
||||
// 属于对外兼容接口而非命名残留,不可改动,否则脚本初始化直接失败。
|
||||
var __EVENT_NAMES = { request: 'request', inited: 'inited', updateAlert: 'updateAlert' }
|
||||
var __handlers = {}
|
||||
globalThis.lx = {
|
||||
@@ -242,7 +241,7 @@ globalThis.lx = {
|
||||
var err = __send(String(name), JSON.stringify(data === undefined ? null : data))
|
||||
return err ? Promise.reject(new Error(err)) : Promise.resolve()
|
||||
},
|
||||
request: function (url, options, callback) { return __lxRequest(url, options, callback, __currentDispatch) },
|
||||
request: function (url, options, callback) { return __sourceRequest(url, options, callback, __currentDispatch) },
|
||||
utils: {
|
||||
crypto: {
|
||||
md5: function (str) { return __md5(str) },
|
||||
@@ -256,11 +255,11 @@ globalThis.lx = {
|
||||
},
|
||||
buffer: {
|
||||
from: function (data, enc) {
|
||||
__log('lx.utils.buffer.from(' + (data instanceof __Bytes ? 'Bytes(' + data.length + ')' : typeof data + (data && data.length !== undefined ? '[' + data.length + ']' : '')) + (enc ? ',' + enc : '') + ')')
|
||||
__log('source.utils.buffer.from(' + (data instanceof __Bytes ? 'Bytes(' + data.length + ')' : typeof data + (data && data.length !== undefined ? '[' + data.length + ']' : '')) + (enc ? ',' + enc : '') + ')')
|
||||
return __fromAny(data, enc)
|
||||
},
|
||||
bufToString: function (buf, format) {
|
||||
__log('lx.utils.buffer.bufToString(' + (buf && buf.length !== undefined ? buf.length + 'B' : typeof buf) + ',' + (format || 'utf8') + ')')
|
||||
__log('source.utils.buffer.bufToString(' + (buf && buf.length !== undefined ? buf.length + 'B' : typeof buf) + ',' + (format || 'utf8') + ')')
|
||||
return buf.toString(format || 'utf8')
|
||||
},
|
||||
},
|
||||
@@ -272,7 +271,7 @@ globalThis.lx = {
|
||||
}
|
||||
// 隔离环境内不依赖 ivm 全局:回调统一走「隔离内注册表 + 自增 id」,
|
||||
// Node 侧通过 jail.get 拿到 __completeHttp 等入口反调
|
||||
function __lxRequest(url, options, callback, dispatchId) {
|
||||
function __sourceRequest(url, options, callback, dispatchId) {
|
||||
options = options || {}
|
||||
var payload = {
|
||||
method: options.method || 'get',
|
||||
@@ -343,9 +342,9 @@ globalThis.__dispatch = function (dispatchId, payloadJson) {
|
||||
`
|
||||
|
||||
/**
|
||||
* 创建隔离执行的 lx 聚合源
|
||||
* 创建隔离执行的外部音源
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.scriptPath] 原始脚本路径,默认项目根目录 lx-music-source-v5.js
|
||||
* @param {string} options.scriptPath 音源脚本路径(必填,sources/ 目录扫描逻辑见 source.js)
|
||||
* @param {Function} [options.http] http 实现,默认项目统一 httpFetch;测试可注入 mock
|
||||
* @param {number} [options.memoryLimit] 隔离内存上限 MB,默认 256
|
||||
* @param {number} [options.initTimeout] 等待脚本上报 inited 的超时 ms,默认 10000
|
||||
@@ -356,8 +355,8 @@ globalThis.__dispatch = function (dispatchId, payloadJson) {
|
||||
* @param {boolean} [options.allowAllHttp] 放行全部 http 请求(调试用,默认 false)
|
||||
* @returns {{ env: string, scriptInfo: object, inited: object|null, apis: object, allowedOrigins: string[], destroy: Function }}
|
||||
*/
|
||||
export const createIvmLxSource = async({
|
||||
scriptPath = DEFAULT_SCRIPT_PATH,
|
||||
export const createIvmSource = async({
|
||||
scriptPath,
|
||||
http: httpImpl = httpFetch,
|
||||
memoryLimit = 256,
|
||||
initTimeout = 10000,
|
||||
@@ -371,8 +370,9 @@ export const createIvmLxSource = async({
|
||||
try {
|
||||
ivm = (await import('isolated-vm')).default
|
||||
} catch {
|
||||
throw new Error('未安装 isolated-vm,无法启用隔离执行兜底:npm i isolated-vm')
|
||||
throw new Error('未安装 isolated-vm,无法启用隔离执行音源:pnpm add isolated-vm')
|
||||
}
|
||||
if (!scriptPath) throw new Error('缺少音源脚本路径 scriptPath(sources/ 目录扫描逻辑见 source.js)')
|
||||
|
||||
const script = fs.readFileSync(scriptPath, 'utf8')
|
||||
const scriptInfo = parseScriptInfo(script, scriptPath)
|
||||
@@ -382,7 +382,7 @@ export const createIvmLxSource = async({
|
||||
? logOption
|
||||
: logOption === false
|
||||
? () => {}
|
||||
: (msg) => console.log('[lx-ivm]', msg)
|
||||
: (msg) => console.log('[source-ivm]', msg)
|
||||
const scriptOrigins = extractAllowedOrigins(script)
|
||||
const origins = (allowedOrigins ?? scriptOrigins).map(o => String(o).replace(/\/+$/, ''))
|
||||
const httpAllowed = (urlStr) => {
|
||||
@@ -433,12 +433,12 @@ export const createIvmLxSource = async({
|
||||
await jail.set('__log', new ivm.Callback((msg) => log(String(msg))))
|
||||
await jail.set('__on', new ivm.Callback((name) => {
|
||||
state.onRegistered = name
|
||||
log(`lx.on(${String(name)})`)
|
||||
log(`source.on(${String(name)})`)
|
||||
}))
|
||||
await jail.set('__send', new ivm.Callback((name, dataJson) => {
|
||||
let data = null
|
||||
try { data = JSON.parse(dataJson) } catch {}
|
||||
log(`lx.send(${String(name)}) ${brief(dataJson ?? '')}`)
|
||||
log(`source.send(${String(name)}) ${brief(dataJson ?? '')}`)
|
||||
if (name === 'inited') {
|
||||
state.inited = data
|
||||
const waiters = state.initWaiters.splice(0)
|
||||
@@ -458,26 +458,26 @@ export const createIvmLxSource = async({
|
||||
else p.reject(new Error(parsed.message || '获取失败'))
|
||||
}))
|
||||
await jail.set('__md5', new ivm.Callback((str) => {
|
||||
log(`lx.utils.crypto.md5(${brief(str)})`)
|
||||
log(`source.utils.crypto.md5(${brief(str)})`)
|
||||
return crypto.createHash('md5').update(String(str)).digest('hex')
|
||||
}))
|
||||
await jail.set('__randomBytesHex', new ivm.Callback((size) => {
|
||||
log(`lx.utils.crypto.randomBytes(${size})`)
|
||||
log(`source.utils.crypto.randomBytes(${size})`)
|
||||
return crypto.randomBytes(size).toString('hex')
|
||||
}))
|
||||
await jail.set('__aesEncryptHex', new ivm.Callback((hexBuf, mode, hexKey, hexIv) => {
|
||||
log(`lx.utils.crypto.aesEncrypt(${Buffer.from(hexBuf, 'hex').length}B, ${mode})`)
|
||||
log(`source.utils.crypto.aesEncrypt(${Buffer.from(hexBuf, 'hex').length}B, ${mode})`)
|
||||
const cipher = crypto.createCipheriv(mode, Buffer.from(hexKey, 'hex'), Buffer.from(hexIv, 'hex'))
|
||||
return Buffer.concat([cipher.update(Buffer.from(hexBuf, 'hex')), cipher.final()]).toString('hex')
|
||||
}))
|
||||
await jail.set('__rsaEncryptHex', new ivm.Callback((hexBuf, pemKey) => {
|
||||
log(`lx.utils.crypto.rsaEncrypt(${Buffer.from(hexBuf, 'hex').length}B, key=${brief(pemKey, 24)})`)
|
||||
log(`source.utils.crypto.rsaEncrypt(${Buffer.from(hexBuf, 'hex').length}B, key=${brief(pemKey, 24)})`)
|
||||
let buf = Buffer.from(hexBuf, 'hex')
|
||||
buf = Buffer.concat([Buffer.alloc(128 - buf.length), buf])
|
||||
return crypto.publicEncrypt({ key: pemKey, padding: crypto.constants.RSA_NO_PADDING }, buf).toString('hex')
|
||||
}))
|
||||
await jail.set('__zlib', new ivm.Callback((opId, op, hex) => {
|
||||
log(`lx.utils.zlib.${op}(${Buffer.from(hex, 'hex').length}B)`)
|
||||
log(`source.utils.zlib.${op}(${Buffer.from(hex, 'hex').length}B)`)
|
||||
const buf = Buffer.from(hex, 'hex')
|
||||
zlib[op](buf, (err, out) => {
|
||||
safeApply(zlibDoneRef, [opId, err ? String(err.message) : null, err ? '' : out.toString('hex')])
|
||||
@@ -488,11 +488,11 @@ export const createIvmLxSource = async({
|
||||
const opts = JSON.parse(optionsJson)
|
||||
const method = String(opts.method || 'get').toUpperCase()
|
||||
if (!httpAllowed(url)) {
|
||||
log(`✋ lx.request ${method} ${url} → 已阻止(不在白名单)`)
|
||||
log(`✋ source.request ${method} ${url} → 已阻止(不在白名单)`)
|
||||
safeApply(completeHttpRef, [cbId, `http blocked by allowlist: ${url}`, null, null])
|
||||
return reqId
|
||||
}
|
||||
log(`→ lx.request ${method} ${brief(url, 120)}`)
|
||||
log(`→ source.request ${method} ${brief(url, 120)}`)
|
||||
const startedAt = Date.now()
|
||||
const options = {
|
||||
method: opts.method || 'get',
|
||||
@@ -506,12 +506,12 @@ export const createIvmLxSource = async({
|
||||
if (!dispatchHttp.has(dispatchId)) dispatchHttp.set(dispatchId, new Set())
|
||||
dispatchHttp.get(dispatchId).add(requestObj)
|
||||
requestObj.promise.then(({ statusCode, statusMessage, headers, body }) => {
|
||||
log(`← lx.request ${method} ${brief(url, 80)} → ${statusCode}(${Date.now() - startedAt}ms)`)
|
||||
log(`← source.request ${method} ${brief(url, 80)} → ${statusCode}(${Date.now() - startedAt}ms)`)
|
||||
forgetHttp(reqId, dispatchId, requestObj)
|
||||
const resp = { statusCode, statusMessage: statusMessage || '', headers: headers || {}, body }
|
||||
return safeApply(completeHttpRef, [cbId, null, JSON.stringify(resp), JSON.stringify(body === undefined ? null : body)])
|
||||
}).catch(err => {
|
||||
log(`← lx.request ${method} ${brief(url, 80)} → 失败(${Date.now() - startedAt}ms)${err?.message ? ' ' + err.message : ''}`)
|
||||
log(`← source.request ${method} ${brief(url, 80)} → 失败(${Date.now() - startedAt}ms)${err?.message ? ' ' + err.message : ''}`)
|
||||
forgetHttp(reqId, dispatchId, requestObj)
|
||||
return safeApply(completeHttpRef, [cbId, String(err && err.message || err), null, null])
|
||||
})
|
||||
@@ -639,4 +639,4 @@ export const createIvmLxSource = async({
|
||||
}
|
||||
}
|
||||
|
||||
export default { createIvmLxSource }
|
||||
export default { createIvmSource }
|
||||
@@ -0,0 +1,76 @@
|
||||
// 外部聚合音源统一入口(isolated-vm 懒加载封装)
|
||||
// 内部委托 source-ivm.js 在硬隔离沙箱中执行音源脚本。
|
||||
// 首次调用 getMusicUrl 时才加载并初始化隔离环境,后续复用同一实例。
|
||||
// 统一接口约定:source.kw.getMusicUrl(songInfo, type) → { promise, cancelHttp },resolve → { type, url }
|
||||
//
|
||||
// 脚本来源(优先级):
|
||||
// 1. 环境变量 SOURCE_SCRIPT_PATH 指定的脚本文件
|
||||
// 2. sources/ 目录下按文件名排序的第一个 .js(脚本文件不入库,见 .gitignore)
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createIvmSource } from './source-ivm.js'
|
||||
|
||||
export const SOURCES = ['kw', 'kg', 'tx', 'wy', 'mg']
|
||||
|
||||
const SOURCES_DIR = fileURLToPath(new URL('../../sources/', import.meta.url))
|
||||
|
||||
const resolveScriptPath = () => {
|
||||
const custom = process.env.SOURCE_SCRIPT_PATH
|
||||
if (custom) return custom
|
||||
if (!fs.existsSync(SOURCES_DIR)) {
|
||||
throw new Error(`音源目录不存在: ${SOURCES_DIR}(将脚本放入 sources/ 目录,或用 SOURCE_SCRIPT_PATH 指定)`)
|
||||
}
|
||||
const files = fs.readdirSync(SOURCES_DIR).filter(f => f.toLowerCase().endsWith('.js')).sort()
|
||||
if (!files.length) {
|
||||
throw new Error('sources/ 目录下没有 .js 音源脚本(将脚本放入该目录,或用 SOURCE_SCRIPT_PATH 指定)')
|
||||
}
|
||||
if (files.length > 1) {
|
||||
console.log(`[source] sources/ 下有 ${files.length} 个脚本,使用 ${files[0]}(可用 SOURCE_SCRIPT_PATH 指定)`)
|
||||
}
|
||||
return path.join(SOURCES_DIR, files[0])
|
||||
}
|
||||
|
||||
let sharedPromise = null
|
||||
|
||||
const getShared = () => {
|
||||
if (!sharedPromise) {
|
||||
// 配置可经环境变量调整,与项目其它适配器保持一致风格
|
||||
sharedPromise = createIvmSource({
|
||||
scriptPath: resolveScriptPath(),
|
||||
log: process.env.SOURCE_LOG !== '0',
|
||||
allowAllHttp: process.env.SOURCE_ALLOW_ALL_HTTP === '1',
|
||||
}).catch(err => {
|
||||
sharedPromise = null // 失败允许下次重试
|
||||
throw err
|
||||
})
|
||||
}
|
||||
return sharedPromise
|
||||
}
|
||||
|
||||
const apiFor = (source) => ({
|
||||
getMusicUrl(songInfo, type) {
|
||||
const requestObj = { cancelHttp() {}, promise: null }
|
||||
let inner = null
|
||||
requestObj.cancelHttp = () => inner?.cancelHttp()
|
||||
requestObj.promise = (async () => {
|
||||
const api = await getShared()
|
||||
if (!api[source]) throw new Error(`脚本不支持的平台: ${source}`)
|
||||
inner = api[source].getMusicUrl(songInfo, type)
|
||||
return inner.promise
|
||||
})()
|
||||
return requestObj
|
||||
},
|
||||
})
|
||||
|
||||
// 兼容导出:musicSdk/api-source.js 以 createApi(source) 方式接入聚合音源
|
||||
export const createApi = apiFor
|
||||
|
||||
const sourceApi = {}
|
||||
for (const s of SOURCES) sourceApi[s] = apiFor(s)
|
||||
|
||||
// 预热:提前触发脚本加载与初始化(可选调用)
|
||||
sourceApi.init = getShared
|
||||
|
||||
export default sourceApi
|
||||
@@ -1,4 +1,4 @@
|
||||
// IPC 事件名(对齐原 lx-music src/common/ipcNames.ts)
|
||||
// IPC 事件名
|
||||
|
||||
export const WIN_MAIN_RENDERER_EVENT_NAME = {
|
||||
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 歌词解码(替代原 lx-music 主进程 IPC 处理器)
|
||||
// 歌词解码(原主进程 IPC 处理器的纯 JS 实现)
|
||||
// - handle_kw_decode_lyric: 酷我歌词(inflate + yeelion XOR + gb18030)
|
||||
// 算法来源: lx-music-desktop src/main/modules/winMain/rendererEvent/kw_decodeLyric.ts
|
||||
// 算法来源: kw 桌面端歌词解码实现
|
||||
// - handle_tx_decode_lyric: QQ音乐歌词(qrc 解码:hex → XOR 偏移 → deflate)
|
||||
// qrc 算法来源: 开源 qrc 解码器(ZhuYiQing/qrcDecode 纯 JS 版)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// 酷狗 krc 歌词解码(对齐原 lx-music src/common/utils/lyricUtils/kg.js)
|
||||
// 酷狗 krc 歌词解码
|
||||
|
||||
import zlib from 'node:zlib'
|
||||
import { decodeName } from '../utils.js'
|
||||
|
||||
// https://github.com/lyswhut/lx-music-desktop/issues/296#issuecomment-683285784
|
||||
// 基于社区通用的 krc 解析思路
|
||||
const enc_key = Buffer.from([0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69], 'binary')
|
||||
|
||||
const decodeLyric = str => new Promise((resolve, reject) => {
|
||||
@@ -42,7 +42,7 @@ const parseLyric = str => {
|
||||
}
|
||||
}
|
||||
let i = 0
|
||||
let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
|
||||
let yrc = str.replace(/\[((\d+),\d+)\].*/g, str => {
|
||||
let result = str.match(/\[((\d+),\d+)\].*/)
|
||||
let time = parseInt(result[2])
|
||||
let ms = time % 1000
|
||||
@@ -58,16 +58,16 @@ const parseLyric = str => {
|
||||
})
|
||||
rlyric = rlyric ? rlyric.join('\n') : ''
|
||||
tlyric = tlyric ? tlyric.join('\n') : ''
|
||||
lxlyric = lxlyric.replace(/<(\d+,\d+),\d+>/g, '<$1>')
|
||||
lxlyric = decodeName(lxlyric)
|
||||
lyric = lxlyric.replace(/<\d+,\d+>/g, '')
|
||||
yrc = yrc.replace(/<(\d+,\d+),\d+>/g, '<$1>')
|
||||
yrc = decodeName(yrc)
|
||||
lyric = yrc.replace(/<\d+,\d+>/g, '')
|
||||
rlyric = decodeName(rlyric)
|
||||
tlyric = decodeName(tlyric)
|
||||
return {
|
||||
lyric,
|
||||
tlyric,
|
||||
rlyric,
|
||||
lxlyric,
|
||||
yrc,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 请求提示消息(替代原 lx-music 的 message.js)
|
||||
// 请求提示消息
|
||||
|
||||
export const requestMsg = {
|
||||
cancelRequest: '请求已取消',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 替代原 lx-music 的 @common/rendererIpc(Electron ipcRenderer.invoke)
|
||||
// 渲染器 IPC 桥(替代 Electron ipcRenderer.invoke)
|
||||
// 服务器环境下 IPC 调用直接映射到本地实现:
|
||||
// handle_kw_decode_lyric → 酷我歌词解码(见 lyricDecode.js)
|
||||
// handle_tx_decode_lyric → QQ音乐歌词解码(见 lyricDecode.js)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 网络请求模块(替代原 lx-music 渲染器 src/renderer/utils/request.js)
|
||||
// 网络请求模块
|
||||
// 接口与原版保持一致:
|
||||
// httpFetch(url, { method, headers, body, form, timeout, lookup, family })
|
||||
// → { promise, cancelHttp }
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// 通用工具函数(对齐 lx-music 原版 src/common/utils/common.ts 与 src/renderer/utils/index.ts)
|
||||
// 通用工具函数
|
||||
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// SDK 根模块 —— musicSdk 内 `import xxx from '../../index.js'` 的解析目标
|
||||
// 聚合导出通用工具函数(对齐原 lx-music src/renderer/utils/index.ts 的导出)
|
||||
// 聚合导出通用工具函数
|
||||
|
||||
export {
|
||||
sizeFormate,
|
||||
|
||||
@@ -78,7 +78,7 @@ export const COMPONENT_SCHEMAS = {
|
||||
lyric: { type: 'string', description: '原文歌词(LRC)' },
|
||||
tlyric: { type: 'string', description: '翻译歌词' },
|
||||
rlyric: { type: 'string', description: '罗马音歌词(部分平台)' },
|
||||
lxlyric: { type: 'string', description: '逐字歌词(部分平台)' },
|
||||
yrc: { type: 'string', description: '逐字歌词(部分平台)' },
|
||||
},
|
||||
additionalProperties: true,
|
||||
required: ['lyric', 'tlyric'],
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// 替代原 lx-music 渲染器的 @renderer/store
|
||||
// 替代原渲染器的 @renderer/store 模块(保留导入别名所需的接口形态)
|
||||
// 服务器环境默认使用内置直连接口(apiSource.value === 'internal'),
|
||||
// 设置 LX_SOURCE_ENABLED=true 可切换至 lx-music-source-v5 聚合源。
|
||||
// 设置 SOURCE_ENABLED 环境变量(任意非 0/false 值)即启用外部聚合音源。
|
||||
|
||||
const enabled = process.env.SOURCE_ENABLED
|
||||
|
||||
export const apiSource = {
|
||||
value: process.env.LX_SOURCE_ENABLED === 'true' ? 'v5' : 'internal',
|
||||
value: enabled && enabled !== '0' && enabled !== 'false' ? 'source' : 'internal',
|
||||
}
|
||||
|
||||
export const userApi = {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// 替代原 lx-music 渲染器的 @renderer/utils(SDK 内 `@renderer/utils` 别名目标)
|
||||
// 渲染器工具模块(SDK 内 `@renderer/utils` 别名目标)
|
||||
|
||||
export { decodeName, formatPlayTime, sizeFormate, dateFormat, dateFormat2, formatPlayCount } from '../common/utils.js'
|
||||
Reference in new issue
Block a user