/** * 插件格式自动检测模块 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 };