Files
YMhut-box-C-/third_party/MoeKoeMusic/api/util/util.js
T

311 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @fileoverview 通用工具函数库
*
* 提供酷狗音乐 API 项目中常用的工具函数,包括:
* - 随机字符串/数字生成
* - Cookie 解析与格式化
* - KRC 歌词解码(XOR 解密 + zlib 解压)
* - 设备 MID 计算(基于 MD5 的大整数转换)
* - GUID 生成(UUID v4 格式)
* - WebGL 指纹哈希生成(浏览器/Node 双环境支持)
*
* @module util
* @requires pako - zlib 解压库(用于 KRC 歌词解码)
* @requires crypto-js - 加密库(用于 MID 计算中的 MD5 哈希)
* @requires big-integer - 大整数库(用于 MID 的进制转换)
*/
const pako = require('pako');
const CryptoJS = require('crypto-js');
const bigInt = require('big-integer');
/**
* 生成随机字符串(大写字母 + 数字)
*
* 字符池: 1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ36个字符)
* 每次从字符池中随机选取一个字符,拼接为指定长度的字符串。
*
* @param {number} [len=16] - 字符串长度,默认 16
* @returns {string} 随机字符串
*
* @example
* randomString(8) // => "A3B7K9X2"
*/
const randomString = (len = 16) => {
const keyString = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const _key = [];
const keyStringArr = keyString.split('');
for (let i = 0; i < len; i += 1) {
const ceil = Math.ceil((keyStringArr.length - 1) * Math.random());
const _tmp = keyStringArr[ceil];
_key.push(_tmp);
}
return _key.join('');
};
/**
* 生成随机数字字符串
*
* 字符池: 123456789010个数字字符)
* 每次从字符池中随机选取一个数字字符,拼接为指定长度的字符串。
*
* @param {number} [len=16] - 字符串长度,默认 16
* @returns {string} 随机数字字符串
*
* @example
* randomNumber(6) // => "384729"
*/
const randomNumber = (len = 16) => {
const keyString = '1234567890';
const _key = [];
const keyStringArr = keyString.split('');
for (let i = 0; i < len; i += 1) {
const ceil = Math.ceil((keyStringArr.length - 1) * Math.random());
const _tmp = keyStringArr[ceil];
_key.push(_tmp);
}
return _key.join('');
};
/**
* 格式化 Cookie 字符串
*
* 移除 Cookie 中的非数据字段(Domain、path、expires、HttpOnly),
* 仅保留有效的键值对数据。
*
* @param {string} cookie - 原始 Cookie 字符串
* @returns {string} 格式化后的 Cookie 字符串
*
* @example
* parseCookieString('token=abc; Domain=.kugou.com; path=/; HttpOnly')
* // => 'token=abc'
*/
const parseCookieString = (cookie) => {
const t = cookie.replace(/\s*(Domain|domain|path|expires)=[^(;|$)]+;*/g, '');
return t.replace(/;HttpOnly/g, '');
};
/**
* Cookie 字符串转 JSON 对象
*
* 将 Cookie 字符串按 `;` 分割,每个键值对按 `=` 分割为 key 和 value。
*
* @param {string} cookie - Cookie 字符串
* @returns {Object} Cookie 键值对对象
*
* @example
* cookieToJson('token=abc; userid=123')
* // => { token: 'abc', userid: '123' }
*/
const cookieToJson = (cookie) => {
if (!cookie) return {};
let cookieArr = cookie.split(';');
let obj = {};
cookieArr.forEach((i) => {
let arr = i.split('=');
obj[arr[0]] = arr[1];
});
return obj;
};
/**
* KRC 歌词解码
*
* 酷狗 KRC 歌词文件的解码流程:
* 1. 跳过前 4 字节(文件头标识)
* 2. 剩余字节与固定密钥进行 XOR 异或解密
* 3. 使用 pako(zlib)解压得到明文歌词
*
* XOR 密钥(16字节循环使用):
* [64, 71, 97, 119, 94, 50, 116, 71, 81, 54, 49, 45, 206, 210, 110, 105]
*
* @param {string | Uint8Array | Buffer} val - 加密的歌词数据
* - string: Base64 编码的歌词数据
* - Uint8Array: 原始字节数组
* - Buffer: Node.js Buffer
* @returns {string} 解码后的明文歌词,解码失败返回空字符串
*/
const decodeLyrics = (val) => {
let bytes = null;
if (val instanceof Uint8Array) bytes = val;
if (Buffer.isBuffer(val)) bytes = new Uint8Array(val);
if (typeof val === 'string') bytes = new Uint8Array(Buffer.from(val, 'base64'));
if (bytes === null) return '';
// XOR 解密密钥(16字节,循环使用)
const enKey = [64, 71, 97, 119, 94, 50, 116, 71, 81, 54, 49, 45, 206, 210, 110, 105];
const krcBytes = bytes.slice(4); // 跳过前 4 字节文件头
const len = krcBytes.byteLength;
// XOR 异或解密
for (let index = 0; index < len; index += 1) {
krcBytes[index] = krcBytes[index] ^ enKey[index % enKey.length];
}
// zlib 解压
try {
const inflate = pako.inflate(krcBytes);
return Buffer.from(inflate).toString('utf8');
} catch {
return '';
}
};
/**
* 计算设备 MID
*
* 将输入字符串(通常是 GUID)进行 MD5 哈希,然后将哈希值作为 16 进制大整数
* 转换为 10 进制字符串表示。
*
* 算法:
* 1. 对输入字符串取 MD5 哈希(32位hex)
* 2. 将 hex 字符串视为 16 进制数
* 3. 逐位累加: sum += digit * 16^(position)
* 4. 返回十进制字符串
*
* @param {string} str - 输入字符串(通常为设备 GUID)
* @returns {string} MID 的十进制字符串表示
*
* @example
* calculateMid('550e8400-e29b-41d4-a716-446655440000')
* // => "123456789012345678901234567890"
*/
const calculateMid = (str) => {
let bigInteger = bigInt(0);
const bigInteger2 = bigInt(16); // 进制基数
const digest = CryptoJS.MD5(str).toString(CryptoJS.enc.Hex); // MD5 哈希
const length = digest.length;
for (let i = 0; i < length; i += 1) {
const charValue = bigInt(parseInt(digest.charAt(i), 16)); // 当前位的值
const powerValue = bigInteger2.pow(length - 1 - i); // 16 的幂次
bigInteger = bigInteger.add(charValue.multiply(powerValue)); // 累加
}
return bigInteger.toString();
};
/**
* 生成随机 GUIDUUID v4 格式)
*
* 格式: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
* - 第三段以 4 开头(UUID v4 标识)
* - 第四段以 8/9/a/b 开头(UUID v4 变体标识)
*
* @returns {string} UUID v4 格式的 GUID 字符串
*
* @example
* getGuid() // => "550e8400-e29b-41d4-a716-446655440000"
*/
const getGuid = () => {
const e = () => {
return ((65536 * (1 + Math.random())) | 0).toString(16).substring(1);
};
return `${e()}${e()}-${e()}-${e()}-${e()}-${e()}${e()}${e()}`;
};
/**
* 生成 WebGL 指纹哈希值
*
* WebGL 指纹是浏览器指纹的重要组成部分,通过以下信息生成唯一哈希:
*
* 浏览器环境:
* 1. 编译顶点/片段着色器,创建 WebGL 程序
* 2. 绘制一个三角形并读取像素数据
* 3. 获取显卡厂商、渲染器名称、WebGL 版本等元数据
* 4. 使用 FNV-1a 64-bit 哈希算法对像素数据 + 元数据进行哈希
*
* Node 环境或 WebGL 不可用时:
* 生成随机 uint64 作为模拟指纹
*
* @returns {string} WebGL 指纹的十进制字符串表示
*/
const generateWebGLHash = () => {
// 浏览器环境:通过 canvas 获取真实的 WebGL 渲染器信息
if (typeof document !== 'undefined') {
try {
const c = document.createElement('canvas');
c.width = 200;
c.height = 50;
const gl = c.getContext('webgl') || c.getContext('experimental-webgl');
if (gl) {
// --- 编译着色器(和 WASM 中的逻辑一致)---
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, 'attribute vec4 position;void main(){gl_Position=position;}');
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, 'void main(){gl_FragColor=vec4(1.0,1.0,1.0,1.0);}');
gl.compileShader(fs);
const prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
gl.useProgram(prog);
// --- 绘制一个三角形 ---
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1]), gl.STATIC_DRAW);
const pos = gl.getAttribLocation(prog, 'position');
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
gl.viewport(0, 0, 200, 50);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// --- 读取渲染结果并哈希 ---
const pixels = new Uint8Array(200 * 50 * 4);
gl.readPixels(0, 0, 200, 50, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
// 同时加入元数据(显卡厂商、渲染器名称、WebGL 版本)
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
const vendor = dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : '';
const renderer = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : '';
const version = gl.getParameter(gl.VERSION);
// FNV-1a 64-bit 哈希算法
let h = BigInt('14695981039346656037'); // FNV offset basis
const prime = BigInt('1099511628211'); // FNV prime
// 哈希像素数据
for (let i = 0; i < pixels.length; i++) {
h = ((h ^ BigInt(pixels[i])) * prime) & BigInt('0xFFFFFFFFFFFFFFFF');
}
// 哈希元数据
const meta = vendor + '|' + renderer + '|' + version;
for (let i = 0; i < meta.length; i++) {
h = ((h ^ BigInt(meta.charCodeAt(i))) * prime) & BigInt('0xFFFFFFFFFFFFFFFF');
}
return h.toString();
}
} catch (e) {}
}
// Node 环境或 WebGL 不可用:生成随机 uint64 作为模拟指纹
const hi = Math.floor(Math.random() * 0xffffffff);
const lo = Math.floor(Math.random() * 0xffffffff);
return (BigInt(hi) * BigInt(0x100000000) + BigInt(lo)).toString();
};
/**
* 判断是否为UUID v4
* @param {string} str
* @returns { Boolean }
*/
const isUUIDv4 = (str) => {
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(str);
}
module.exports = {
decodeLyrics,
cookieToJson,
parseCookieString,
randomString,
randomNumber,
calculateMid,
getGuid,
generateWebGLHash,
isUUIDv4
};