Files
koneko-music-tester/lib/detector.js

752 lines
27 KiB
JavaScript
Raw Permalink 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.
/**
* 插件格式自动检测模块 v3.0
* 支持: Lx Music / QZV2 (QZ PC + 澜音Ceru) / MusicFree
* 核心改进: 深度逆向分析 QZ 插件结构,支持混淆代码解析
*
* 逆向分析来源:
* - QZMusic_PC/src/main/pluginSystem.ts (GitHub: lqtmcstudio/QZMusic_PC)
* - CeruMusic 插件开发文档 (ceru.docs.shiqianjiang.cn)
* - 实际 QZ 插件样本 (mg/wy/tx/kw/kg 平台)
*/
const PLATFORMS = {
LX: 'lx',
QZV2: 'qzv2',
MUSICFREE: 'musicfree',
UNKNOWN: 'unknown'
};
const PLATFORM_LABELS = {
lx: '落雪音乐 (Lx Music)',
qzv2: '清泽音乐 (QZV2)',
musicfree: 'MusicFree',
unknown: '未知格式'
};
/**
* 音乐源平台映射表
* key = pluginInfo.info.id 或 Lx Music source key
* 逆向来源: pluginSystem.ts getPluginId() + 澜音文档 sources 定义
*/
const MUSIC_SOURCES = {
'kw': { name: '酷我音乐', fullName: 'KuWo Music', domains: ['kuwo.cn', 'kuwo.com'] },
'tx': { name: 'QQ音乐', fullName: 'QQ Music', domains: ['qq.com', 'y.qq.com', 'c.y.qq.com', 'u.y.qq.com'] },
'wy': { name: '网易云音乐', fullName: 'NetEase Music', domains: ['163.com', 'music.126.net', 'music.163.com', 'interface.music.163.com'] },
'kg': { name: '酷狗音乐', fullName: 'KuGou Music', domains: ['kugou.com', 'kugou.cn'] },
'mg': { name: '咪咕音乐', fullName: 'Migu Music', domains: ['migu.cn', 'miguvideo', 'jadeite.migu.cn', 'musicapp.cn', 'nf.migu.cn'] },
'local': { name: '本地音乐', fullName: 'Local', domains: [] },
};
/**
* QZ 插件 supportFunc 已知值映射
* 逆向来源: 实际插件样本 + pluginSystem.ts callCandidate 机制
*/
const SUPPORT_FUNC_NAMES = {
'search_song': '歌曲搜索',
'search_playlist': '歌单搜索',
'search_album': '专辑搜索',
'playlist': '歌单详情',
'album': '专辑详情',
'lyric': '歌词获取',
'search': '搜索',
'getUrl': '获取播放链接',
'getLyric': '获取歌词',
'getPlaylist': '获取歌单',
'getAlbum': '获取专辑',
'musicSearch': '音乐搜索',
'hotSearch': '热搜',
'songList': '歌单列表',
};
/**
* 音质等级完整映射表
* 逆向来源: QZ Flutter main.dart 枚举 + 澜音文档 qualitys 定义 + 实际插件样本
*/
const QUALITY_MAP = {
'128k': { name: '标准音质', shortName: '标准', level: 1 },
'192k': { name: '较高音质', shortName: '较高', level: 2 },
'320k': { name: '高品音质', shortName: '高品', level: 3 },
'flac': { name: '无损音质', shortName: '无损', level: 4 },
'flac24bit': { name: 'Hi-Res FLAC', shortName: 'Hi-Res', level: 5 },
'hires': { name: 'Hi-Res 高解析度', shortName: 'Hi-Res', level: 5 },
'atmos': { name: '杜比全景声', shortName: '全景声', level: 6 },
'master': { name: '母带音质', shortName: '母带', level: 7 },
'low': { name: '低音质', shortName: '低', level: 0 },
'standard': { name: '标准音质', shortName: '标准', level: 1 },
'exhigh': { name: '极高音质', shortName: '极高', level: 3 },
'high': { name: '高音质', shortName: '高', level: 3 },
'lossless': { name: '无损音质', shortName: '无损', level: 4 },
'super': { name: '超高音质', shortName: '超高', level: 5 },
};
/** 所有支持的平台信息(用于前端展示) */
const ALL_PLATFORMS = [
{
id: 'lx',
name: '落雪音乐',
fullName: 'Lx Music',
format: 'JavaScript 脚本 (事件驱动)',
qualities: ['128k', '192k', '320k', 'flac', 'flac24bit', 'hires', 'master'],
features: ['musicUrl', 'lyric', 'pic'],
sources: ['kw', 'kg', 'tx', 'wy', 'mg'],
detectionHints: ['globalThis.lx', 'EVENT_NAMES', '@name 注释头', 'sources/qualitys 对象'],
docUrl: 'https://lxmusic.toside.cn/desktop/custom-source'
},
{
id: 'qzv2',
name: '清泽音乐',
fullName: 'QZ Music v2',
format: 'JS 模块 (ZIP/JS/QZ) - webpack 打包',
qualities: ['128k', '320k', 'flac', 'hires', 'atmos', 'master', 'standard', 'exhigh', 'lossless'],
features: ['search_song', 'search_playlist', 'playlist', 'album', 'lyric', 'getUrl', 'musicSearch'],
sources: ['kw', 'tx', 'wy', 'kg', 'mg'],
detectionHints: ['pluginInfo.info.id', 'getUrl(id, quality)', 'supportFunc', 'musicSearch', '__nccwpck_require__'],
docUrl: 'https://music.qz.shiqianjiang.cn/'
},
{
id: 'musicfree',
name: 'MusicFree',
fullName: 'MusicFree',
format: 'CommonJS 模块',
qualities: ['low', 'standard', 'high', 'super', 'hires', 'master'],
features: ['search', 'getMediaSource', 'getMusicInfo', 'getLyric', 'getAlbumInfo'],
sources: ['自定义'],
detectionHints: ['platform:', 'getMediaSource', 'supportedSearchType', 'srcUrl'],
docUrl: 'https://musicfree.catcat.work/plugin/protocol.html'
}
];
// ============================================================
// QZ pluginInfo 提取 - 核心逆向分析逻辑
// ============================================================
/**
* 尝试从代码中提取 pluginInfo 对象 (QZV2 格式)
* 使用平衡括号匹配算法提取 JSON 子串
* 支持: webpack 打包代码、混淆代码
*/
function extractPluginInfo(code) {
const patterns = [
/pluginInfo\s*:\s*\{/,
/["']pluginInfo["']\s*:\s*\{/,
/pluginInfo\s*=\s*\{/,
];
for (const pattern of patterns) {
const match = code.match(pattern);
if (!match) continue;
const startIdx = match.index + match[0].length - 1;
const result = extractBalancedObject(code, startIdx);
if (result) {
try {
const obj = eval('(' + result + ')');
if (obj && typeof obj === 'object') return obj;
} catch (e) {
try {
const fixed = result.replace(/,\s*([}\]])/g, '$1');
const obj = JSON.parse(fixed);
if (obj && typeof obj === 'object') return obj;
} catch (e2) {
// 混淆代码: 尝试部分提取
const partial = extractPluginInfoPartial(result);
if (partial) return partial;
}
}
}
}
return null;
}
/**
* 从混淆的 pluginInfo 字符串中提取可读信息
* 混淆代码中 info.id 通常是明文字符串
*/
function extractPluginInfoPartial(rawStr) {
const partial = { info: {}, quality: [], supportFunc: [], env: [], ext: [] };
// 提取 info.id (通常是明文)
const idMatch = rawStr.match(/['"]?id['"]?\s*:\s*['"]([a-z0-9_-]+)['"]/i);
if (idMatch) partial.info.id = idMatch[1];
// 提取 info.version
const versionMatch = rawStr.match(/['"]?version['"]?\s*:\s*['"]?(\d[\d.]*)/);
if (versionMatch) partial.info.version = versionMatch[1];
// 提取 info.name (仅提取明文字符串,跳过混淆变量引用)
const nameMatch = rawStr.match(/['"]?name['"]?\s*:\s*['"`]([^'"`]+)['"`]/);
if (nameMatch && !nameMatch[1].startsWith('_0x') && !nameMatch[1].match(/^[a-f0-9]{6,}$/i)) {
partial.info.name = nameMatch[1];
}
// 提取 quality 中的明文 id
const qualityIdMatches = rawStr.matchAll(/['"]?id['"]?\s*:\s*['"]([a-z0-9_]+)['"]/gi);
for (const m of qualityIdMatches) {
if (QUALITY_MAP[m[1]]) {
partial.quality.push({ id: m[1], name: QUALITY_MAP[m[1]].name, ui: '' });
}
}
// 提取 supportFunc 中的明文字符串
for (const funcName of Object.keys(SUPPORT_FUNC_NAMES)) {
const regex = new RegExp(`['"]${funcName}['"]`);
if (regex.test(rawStr)) {
partial.supportFunc.push(funcName);
}
}
if (partial.info.id || partial.quality.length > 0 || partial.supportFunc.length > 0) {
return partial;
}
return null;
}
/**
* 从指定位置开始提取平衡的 {} 对象
*/
function extractBalancedObject(code, startIdx) {
if (code[startIdx] !== '{') return null;
let depth = 0;
let inString = false;
let stringChar = '';
let escaped = false;
for (let i = startIdx; i < code.length; i++) {
const char = code[i];
if (escaped) { escaped = false; continue; }
if (char === '\\') { escaped = true; continue; }
if (inString) {
if (char === stringChar) inString = false;
continue;
}
if (char === '"' || char === "'" || char === '`') {
inString = true;
stringChar = char;
continue;
}
if (char === '{') depth++;
else if (char === '}') {
depth--;
if (depth === 0) return code.substring(startIdx, i + 1);
}
}
return null;
}
/**
* 从代码中提取 QZ API 端点
* 逆向来源: getUrl 函数模式分析
* 典型模式: axios.get(`https://api.qz.shiqianjiang.cn/music/url?songId=${songId}&quality=${quality}&source=mg`)
*/
function extractQzApiEndpoint(code) {
const result = {
apiUrl: '',
sourceParam: '',
apiKey: '',
rawPattern: '',
};
// 查找 QZ API URL - 支持模板字符串 ${} 变量
// 逆向来源: getUrl 函数中 axios.get(`https://api.qz.shiqianjiang.cn/music/url?songId=${songId}&quality=${quality}&source=mg`)
const apiPatterns = [
// 匹配含 ${} 模板变量的 URL
/https?:\/\/api\.qz\.shiqianjiang\.cn(?:[^\s`"'<>]|\$\{[^}]+\})*/,
/https?:\/\/[a-z0-9.-]+\/music\/url(?:[^\s`"'<>]|\$\{[^}]+\})*/,
/https?:\/\/[a-z0-9.-]+\/api\/music(?:[^\s`"'<>]|\$\{[^}]+\})*/,
];
for (const pattern of apiPatterns) {
const match = code.match(pattern);
if (match) {
result.apiUrl = match[0];
result.rawPattern = 'qz_api';
// 提取 source 参数 - 从 URL 中查找
const sourceMatch = result.apiUrl.match(/source[=:](['"]?)(\w+)\1/);
if (sourceMatch) {
result.sourceParam = sourceMatch[2];
} else {
// 如果 URL 中没有明文 source从代码上下文中查找
// 模式: source=mg 或 source=${variable} 后面的 &source=mg
const contextSourceMatch = code.match(/source[=:]\s*['"]?(wy|tx|kw|kg|mg)['"]?/i);
if (contextSourceMatch) {
result.sourceParam = contextSourceMatch[1];
}
}
// 从代码中查找 X-API-KEY
const apiKeyMatch = code.match(/X-API-KEY['"]?\s*:\s*['"]([^'"]+)['"]/);
if (apiKeyMatch) {
result.apiKey = apiKeyMatch[1];
}
break;
}
}
return result;
}
/**
* 从代码中提取音乐平台直接 API 端点
* 按平台分组,用于延时测试
*/
function extractPlatformApiUrls(code, musicPlatform) {
// 匹配 URL - 支持模板字符串 ${} 变量
const allUrls = code.match(/https?:\/\/(?:[^\s"'`<>)}\]]|\$\{[^}]+\})+/g) || [];
const uniqueUrls = [...new Set(allUrls)];
// 排除无关 URL
const excludePatterns = [
/w3\.org/i, /schema\.org/i, /github\.com/i, /nodejs\.org/i,
/mozilla\.org/i, /google\.com/i, /localhost/i, /encoding\.spec/i,
/icu-project\.org/i, /tools\.ietf\.org/i, /haible\.de/i,
/moztw\.org/i, /khngai\.com/i, /abelcheung\.org/i,
/\.png$|\.jpg$|\.jpeg$|\.gif$|\.svg$|\.ico$|\.css$|\.woff$|\.ttf$/i,
/catcat\.work/i, /lxmusic\.toside\.cn/i, /qz\.shiqianjiang/i,
/wikipedia\.org/i, /doubaocdn/i, /aka\.doubaocdn/i,
/unpkg\.com/i, /cdn\.jsdelivr/i, /fonts\.googleapis/i,
/creativecommons/i, /gnu\.org/i, /opensource\.org/i,
];
let filtered = uniqueUrls.filter(u => !excludePatterns.some(p => p.test(u)));
// 如果有音乐平台信息,优先保留与平台域名匹配的 URL
if (musicPlatform && MUSIC_SOURCES[musicPlatform]) {
const domains = MUSIC_SOURCES[musicPlatform].domains;
if (domains.length > 0) {
const platformUrls = filtered.filter(u => {
const lower = u.toLowerCase();
return domains.some(d => lower.includes(d));
});
if (platformUrls.length > 0) {
// 同时保留 QZ API URL 和平台直连 URL
const qzApiUrls = filtered.filter(u => /api\.qz\.shiqianjiang/i.test(u));
filtered = [...new Set([...qzApiUrls, ...platformUrls])];
}
}
}
// 去掉模板字符串中的 ${} 变量,替换为示例值
const cleanedUrls = filtered.map(u => {
return u.replace(/\$\{[^}]+\}/g, 'test').replace(/\$\w+/g, 'test');
});
return [...new Set(cleanedUrls)].slice(0, 10);
}
// ============================================================
// 格式检测主函数
// ============================================================
function detectPlatform(code, filename = '') {
const scores = { lx: 0, qzv2: 0, musicfree: 0 };
// === Lx Music 检测 ===
if (/\/\*\*[\s\S]*?@name\s+/m.test(code)) scores.lx += 2;
if (/globalThis\.lx\b/.test(code)) scores.lx += 3;
if (/EVENT_NAMES/.test(code)) scores.lx += 2;
if (/send\s*\(\s*EVENT_NAMES\.inited/.test(code)) scores.lx += 3;
if (/lx\.(request|on|send)\b/.test(code)) scores.lx += 2;
if (/qualitys\s*:/.test(code)) scores.lx += 2; // 澜音格式特征
if (/cerumusic\.(request|utils)/.test(code)) scores.lx += 3; // 澜音原生
if (/type\s*:\s*['"]cr['"]/.test(code)) scores.lx += 3; // 澜音 type=cr
// === QZV2 检测 (QZ PC 原生格式) ===
if (/pluginInfo\s*[:?]\s*\{/.test(code)) scores.qzv2 += 3;
if (/pluginInfo\s*:\s*\{[\s\S]*?info\s*:/.test(code)) scores.qzv2 += 3; // 嵌套 info
if (/getUrl\s*[:(]/.test(code)) scores.qzv2 += 2;
if (/supportFunc/.test(code)) scores.qzv2 += 3;
if (/__nccwpck_require__/.test(code)) scores.qzv2 += 2; // webpack 打包特征
if (/musicSearch/.test(code)) scores.qzv2 += 2;
if (/search_song|search_playlist|search_album/.test(code)) scores.qzv2 += 2;
if (/getPlay(?:list|List)\s*[:(]/.test(code)) scores.qzv2 += 1;
if (/allPage/.test(code)) scores.qzv2 += 1;
if (/api\.qz\.shiqianjiang\.cn/.test(code)) scores.qzv2 += 3; // QZ 官方 API
if (/X-API-KEY/.test(code)) scores.qzv2 += 1;
// === MusicFree 检测 ===
if (/platform\s*:\s*["'`]/.test(code)) scores.musicfree += 2;
if (/getMediaSource\s*[:(]/.test(code)) scores.musicfree += 3;
if (/getMusicInfo\s*[:(]/.test(code)) scores.musicfree += 2;
if (/getAlbumInfo|getMusicSheetInfo/.test(code)) scores.musicfree += 2;
if (/supportedSearchType/.test(code)) scores.musicfree += 2;
if (/cacheControl/.test(code)) scores.musicfree += 1;
if (/srcUrl/.test(code)) scores.musicfree += 1;
if (/primaryKey/.test(code)) scores.musicfree += 1;
if (/getTopLists|getRecommendSheetTags/.test(code)) scores.musicfree += 2;
// 排除规则
if (scores.musicfree >= 3 && scores.qzv2 > 0) {
if (/getMediaSource/.test(code) && !/pluginInfo/.test(code)) {
scores.qzv2 = Math.max(0, scores.qzv2 - 2);
}
}
let bestPlatform = PLATFORMS.UNKNOWN;
let bestScore = 0;
for (const [platform, score] of Object.entries(scores)) {
if (score > bestScore) { bestScore = score; bestPlatform = platform; }
}
if (bestScore < 2) bestPlatform = PLATFORMS.UNKNOWN;
const metadata = extractMetadata(code, bestPlatform, filename);
return { platform: bestPlatform, platformLabel: PLATFORM_LABELS[bestPlatform], metadata, confidence: bestScore, scores };
}
// ============================================================
// 元数据提取
// ============================================================
function extractMetadata(code, platform, filename = '') {
const metadata = {
name: '',
version: '',
author: '',
description: '',
homepage: '',
quality: [],
qualityDetails: [],
features: [],
sources: [],
musicPlatform: '',
musicPlatformName: '',
apiUrls: [],
filteredApiUrls: [],
qzApiEndpoint: null,
isObfuscated: false,
};
// 检测是否混淆
metadata.isObfuscated = /_0x[0-9a-f]+/.test(code) || code.split('\n').length < 10 && code.length > 50000;
if (platform === 'qzv2') {
extractQZV2Metadata(code, metadata);
} else if (platform === 'lx') {
extractLxMetadata(code, metadata);
} else if (platform === 'musicfree') {
extractMusicFreeMetadata(code, metadata);
}
// 根据 URL 域名推断音乐平台(备用方法)
if (!metadata.musicPlatform) {
const allUrls = code.match(/https?:\/\/[^\s"'`<>)}\]]+/g) || [];
metadata.musicPlatform = detectPlatformFromUrls(allUrls);
if (metadata.musicPlatform && MUSIC_SOURCES[metadata.musicPlatform]) {
metadata.musicPlatformName = MUSIC_SOURCES[metadata.musicPlatform].name;
}
}
// 过滤 API URL
metadata.filteredApiUrls = extractPlatformApiUrls(code, metadata.musicPlatform);
// 提取 QZ API 端点信息
metadata.qzApiEndpoint = extractQzApiEndpoint(code);
if (!metadata.name && filename) {
metadata.name = filename.replace(/\.(js|zip|qz)$/i, '');
}
return metadata;
}
/**
* 提取 QZV2 格式元数据 - 深度解析 pluginInfo
* 支持混淆代码和 webpack 打包代码
*/
function extractQZV2Metadata(code, metadata) {
// 1. 尝试解析完整的 pluginInfo 对象
const pluginInfo = extractPluginInfo(code);
if (pluginInfo) {
// info 字段
if (pluginInfo.info) {
metadata.name = pluginInfo.info.name || '';
metadata.version = String(pluginInfo.info.version || '');
metadata.description = pluginInfo.info.description || '';
metadata.author = pluginInfo.info.author || '';
// 从 info.id 获取音乐平台 (关键!)
// 逆向来源: pluginSystem.ts - info.id 同时作为音源平台标识
const platformId = pluginInfo.info.id;
if (platformId && MUSIC_SOURCES[platformId]) {
metadata.musicPlatform = platformId;
metadata.musicPlatformName = MUSIC_SOURCES[platformId].name;
metadata.sources = [platformId];
} else if (platformId) {
metadata.musicPlatform = platformId;
metadata.musicPlatformName = platformId.toUpperCase();
metadata.sources = [platformId];
}
}
// quality 字段 - 解析完整的音质对象数组
if (Array.isArray(pluginInfo.quality) && pluginInfo.quality.length > 0) {
metadata.qualityDetails = pluginInfo.quality.map(q => ({
id: q.id || '',
name: q.name || (QUALITY_MAP[q.id] ? QUALITY_MAP[q.id].name : q.id),
ui: q.ui || ''
}));
metadata.quality = pluginInfo.quality.map(q => q.id).filter(Boolean);
}
// supportFunc 字段
if (Array.isArray(pluginInfo.supportFunc) && pluginInfo.supportFunc.length > 0) {
metadata.features = pluginInfo.supportFunc;
}
// env 字段
if (Array.isArray(pluginInfo.env) && pluginInfo.env.length > 0) {
metadata.features.push('env_config');
// 提取 env 配置项
metadata.envConfig = pluginInfo.env.map(e => ({
key: e.key || '',
name: e.name || '',
description: e.description || '',
})).filter(e => e.key);
}
// ext 字段
if (Array.isArray(pluginInfo.ext) && pluginInfo.ext.length > 0) {
metadata.features.push('extensions');
// 提取扩展信息
metadata.extConfig = pluginInfo.ext.map(e => ({
name: e.name || '',
description: e.description || '',
entry: e.entry || '',
type: e.type || '',
})).filter(e => e.name);
}
}
// 2. 如果 pluginInfo 解析失败或 info.id 未提取到,尝试从代码中直接搜索
if (!metadata.musicPlatform) {
// 搜索 pluginInfo 附近的 info.id (混淆代码中 id 值仍是明文)
const idPatterns = [
/info['"]?\s*:\s*\{[^}]*?['"]?id['"]?\s*:\s*['"]([a-z]{2})['"]/i,
/['"]?id['"]?\s*:\s*['"]([a-z]{2})['"][^}]*?['"]?version['"]?/i,
/source[=:'"]+\s*['"]?(wy|tx|kw|kg|mg)['"]/i,
];
for (const pattern of idPatterns) {
const match = code.match(pattern);
if (match && MUSIC_SOURCES[match[1]]) {
metadata.musicPlatform = match[1];
metadata.musicPlatformName = MUSIC_SOURCES[match[1]].name;
metadata.sources = [match[1]];
break;
}
}
}
// 3. 正则回退: 提取 name (仅从 pluginInfo 上下文中提取,避免混淆代码误匹配)
if (!metadata.name) {
// 在 pluginInfo 附近的 info 块中查找 name
const infoNameMatch = code.match(/info['"]?\s*:\s*\{[^}]*?['"]?name['"]?\s*:\s*["'`]([^"'`]+)["'`]/);
if (infoNameMatch && !infoNameMatch[1].startsWith('_0x') && !infoNameMatch[1].match(/^[a-f0-9]{6,}$/i)) {
metadata.name = infoNameMatch[1];
}
}
// 4. 正则回退: 提取 version
if (!metadata.version) {
const versionMatch = code.match(/['"]?version['"]?\s*:\s*['"]?(\d[\d.]*)/);
if (versionMatch) metadata.version = versionMatch[1];
}
// 5. 正则回退: 提取 description
if (!metadata.description) {
const descMatch = code.match(/['"]?description['"]?\s*:\s*["'`]([^"'`]+)["'`]/);
if (descMatch && !descMatch[1].startsWith('_0x')) metadata.description = descMatch[1];
}
// 6. 如果还没找到音乐平台,从文件名推断
if (!metadata.musicPlatform && metadata.name) {
const idMatch = metadata.name.match(/\b(kw|tx|wy|kg|mg)\b/i);
if (idMatch) {
const id = idMatch[1].toLowerCase();
if (MUSIC_SOURCES[id]) {
metadata.musicPlatform = id;
metadata.musicPlatformName = MUSIC_SOURCES[id].name;
metadata.sources = [id];
}
}
}
// 7. 如果还没找到音质,使用正则从代码中搜索已知音质 ID
if (metadata.quality.length === 0) {
const qualitySet = new Set();
const qualityIds = Object.keys(QUALITY_MAP);
for (const qid of qualityIds) {
const regex = new RegExp(`["'\`]${qid}["'\`]`);
if (regex.test(code)) qualitySet.add(qid);
}
metadata.quality = [...qualitySet];
metadata.qualityDetails = metadata.quality.map(q => ({
id: q,
name: QUALITY_MAP[q] ? QUALITY_MAP[q].name : q,
ui: ''
}));
}
// 8. 补充功能检测 (从代码中搜索已知 supportFunc 名称)
if (metadata.features.length === 0) {
for (const funcName of Object.keys(SUPPORT_FUNC_NAMES)) {
const regex = new RegExp(`['"]${funcName}['"]`);
if (regex.test(code)) metadata.features.push(funcName);
}
}
// 9. 额外功能检测
if (/getUrl/.test(code) && !metadata.features.includes('getUrl')) metadata.features.push('getUrl');
if (/getLyric/.test(code) && !metadata.features.some(f => f.includes('lyric') || f.includes('Lyric'))) metadata.features.push('getLyric');
if (/musicSearch/.test(code) && !metadata.features.includes('musicSearch')) metadata.features.push('musicSearch');
if (/allPage/.test(code)) metadata.features.push('allPage');
if (/getAlbum/.test(code) && !metadata.features.includes('album')) metadata.features.push('getAlbum');
if (/hotSearch/.test(code) && !metadata.features.includes('hotSearch')) metadata.features.push('hotSearch');
}
/**
* 提取 Lx Music 格式元数据
*/
function extractLxMetadata(code, metadata) {
const nameMatch = code.match(/@name\s+(.+)/);
if (nameMatch) metadata.name = nameMatch[1].trim();
const descMatch = code.match(/@description\s+(.+)/);
if (descMatch) metadata.description = descMatch[1].trim();
const versionMatch = code.match(/@version\s+(.+)/);
if (versionMatch) metadata.version = versionMatch[1].trim();
const authorMatch = code.match(/@author\s+(.+)/);
if (authorMatch) metadata.author = authorMatch[1].trim();
const homeMatch = code.match(/@homepage\s+(.+)/);
if (homeMatch) metadata.homepage = homeMatch[1].trim();
// 澜音格式: pluginInfo 扁平结构
if (!metadata.name) {
const crNameMatch = code.match(/name\s*:\s*['"]([^'"]+)['"]/);
if (crNameMatch) metadata.name = crNameMatch[1];
}
if (!metadata.version) {
const crVersionMatch = code.match(/version\s*:\s*['"]([^'"]+)['"]/);
if (crVersionMatch) metadata.version = crVersionMatch[1];
}
if (!metadata.author) {
const crAuthorMatch = code.match(/author\s*:\s*['"]([^'"]+)['"]/);
if (crAuthorMatch) metadata.author = crAuthorMatch[1];
}
// 音质检测 - 完整等级体系
const qualitySet = new Set();
for (const qid of Object.keys(QUALITY_MAP)) {
const regex = new RegExp(`['"\`]${qid}['"\`]`);
if (regex.test(code)) qualitySet.add(qid);
}
metadata.quality = [...qualitySet];
if (qualitySet.size > 0) {
metadata.qualityDetails = metadata.quality.map(q => ({
id: q, name: QUALITY_MAP[q] ? QUALITY_MAP[q].name : q, ui: ''
}));
}
// 功能检测
if (/musicUrl/.test(code)) metadata.features.push('musicUrl');
if (/lyric/.test(code)) metadata.features.push('lyric');
if (/pic/.test(code)) metadata.features.push('pic');
if (/cerumusic\.request/.test(code)) metadata.features.push('cerumusic_request');
// 音源平台检测 - sources 对象的 key
const sourcePattern = /\b(kw|kg|tx|wy|mg|local)\b\s*:/g;
let match;
const sourceSet = new Set();
while ((match = sourcePattern.exec(code)) !== null) sourceSet.add(match[1]);
metadata.sources = [...sourceSet];
// 设置主音乐平台
for (const s of metadata.sources) {
if (s !== 'local' && MUSIC_SOURCES[s]) {
metadata.musicPlatform = s;
metadata.musicPlatformName = MUSIC_SOURCES[s].name;
break;
}
}
// 多平台标记
if (metadata.sources.filter(s => s !== 'local').length > 1) {
metadata.musicPlatform = 'multi';
metadata.musicPlatformName = '多平台';
}
}
/**
* 提取 MusicFree 格式元数据
*/
function extractMusicFreeMetadata(code, metadata) {
const platformMatch = code.match(/platform\s*:\s*["'`]([^"'`]+)["'`]/);
if (platformMatch) {
metadata.name = platformMatch[1];
const pid = platformMatch[1].toLowerCase();
for (const [key, val] of Object.entries(MUSIC_SOURCES)) {
if (pid.includes(key) || pid.includes(val.name) || (val.fullName && pid.includes(val.fullName.toLowerCase()))) {
metadata.musicPlatform = key;
metadata.musicPlatformName = val.name;
metadata.sources = [key];
break;
}
}
}
const versionMatch = code.match(/version\s*:\s*["'`]([^"'`]+)["'`]/);
if (versionMatch) metadata.version = versionMatch[1];
const authorMatch = code.match(/author\s*:\s*["'`]([^"'`]+)["'`]/);
if (authorMatch) metadata.author = authorMatch[1];
const srcUrlMatch = code.match(/srcUrl\s*:\s*["'`]([^"'`]+)["'`]/);
if (srcUrlMatch) metadata.homepage = srcUrlMatch[1];
// 音质检测
const qualitySet = new Set();
for (const qid of Object.keys(QUALITY_MAP)) {
const regex = new RegExp(`['"\`]${qid}['"\`]`);
if (regex.test(code)) qualitySet.add(qid);
}
metadata.quality = [...qualitySet];
if (qualitySet.size > 0) {
metadata.qualityDetails = metadata.quality.map(q => ({
id: q, name: QUALITY_MAP[q] ? QUALITY_MAP[q].name : q, ui: ''
}));
}
// 功能检测
const featureMap = [
['search', 'search'], ['getMediaSource', 'getMediaSource'], ['getMusicInfo', 'getMusicInfo'],
['getLyric', 'getLyric'], ['getAlbumInfo', 'getAlbumInfo'], ['getMusicSheetInfo', 'getMusicSheetInfo'],
['getArtistWorks', 'getArtistWorks'], ['importMusicItem', 'importMusicItem'],
['importMusicSheet', 'importMusicSheet'], ['getTopLists', 'getTopLists'],
['getTopListDetail', 'getTopListDetail'], ['getRecommendSheetTags', 'getRecommendSheetTags'],
['getRecommendSheetsByTag', 'getRecommendSheetsByTag'], ['getMusicComments', 'getMusicComments']
];
for (const [pattern, label] of featureMap) {
if (new RegExp(pattern).test(code)) metadata.features.push(label);
}
}
/**
* 从 URL 列表推断音乐平台
*/
function detectPlatformFromUrls(urls) {
for (const url of urls) {
const lowerUrl = url.toLowerCase();
for (const [id, info] of Object.entries(MUSIC_SOURCES)) {
if (id === 'local') continue;
for (const domain of info.domains) {
if (lowerUrl.includes(domain)) return id;
}
}
}
return '';
}
module.exports = {
detectPlatform, PLATFORMS, PLATFORM_LABELS, ALL_PLATFORMS, MUSIC_SOURCES,
QUALITY_MAP, SUPPORT_FUNC_NAMES, extractPluginInfo, extractQzApiEndpoint
};