refactor!: 移除品牌字样,音源改为 sources/ 目录自动加载,脚本不入库
This commit is contained in:
1 parent
f548e5990b
commit
5dd0468860
34 files changed
+256
-232
No files matched your search
+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