199 lines
9.0 KiB
JavaScript
199 lines
9.0 KiB
JavaScript
|
|
/**
|
||
|
|
* 插件格式自动检测模块
|
||
|
|
* 支持检测: Lx Music / QZV2 / MusicFree 三种插件格式
|
||
|
|
*/
|
||
|
|
|
||
|
|
const PLATFORMS = {
|
||
|
|
LX: 'lx',
|
||
|
|
QZV2: 'qzv2',
|
||
|
|
MUSICFREE: 'musicfree',
|
||
|
|
UNKNOWN: 'unknown'
|
||
|
|
};
|
||
|
|
|
||
|
|
const PLATFORM_LABELS = {
|
||
|
|
lx: '落雪音乐 (Lx Music)',
|
||
|
|
qzv2: '清泽音乐 (QZV2)',
|
||
|
|
musicfree: 'MusicFree',
|
||
|
|
unknown: '未知格式'
|
||
|
|
};
|
||
|
|
|
||
|
|
/** 所有支持的平台信息(用于前端展示) */
|
||
|
|
const ALL_PLATFORMS = [
|
||
|
|
{
|
||
|
|
id: 'lx',
|
||
|
|
name: '落雪音乐',
|
||
|
|
fullName: 'Lx Music',
|
||
|
|
format: 'JavaScript 脚本',
|
||
|
|
qualities: ['128k', '192k', '320k', 'flac', 'flac24bit', 'master'],
|
||
|
|
features: ['musicUrl', 'lyric', 'pic'],
|
||
|
|
sources: ['kw', 'kg', 'tx', 'wy', 'mg', 'local'],
|
||
|
|
detectionHints: ['globalThis.lx', 'EVENT_NAMES', '@name 注释头'],
|
||
|
|
docUrl: 'https://lxmusic.toside.cn/desktop/custom-source'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'qzv2',
|
||
|
|
name: '清泽音乐',
|
||
|
|
fullName: 'QZ Music v2',
|
||
|
|
format: 'JS 模块 (ZIP/JS)',
|
||
|
|
qualities: ['low', 'standard', 'exhigh', 'high', 'lossless', 'hires', 'master'],
|
||
|
|
features: ['search', 'getUrl', 'getLyric', 'getPlaylist', 'getAlbum'],
|
||
|
|
sources: ['自定义'],
|
||
|
|
detectionHints: ['pluginInfo', 'getUrl', 'supportFunc'],
|
||
|
|
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', 'getMusicSheetInfo', 'getArtistWorks', 'getTopLists', 'getRecommendSheetTags', 'getMusicComments'],
|
||
|
|
sources: ['自定义'],
|
||
|
|
detectionHints: ['platform:', 'getMediaSource', 'supportedSearchType'],
|
||
|
|
docUrl: 'https://musicfree.catcat.work/plugin/protocol.html'
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 检测插件格式
|
||
|
|
*/
|
||
|
|
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 (/['"`]128k['"`]|['"`]320k['"`]|['"`]flac24bit['"`]/.test(code)) scores.lx += 1;
|
||
|
|
if (/sources\s*:\s*\{/.test(code) && /qualitys/.test(code)) scores.lx += 2;
|
||
|
|
|
||
|
|
// === QZV2 ===
|
||
|
|
if (/pluginInfo\s*[:?]/.test(code)) scores.qzv2 += 3;
|
||
|
|
if (/getUrl\s*[:(]/.test(code)) scores.qzv2 += 2;
|
||
|
|
if (/supportFunc/.test(code)) scores.qzv2 += 2;
|
||
|
|
if (/getPlay(list|List)\s*[:(]/.test(code)) scores.qzv2 += 2;
|
||
|
|
if (/musicSearch/.test(code)) scores.qzv2 += 2;
|
||
|
|
if (/info\s*:\s*\{[\s\S]*?id\s*:/.test(code) && /info\s*:\s*\{[\s\S]*?name\s*:/.test(code)) scores.qzv2 += 1;
|
||
|
|
if (/allPage/.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: [], features: [], sources: [], apiUrls: []
|
||
|
|
};
|
||
|
|
|
||
|
|
const urlMatches = code.match(/https?:\/\/[^\s"'`<>)}\]]+/g) || [];
|
||
|
|
metadata.apiUrls = [...new Set(urlMatches)].slice(0, 20);
|
||
|
|
|
||
|
|
if (platform === 'lx') {
|
||
|
|
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();
|
||
|
|
|
||
|
|
const qualitySet = new Set();
|
||
|
|
if (/['"`]128k['"`]/.test(code)) qualitySet.add('128k');
|
||
|
|
if (/['"`]192k['"`]/.test(code)) qualitySet.add('192k');
|
||
|
|
if (/['"`]320k['"`]/.test(code)) qualitySet.add('320k');
|
||
|
|
if (/['"`]flac24bit['"`]/.test(code)) qualitySet.add('flac24bit');
|
||
|
|
if (/['"`]flac['"`]/.test(code)) qualitySet.add('flac');
|
||
|
|
if (/['"`]hires['"`]/.test(code)) qualitySet.add('hires');
|
||
|
|
if (/['"`]master['"`]/.test(code)) qualitySet.add('master');
|
||
|
|
metadata.quality = [...qualitySet];
|
||
|
|
|
||
|
|
if (/musicUrl/.test(code)) metadata.features.push('musicUrl');
|
||
|
|
if (/lyric/.test(code)) metadata.features.push('lyric');
|
||
|
|
if (/pic/.test(code)) metadata.features.push('pic');
|
||
|
|
|
||
|
|
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];
|
||
|
|
|
||
|
|
} else if (platform === 'qzv2') {
|
||
|
|
const nameMatch = code.match(/name\s*:\s*["'`]([^"'`]+)["'`]/); if (nameMatch) metadata.name = nameMatch[1];
|
||
|
|
const idMatch = code.match(/id\s*:\s*["'`]([^"'`]+)["'`]/); if (idMatch) metadata.description = `ID: ${idMatch[1]}`;
|
||
|
|
const descMatch = code.match(/description\s*:\s*["'`]([^"'`]+)["'`]/); if (descMatch) metadata.description = descMatch[1];
|
||
|
|
const versionMatch = code.match(/version\s*:\s*["'`]?(["'`]?)([\d.]+)/); if (versionMatch) metadata.version = versionMatch[2];
|
||
|
|
|
||
|
|
const qualityMatch = code.match(/quality\s*:\s*\[([^\]]+)\]/);
|
||
|
|
if (qualityMatch) {
|
||
|
|
const qItems = qualityMatch[1].match(/["'`]([^"'`]+)["'`]/g);
|
||
|
|
if (qItems) metadata.quality = qItems.map(q => q.replace(/["'`]/g, ''));
|
||
|
|
}
|
||
|
|
|
||
|
|
const supportFuncMatch = code.match(/supportFunc\s*:\s*\[([^\]]+)\]/);
|
||
|
|
if (supportFuncMatch) {
|
||
|
|
const fItems = supportFuncMatch[1].match(/["'`]([^"'`]+)["'`]/g);
|
||
|
|
if (fItems) metadata.features = fItems.map(f => f.replace(/["'`]/g, ''));
|
||
|
|
}
|
||
|
|
if (/getUrl/.test(code) && !metadata.features.includes('getUrl')) metadata.features.push('getUrl');
|
||
|
|
if (/getLyric/.test(code) && !metadata.features.includes('getLyric')) metadata.features.push('getLyric');
|
||
|
|
if (/search/.test(code) && !metadata.features.includes('search')) metadata.features.push('search');
|
||
|
|
|
||
|
|
} else if (platform === 'musicfree') {
|
||
|
|
const platformMatch = code.match(/platform\s*:\s*["'`]([^"'`]+)["'`]/); if (platformMatch) metadata.name = platformMatch[1];
|
||
|
|
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();
|
||
|
|
if (/['"`]low['"`]/.test(code)) qualitySet.add('low');
|
||
|
|
if (/['"`]standard['"`]/.test(code)) qualitySet.add('standard');
|
||
|
|
if (/['"`]high['"`]/.test(code)) qualitySet.add('high');
|
||
|
|
if (/['"`]super['"`]/.test(code)) qualitySet.add('super');
|
||
|
|
if (/['"`]hires['"`]/.test(code)) qualitySet.add('hires');
|
||
|
|
if (/['"`]master['"`]/.test(code)) qualitySet.add('master');
|
||
|
|
metadata.quality = [...qualitySet];
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!metadata.name && filename) metadata.name = filename.replace(/\.(js|zip)$/i, '');
|
||
|
|
return metadata;
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { detectPlatform, PLATFORMS, PLATFORM_LABELS, ALL_PLATFORMS };
|