/** * @name HYWmusic_beta_公益测试 * @version v0.74.0 * @author Ryn * @description 你知道吗我的trae积分用完了……我想要赞助喵…… 一群(满了)1094095648 二群(可入)965503129 * @homepage https://github.com/Macrohard0001/HYWmusic_source * @license MIT * @updateUrl http://103.79.184.97/api/releases?script=HYWmusic_beta_%E5%85%AC%E7%9B%8A%E6%B5%8B%E8%AF%95&scriptType=free&releaseType=lx&version=v0.74.0 * * 支持平台: kw、kg、tx、wy、mg * 支持音质: 128k、320k、flac、flac24bit、master、atmos_plus、atmos、hires * 生成时间: 2026-08-06T07:52:23.078Z * * 协议参考:ikun-music-source.js + lxmusic.toside.cn/desktop/custom-source * - MUSIC_QUALITY 每平台独立音质(按后端勾选写入) * - on handler 纯 Promise 风格:({action, source, info}) => Promise * - inited 发送 status:true + sources * - API_BASE 必须注入,禁止回退 localhost */ 'use strict' const DEV_ENABLE = false const UPDATE_ENABLE = true const { EVENT_NAMES, request, on, send, env, version: LX_VERSION } = globalThis.lx // ====== 每平台独立音质(参考 ikun) ====== const MUSIC_QUALITY = JSON.parse('{"kw":["128k","320k","flac","flac24bit","master","atmos_plus","atmos","hires"],"kg":["128k","320k","flac","flac24bit","master","atmos_plus","atmos","hires"],"tx":["128k","320k","flac","flac24bit","master","atmos_plus","atmos","hires"],"wy":["128k","320k","flac","flac24bit","master","atmos_plus","atmos","hires"],"mg":["128k"]}') const MUSIC_SOURCE = Object.keys(MUSIC_QUALITY) // ====== 运行参数 ====== const API_BASE = 'http://103.79.184.97' const CARD_KEY = 'PYPW-QFRL-3DBF-95O6' // ====== 日志 ====== const log = { info: (...args) => { try { console.log('[HYWmusic]', ...args) } catch(e) {} }, error: (...args) => { try { console.error('[HYWmusic ERROR]', ...args) } catch(e) {} }, warn: (...args) => { try { console.warn('[HYWmusic WARN]', ...args) } catch(e) {} }, } // ====== API_BASE 检查:禁止回退 localhost ====== if (!API_BASE || !/^https?:\/\//.test(API_BASE)) { log.error('API_BASE 未配置或格式非法: "' + API_BASE + '",所有请求都将失败') log.error('请联系发行版管理员在创建发行版时设置 metadata.apiUrl') } // ====== HTTP 请求(严格对齐 ikun:仅 callback 风格 request) ====== const httpFetch = (url, options = { method: 'GET' }) => { return new Promise((resolve, reject) => { if (!API_BASE || !/^https?:\/\//.test(API_BASE)) { return reject(new Error('API_BASE 未配置或格式非法')) } const headers = { ...(options.headers || {}), 'User-Agent': env ? `lx-music-${env}/${LX_VERSION}` : `lx-music-request/${LX_VERSION || '1.0.0'}`, } if (CARD_KEY) headers['X-Card-Key'] = CARD_KEY const reqOptions = { ...options, headers } if (!reqOptions.method) reqOptions.method = 'GET' // 兼容 LX 沙箱 request 的两种 callback 签名: // 2 参数: (err, resp) — resp.body 包含响应体 // 3 参数: (err, resp, body) — body 是独立解析的响应体(needle 风格) // 部分版本 resp.body 为 undefined,body 在第三个参数;两者都取以兜底 request(url, reqOptions, (err, resp, body) => { if (err) return reject(err) const respBody = (resp && resp.body !== undefined && resp.body !== null) ? resp.body : body resolve({ statusCode: resp ? resp.statusCode : undefined, headers: resp ? resp.headers : undefined, body: respBody, }) }) }) } // ====== 超时保护(保留但默认不使用,脚本内部调用) ====== // const withTimeout = (promise, ms) => Promise.race([ // promise, // new Promise((_, reject) => setTimeout(() => reject(new Error('请求超时(' + ms + 'ms)')), ms)) // ]) // ====== musicInfo 字段收集:透传完整字段 ====== const collectMusicInfoParams = (musicInfo, platform) => { if (!musicInfo) return {} const params = {} const songId = musicInfo.songmid || musicInfo.songId || musicInfo.id || musicInfo.hash || musicInfo.rid || musicInfo.musicId || musicInfo.copyrightId || musicInfo.songid || '' if (songId) params.songId = songId if (musicInfo.songmid) params.songmid = musicInfo.songmid if (musicInfo.hash) params.hash = musicInfo.hash const fields = ['albumAudioId', 'strMediaMid', 'mediaMid', 'copyrightId', 'rid', 'musicId', 'albumId', 'albumName', 'albumMid', 'songname', 'songName', 'name', 'singer', 'singers', 'artist'] for (const f of fields) { if (musicInfo[f] !== undefined && musicInfo[f] !== null && musicInfo[f] !== '') params[f] = musicInfo[f] if (musicInfo.meta && musicInfo.meta[f] !== undefined && musicInfo.meta[f] !== null && musicInfo.meta[f] !== '') params[f] = musicInfo.meta[f] } params.platform = platform params.source = platform // 兼容 /api/music/info(仅读 source,不读 platform) return params } // ====== 获取音乐 URL(GET + query 参数,服务端仅支持 GET) ====== const handleGetMusicUrl = async (source, musicInfo, quality) => { const params = collectMusicInfoParams(musicInfo, source) if (quality) params.quality = quality if (CARD_KEY) params.key = CARD_KEY const query = Object.entries(params) .filter(([, v]) => v !== undefined && v !== null && v !== '') .map(([k, v]) => k + '=' + encodeURIComponent(String(v))) .join('&') const url = API_BASE + '/api/music/url' + (query ? '?' + query : '') const resp = await httpFetch(url, { method: 'GET' }) let respBody = resp && resp.body if (typeof respBody === 'string') { try { respBody = JSON.parse(respBody) } catch (e) { throw new Error('服务端返回非 JSON 数据') } } if (!respBody || typeof respBody !== 'object') { throw new Error('空响应') } switch (respBody.code) { case 200: return respBody.url || respBody.data || respBody case 401: case 403: throw new Error(respBody.message || '鉴权失败') case 429: throw new Error('请求过速') case 500: throw new Error(respBody.message || '服务器错误') default: throw new Error(respBody.message || ('未知错误 code=' + respBody.code)) } } // ====== 获取歌词 ====== const handleGetLyric = async (source, musicInfo) => { const params = collectMusicInfoParams(musicInfo, source) params.action = 'lyric' try { const query = Object.entries(params) .filter(([, v]) => v !== undefined && v !== null && v !== '') .map(([k, v]) => k + '=' + encodeURIComponent(String(v))) .join('&') const url = API_BASE + '/api/music/info' + (query ? '?' + query : '') const resp = await httpFetch(url, { method: 'GET' }) let respBody = resp && resp.body if (typeof respBody === 'string') { try { respBody = JSON.parse(respBody) } catch (e) { respBody = null } } if (!respBody || respBody.code !== 200) return { lyric: '', tlyric: null, rlyric: null, lxlyric: null } const data = respBody.data || respBody return { lyric: data.lyric || '', tlyric: data.tlyric || null, rlyric: data.rlyric || null, lxlyric: data.lxlyric || null, } } catch (e) { return { lyric: '', tlyric: null, rlyric: null, lxlyric: null } } } // ====== 获取封面 ====== const handleGetPic = async (source, musicInfo) => { const params = collectMusicInfoParams(musicInfo, source) params.action = 'pic' try { const query = Object.entries(params) .filter(([, v]) => v !== undefined && v !== null && v !== '') .map(([k, v]) => k + '=' + encodeURIComponent(String(v))) .join('&') const url = API_BASE + '/api/music/info' + (query ? '?' + query : '') const resp = await httpFetch(url, { method: 'GET' }) let respBody = resp && resp.body if (typeof respBody === 'string') { try { respBody = JSON.parse(respBody) } catch (e) { return '' } } if (!respBody || respBody.code !== 200) return '' const data = respBody.data || respBody return data.pic || data.url || '' } catch (e) { return '' } } // ====== on request(严格对齐 ikun:纯 Promise 风格,无 withTimeout) ====== on(EVENT_NAMES.request, ({ action, source, info }) => { switch (action) { case 'musicUrl': return handleGetMusicUrl(source, info.musicInfo, info.type) case 'lyric': return handleGetLyric(source, info.musicInfo) case 'pic': return handleGetPic(source, info.musicInfo) default: return Promise.reject('action not support: ' + action) } }) // ====== 构建 sources(每平台独立 qualitys,参考 ikun) ====== const musicSources = {} MUSIC_SOURCE.forEach((item) => { musicSources[item] = { name: item, type: 'music', actions: ['musicUrl', 'lyric', 'pic'], qualitys: MUSIC_QUALITY[item], } }) // ====== 发送 inited(参考 ikun:status: true + openDevTools) ====== send(EVENT_NAMES.inited, { status: true, openDevTools: DEV_ENABLE, sources: musicSources, })