/*! * @name 星海音乐源 * @description GDAPI | 聚合 | ChKSz API | 全平台支持24FLAC,网易、酷狗、QQ最高支持母带 * @version v3.2.13 * @Update 优化wy,可以不使用ChKSz API获取母带;出现调试/开发者也可以通过更新关闭;注意ChKSz API,代理解密地址自行填写 * @author 万去了了 * @homepage https://zrcdy.dpdns.org/ * @lastUpdate 2026-08-18 * @md5 */ const { EVENT_NAMES, request, on, send, env } = globalThis.lx; // ==================== 用户配置区域 ==================== // https://github.com/cdyUuu/kuwo-music-relay // 酷我代理解密配置(用于解密酷我加密无损格式,如 mflac/mgg) // 填入你自行部署的代理解密地址,留空则不启用代理解密 const KW_DECRYPT_PROXY = { url: '', // 在此填入代理解密地址(如 https://your-domain.com/decrypt.php),留空则不启用 allowEncryptedLossless: false, // 设为 true 启用代理解密 urlParamName: 'url', ekeyParamName: 'ekey', }; // ChKSz API 配置(网易SVIP接口 + QQ音乐接口,需要 apikey) // 启用且 apikey 不为空时,对应平台优先使用 chksz 接口 const CHKSZ_CONFIG = { apikey: '', // 在此填入 chksz 的 apikey,留空则不启用 chksz 接口 enableNetease: true, // 启用 chksz 网易云 SVIP 接口(支持到母带) enableQQ: true, // 启用 chksz QQ 音乐接口(支持到 master) }; // ==================================================== const URL_CONFIG = { domains: { primary: 'yy.zddyr.top', fallback: 'zrcdy.dpdns.org', gdStudio: 'music-api.gdstudio.xyz', chkszNew: 'api.chksz.com' }, paths: { backend: '/lx/api/', version: '/lx/versionh2.php', update: '/lx/vers.php', ip: '/ip.php', gdApi: '/api.php', chkszNetease: '/api/163_music', chkszQQ: '/api/qq_music' }, gdParams: 'use_xbridge3=true&loader_name=forest&need_sec_link=1&sec_link_scene=im&theme=light' }; const buildUrl = (domainKey, pathKey, extraQuery = '') => { const domain = URL_CONFIG.domains[domainKey]; const path = URL_CONFIG.paths[pathKey]; if (!domain || !path) throw new Error(`URL配置错误: ${domainKey} / ${pathKey}`); let url = `https://${domain}${path}`; if (extraQuery) { if (extraQuery.startsWith('&') && !path.includes('?')) { url += '?' + extraQuery.substring(1); } else { url += extraQuery; } } return url; }; const SCRIPT_VERSION = 'v3.2.13'; const SCRIPT_NAME = 'XingHaiMusicSource'; const SOURCE_MAP = { tx: 'qq', mg: 'migu', kw: 'kw', kg: 'kg' }; const PLATFORM_NAMES = { wy: '网易云音乐', tx: 'QQ音乐', kw: '酷我音乐', kg: '酷狗音乐', mg: '咪咕音乐' }; const MUSIC_QUALITIES = { wy: ['128k','320k','flac','hires','atmos','master'], tx: ['128k','192k','320k','flac','hires','atmos','atmos_plus','master'], kw: ['128k','320k','flac','hires','atmos','master'], kg: ['128k','320k','flac','hires','atmos','master'], mg: ['128k','320k','flac'] }; // ChKSz 网易云 level 映射(需将插件音质转换为 chksz 的 level 值) const CHKSZ_NETEASE_LEVEL_MAP = { '128k': 'standard', '320k': 'exhigh', 'flac': 'lossless', 'hires': 'hires', 'atmos': 'jymaster', 'master': 'jymaster' }; // ChKSz QQ 音质 size 映射 const CHKSZ_QQ_SIZE_MAP = { '128k': '128k', '192k': '320k', '320k': '320k', 'flac': 'flac', 'hires': 'hires', 'atmos': 'master', 'atmos_plus': 'master', 'master': 'master' }; // GD API 音质映射(hires 用 999,失败降级 740) // GD 不支持 atmos/master,这些音质不会走 GD const GD_BR_MAP = { '128k':'128', '320k':'320', 'flac':'740', 'hires':'999' }; // GD 支持的音质集合(atmos/master 不在 GD 支持范围) const GD_SUPPORTED_QUALITIES = new Set(['128k','320k','flac','hires']); const TOKEN_TTL = 5 * 60 * 1000; let userIp = null; let userToken = ''; let tokenTimestamp = 0; let clientHeader = ''; let deviceId = ''; let availablePlatforms = []; let backendAggBlocked = false; // 后端聚合接口 403 屏蔽标志(403后不再请求,除非脚本重启) const extraCache = new Map(); // -------------------- 工具函数 -------------------- function isBuffer(obj) { return obj && typeof obj === 'object' && ((typeof Buffer !== 'undefined' && Buffer.isBuffer(obj)) || (typeof obj.constructor === 'function' && obj.constructor.name === 'Buffer')); } function safeParseBody(body) { if (typeof body === 'string') { const trimmed = body.trim(); if (/^[{["]/.test(trimmed)) { try { return JSON.parse(trimmed); } catch (e) {} } return body; } if (typeof body === 'object' && body !== null) { try { if (typeof body.toString === 'function' && body.toString() !== '[object Object]') body = body.toString('utf-8'); } catch (e) {} if (typeof body === 'object' && !isBuffer(body)) return body; } try { if (isBuffer(body)) { if (globalThis.lx?.utils?.buffer?.bufToString) body = globalThis.lx.utils.buffer.bufToString(body, 'utf-8'); else if (typeof Buffer !== 'undefined') body = Buffer.from(body).toString('utf-8'); else body = String(body); } } catch (e) {} if (typeof body === 'string') { const trimmed = body.trim(); if (/^[{["]/.test(trimmed)) { try { return JSON.parse(trimmed); } catch (e) {} } } return body; } function safeBase64Encode(str) { try { if (globalThis.lx?.utils?.buffer?.from) { const buf = globalThis.lx.utils.buffer.from(str, 'utf-8'); return globalThis.lx.utils.buffer.bufToString(buf, 'base64'); } if (typeof Buffer !== 'undefined') return Buffer.from(str, 'utf-8').toString('base64'); return btoa(unescape(encodeURIComponent(str))); } catch (e) { return str; } } function simpleGetQueryParam(url, key) { if (typeof url !== 'string' || !url) return null; const qIdx = url.indexOf('?'); if (qIdx < 0) return null; let query = url.substring(qIdx + 1); const hashIdx = query.indexOf('#'); if (hashIdx >= 0) query = query.substring(0, hashIdx); const pairs = query.split('&'); for (const p of pairs) { const eq = p.indexOf('='); if (eq < 0) continue; if (p.substring(0, eq) === key) { try { return decodeURIComponent(p.substring(eq + 1)); } catch (e) { return p.substring(eq + 1); } } } return null; } function generateDeviceId() { return 'lx-online-' + Math.random().toString(36).substring(2, 8) + Date.now().toString(36).slice(-4); } function buildClientHeader() { let deviceType = 'unknown'; try { const p = (env?.platform || '').toLowerCase(); if (p.includes('android')) deviceType = 'Android'; else if (p.includes('ios')) deviceType = 'iOS'; else if (p.includes('win')) deviceType = 'Windows'; else if (p.includes('mac')) deviceType = 'macOS'; else if (p.includes('linux')) deviceType = 'Linux'; } catch (e) {} return `${SCRIPT_NAME}/${SCRIPT_VERSION} (${deviceType})`; } function generateToken(ip) { if (!deviceId) deviceId = generateDeviceId(); const payload = { device_id: deviceId, ip: ip || '0.0.0.0', timestamp: Math.floor(Date.now() / 1000), random: Math.random().toString(36).substring(2, 12) }; tokenTimestamp = Date.now(); return safeBase64Encode(JSON.stringify(payload)); } function ensureTokenFresh() { if (!userToken || (Date.now() - tokenTimestamp) > TOKEN_TTL) { userToken = generateToken(userIp); } } const httpFetch = (url, options = {}) => new Promise((resolve, reject) => { if (!options.noAuth) ensureTokenFresh(); const headers = { ...(options.headers || {}) }; if (!options.noAuth) { if (userToken) headers['X-Token'] = userToken; if (clientHeader) headers['X-Client'] = clientHeader; } if (!headers['User-Agent']) headers['User-Agent'] = 'lx-music'; request(url, { ...options, headers }, (err, resp) => { if (err) return reject(err); resolve({ body: safeParseBody(resp.body), statusCode: resp.statusCode, headers: resp.headers || {} }); }); }); function mapQuality(target, avail) { const pm = { '臻品母带': 'jymaster', '臻品音质2.0': 'sky', '臻品音质AI': 'jyeffect', '臻品音质': 'jyeffect', 'Hires 无损24-Bit': 'hires', 'Hi-Res': 'hires', 'FLAC': 'flac', '320k': '320k', '192k': '192k', '128k': '128k' }; if (avail.includes(target)) return target; const m = pm[target]; if (m && avail.includes(m)) return m; const order = ['jymaster', 'sky', 'jyeffect', 'hires', 'flac24bit', 'master', 'flac', '320k', '192k', '128k']; for (const q of order) if (avail.includes(q)) return q; return avail[0] || '128k'; } // -------------------- 酷我加密链接处理 -------------------- function processKwEncryptedUrl(data, source) { if (source !== 'kw' || !KW_DECRYPT_PROXY.allowEncryptedLossless) { return data?.url || ''; } let ekey = null; if (data?.ekey) { ekey = typeof data.ekey === 'string' ? data.ekey.trim() : String(data.ekey).trim(); } if (!ekey && data?.url && typeof data.url === 'string') { ekey = simpleGetQueryParam(data.url, 'ekey'); } if (!ekey || !KW_DECRYPT_PROXY.url) { return data?.url || ''; } const rawUrl = typeof data.url === 'string' ? data.url : String(data.url); try { return `${KW_DECRYPT_PROXY.url}?${KW_DECRYPT_PROXY.urlParamName}=${encodeURIComponent(rawUrl)}&${KW_DECRYPT_PROXY.ekeyParamName}=${encodeURIComponent(ekey)}`; } catch (e) { return rawUrl; } } // -------------------- 网络接口 -------------------- async function fetchIp() { try { const r = await httpFetch(buildUrl('primary', 'ip'), { timeout: 3000 }); if (r.body?.ip) { userIp = r.body.ip; userToken = generateToken(userIp); } } catch (e) {} } // ChKSz 网易云 SVIP 接口(需 apikey) async function getWyChkszUrl(id, quality) { const level = CHKSZ_NETEASE_LEVEL_MAP[quality]; if (!level) throw new Error('chksz不支持该品质'); const url = `https://${URL_CONFIG.domains.chkszNew}${URL_CONFIG.paths.chkszNetease}?id=${id}&level=${level}&apikey=${encodeURIComponent(CHKSZ_CONFIG.apikey)}`; const resp = await httpFetch(url, { headers: { 'User-Agent': 'LX-Music-Mobile' }, timeout: 8000, noAuth: true }); if (resp.statusCode !== 200 || resp.body.code !== 200 || !resp.body.data?.url) { throw new Error(`chksz网易失败(${resp.statusCode}): ${resp.body?.msg || '未返回url'}`); } return { url: resp.body.data.url, lyric: null, cover: resp.body.data.picUrl || null }; } // ChKSz QQ 音乐接口(需 apikey) async function getTxChkszUrl(musicInfo, quality) { const size = CHKSZ_QQ_SIZE_MAP[quality]; if (!size) throw new Error('chksz不支持该品质'); const mid = musicInfo.songmid || musicInfo.id; if (!mid) throw new Error('缺少QQ mid'); const url = `https://${URL_CONFIG.domains.chkszNew}${URL_CONFIG.paths.chkszQQ}?mid=${mid}&size=${size}&type=json&apikey=${encodeURIComponent(CHKSZ_CONFIG.apikey)}`; const resp = await httpFetch(url, { headers: { 'User-Agent': 'LX-Music-Mobile' }, timeout: 8000, noAuth: true }); if (resp.statusCode !== 200 || resp.body.code !== 200 || !resp.body.url) { throw new Error(`chksz QQ失败(${resp.statusCode}): ${resp.body?.msg || '未返回url'}`); } return { url: resp.body.url, lyric: resp.body.lrc || null, cover: resp.body.cover || null }; } // 网易 GD 接口(hires 用 br=999,失败降级 740) async function getWyGDUrl(id, q) { const br = GD_BR_MAP[q] || '320'; const url = buildUrl('gdStudio', 'gdApi', `&${URL_CONFIG.gdParams}&types=url&source=netease&id=${id}&br=${br}`); let resp = await httpFetch(url, { headers: { 'User-Agent': 'LX-Music-Mobile' }, timeout: 8000, noAuth: true }); // hires 请求失败或无url,降级到标准无损 flac if (q === 'hires' && (resp.statusCode !== 200 || !resp.body.url)) { const fallbackUrl = buildUrl('gdStudio', 'gdApi', `&${URL_CONFIG.gdParams}&types=url&source=netease&id=${id}&br=740`); resp = await httpFetch(fallbackUrl, { headers: { 'User-Agent': 'LX-Music-Mobile' }, timeout: 8000, noAuth: true }); } if (resp.statusCode !== 200 || !resp.body.url) { throw new Error(`GD接口状态${resp.statusCode},未返回音频`); } return { url: resp.body.url, lyric: null, cover: null }; } // 自建后端接口(通用) async function getUrlFromBackend(source, musicInfo, quality) { const backendSource = SOURCE_MAP[source] || source; const baseUrl = buildUrl('primary', 'backend'); const params = {}; if (backendSource === 'kg') { const types = musicInfo._types || {}; params.source = 'kg'; params.quality = quality || ''; params.songmid = musicInfo.songmid || musicInfo.id || ''; params.albumId = musicInfo.albumId || ''; params.mainHash = musicInfo.hash || ''; if (types[quality]?.hash) params.hash = types[quality].hash; } else { params.source = backendSource; params.name = musicInfo.name || ''; params.singer = musicInfo.singer || ''; params.songmid = musicInfo.songmid || musicInfo.id || ''; params.interval = musicInfo.interval || ''; params.albumName = musicInfo.albumName || musicInfo.album || ''; params.quality = quality || ''; } const query = Object.keys(params).map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`).join('&'); const url = `${baseUrl}?${query}`; const resp = await httpFetch(url, { method: 'GET', timeout: 8000 }); // 403 检测:标记屏蔽,后续不再请求此接口(除非脚本重启) if (resp.statusCode === 403) { backendAggBlocked = true; throw new Error('后端聚合接口返回403,已屏蔽'); } if (resp.statusCode !== 200) throw new Error(`后端接口状态${resp.statusCode}`); const data = resp.body; if (data.code !== 200 || !data.url) throw new Error(data.msg || '后端无可用链接'); const finalUrl = processKwEncryptedUrl(data, backendSource); return { url: finalUrl, lyric: data.lrc || null, cover: data.picture || null }; } // -------------------- 核心:获取音乐URL -------------------- async function fetchMusicUrl(source, musicInfo, quality) { const id = musicInfo.hash ?? musicInfo.songmid ?? musicInfo.id; if (!id) throw new Error('缺少 songId'); let actualQuality = mapQuality(quality, MUSIC_QUALITIES[source] || ['128k','320k','flac']); if (source === 'kw' && !KW_DECRYPT_PROXY.allowEncryptedLossless) { actualQuality = mapQuality(quality, ['128k','320k','flac']); } let result = { url: '', lyric: null, cover: null }; let lastError = ''; const chkszEnabled = !!(CHKSZ_CONFIG.apikey && CHKSZ_CONFIG.apikey.trim()); // --- 网易云音乐 --- // 链路: chksz(有key优先) → 后端聚合(403屏蔽) → GD API if (source === 'wy') { // 1. 优先 chksz SVIP 接口(需启用且有 apikey) if (chkszEnabled && CHKSZ_CONFIG.enableNetease) { try { result = await getWyChkszUrl(id, actualQuality); } catch (e) { lastError = `chksz网易失败: ${e.message}`; } } // 2. chksz 失败/未启用 → 后端聚合接口(403屏蔽后跳过) // 后端聚合支持音质与主音质一致: 128k, 320k, flac, hires, atmos, master // 无需转换,直接透传 if (!result.url && !backendAggBlocked) { try { result = await getUrlFromBackend('wy', musicInfo, actualQuality); } catch (e) { lastError = `后端聚合失败: ${e.message}`; } } // 3. 后端聚合失败/屏蔽 → GD 接口 if (!result.url && GD_SUPPORTED_QUALITIES.has(actualQuality)) { try { result = await getWyGDUrl(id, actualQuality); } catch (e) { lastError = `GD接口失败: ${e.message}`; } } } // --- QQ 音乐 --- else if (source === 'tx') { // 1. 优先 chksz QQ 接口 if (chkszEnabled && CHKSZ_CONFIG.enableQQ) { try { result = await getTxChkszUrl(musicInfo, actualQuality); } catch (e) { lastError = `chksz QQ失败: ${e.message}`; } } // 2. 回退:自建后端 if (!result.url) { try { result = await getUrlFromBackend(source, musicInfo, actualQuality); } catch (e) { lastError = `后端失败: ${e.message}`; } } } // --- 其他平台:自建后端 --- else { try { result = await getUrlFromBackend(source, musicInfo, actualQuality); } catch (e) { lastError = `后端失败: ${e.message}`; } } extraCache.set(id, { lyric: result.lyric, cover: result.cover }); // 返回守卫:允许 http(含本地IP,如 127.0.0.1)与 https 链接 const trimmedUrl = typeof result.url === 'string' ? result.url.trim() : ''; if (typeof result.url !== 'string' || trimmedUrl.length < 1 || !trimmedUrl.match(/^https?:\/\//i)) { throw new Error(lastError || '获取播放链接失败'); } return trimmedUrl; } // -------------------- 更新检查 -------------------- async function checkUpdate() { const versionUrls = [ buildUrl('primary', 'version') + '?ver=' + encodeURIComponent(SCRIPT_VERSION), buildUrl('fallback', 'version') + '?ver=' + encodeURIComponent(SCRIPT_VERSION) ]; try { const resp = await Promise.any(versionUrls.map(u => httpFetch(u, { timeout: 5000 }))); if (resp.statusCode === 200 && resp.body && resp.body.update_url) { send(EVENT_NAMES.updateAlert, { log: resp.body.changelog || resp.body.message || `发现新版本 ${resp.body.version || ''}`, updateUrl: resp.body.update_url }); } } catch (e) {} } // -------------------- 事件处理 -------------------- on(EVENT_NAMES.request, async ({ action, source, info }) => { if (!source || !MUSIC_QUALITIES[source]) throw new Error(`不支持的音乐源: ${source}`); if (action === 'musicUrl') { if (!info?.musicInfo || !info.type) throw new Error('参数不完整'); return fetchMusicUrl(source, info.musicInfo, info.type); } const id = info?.musicInfo?.hash ?? info?.musicInfo?.songmid ?? info?.musicInfo?.id; const cached = extraCache.get(id); if (action === 'lyric') return cached?.lyric ? { lyric: cached.lyric, tlyric: '' } : null; if (action === 'pic') return cached?.cover || null; throw new Error(`不支持的操作: ${action}`); }); // -------------------- 启动 -------------------- (async () => { deviceId = generateDeviceId(); clientHeader = buildClientHeader(); userToken = generateToken(null); availablePlatforms = ['wy', 'tx', 'kg', 'kw', 'mg']; const sources = {}; availablePlatforms.forEach(p => { sources[p] = { name: PLATFORM_NAMES[p], type: 'music', actions: ['musicUrl', 'lyric', 'pic'], qualitys: MUSIC_QUALITIES[p] }; }); send(EVENT_NAMES.inited, { openDevTools: false, status: true, sources }); fetchIp(); checkUpdate(); })();