/** * 本插件使用开源项目:https://github.com/UnblockNeteaseMusic/server */ const MUSIC_PATH = 'data/third/unblock-netease-music' const DOWNLOAD_PATH = Plugin.DownloadsPath || MUSIC_PATH + '/downloads' const PID_FILE = MUSIC_PATH + '/unblock-netease-music.pid' const PROCESS_NAME = 'unblockneteasemusic.exe' const PROCESS_PATH = MUSIC_PATH + '/' + PROCESS_NAME const YT_DLP_PATH = MUSIC_PATH + '/yt-dlp.exe' // 存储插件全局变量 window[Plugin.id] = window[Plugin.id] || { unblockHistory: Vue.ref([]), onServiceStopped: Plugins.debounce(async () => { console.log(`[${Plugin.name}]`, '插件已停止') await Plugins.WriteFile(PID_FILE, '0') }, 0) } /* 触发器 安装 */ const onInstall = async () => { await installUnblockMusic() return 0 } /* 触发器 卸载 */ const onUninstall = async () => { if (await isUnblockMusicRunning()) { throw '请先停止插件服务!' } await Plugins.confirm('确定要卸载吗', '将删除插件资源:' + MUSIC_PATH + '\n\n若已安装CA证书,记得手动卸载哦') await Plugins.RemoveFile(MUSIC_PATH) return 0 } /* 触发器 配置插件时 */ const onConfigure = async (config, old) => { if (config.Source.length === 0) { throw '请至少勾选一个音源' } if (await isUnblockMusicRunning()) { await stopUnblockMusicService() await startUnblockMusicService(config) } } /* 触发器 手动触发 */ const onRun = async () => { const modal = Plugins.modal( { title: Plugin.name, submit: false, width: '80', cancelText: 'common.close', maskClosable: true, afterClose() { modal.destroy() } }, { action: () => Vue.h('div', { class: 'mr-auto text-12', style: { color: 'var(--card-color)' } }, '注:重载界面后,需要重启服务才能记录解锁日志。') } ) const content = { template: `
`, setup() { const { ref, h } = Vue const dataSource = window[Plugin.id].unblockHistory const loading = ref(false) const isRunning = ref() isUnblockMusicRunning().then((res) => { isRunning.value = res }) return { columns: [ { title: '解锁时间', key: 'time', align: 'center', customRender: ({ value }) => Plugins.formatDate(value, 'YYYY-MM-DD HH:mm:ss') }, { title: '音频ID', key: 'audioId', align: 'center' }, { title: '歌曲名', key: 'songName', align: 'center' }, { title: '链接', key: 'url', align: 'center', minWidth: '160px', customRender: ({ value, record }) => h( 'div', { onClick: async () => { if (record._status === '已下载') { await Plugins.confirm('提示', '已下载,你想重新下载吗?') } else if (record._status === '失败') { } else if (record._status) { Plugins.message.info('正在下载,稍等片刻') return } const ext = value.match(/\.(mp3|flac|wav|aac|ogg|m4a)(?:\?.*)?$/i)?.[1] const filePath = DOWNLOAD_PATH + '/' + (record.songName + (ext ? `.${ext}` : '.mp3')) let failed = false await Plugins.Download(value, filePath, {}, (p, t) => { record._status = Plugins.formatBytes(p) + '/' + Plugins.formatBytes(t) + ' (' + ((p / t) * 100).toFixed(2) + '%)' // 小于4K,非正常的音频文件 if (p === t && t < 4096) { failed = true } }) if (failed) { Plugins.message.error('音频文件过小,已删除') Plugins.RemoveFile(filePath) record._status = '失败' } else { record._status = '已下载' } }, class: 'cursor-pointer ', style: { color: 'var(--primary-color)' } }, record._status || '下载' ) } ], dataSource, loading, isRunning, handleToggle: async () => { loading.value = true if (isRunning.value) { await stopUnblockMusicService().catch((e) => Plugins.message.error(e)) await switchTo(0) Plugin.status = 2 } else { await startUnblockMusicService().catch((e) => Plugins.message.error(e)) await switchTo(1) Plugin.status = 1 } loading.value = false isRunning.value = await isUnblockMusicRunning() }, handleClear: () => { dataSource.value.splice(0) }, handleOpen: async () => { const path = await Plugins.AbsolutePath(DOWNLOAD_PATH) Plugins.BrowserOpenURL(path) } } } } modal.setContent(content, null, null, false) modal.open() } /* 触发器 核心启动前 */ const onBeforeCoreStart = async (config, profile) => { console.log(`[${Plugin.name}]`, 'onBeforeCoreStart 启动插件并注入配置') if (!(await isUnblockMusicRunning())) { await startUnblockMusicService() } const isClash = !!config.mode const group = isClash ? config['proxy-groups'] : config['outbounds'] const flag = isClash ? 'name' : 'tag' const direct = (group.find((v) => v[flag] === '🎯 全球直连') || group.find((v) => v[flag] === '🎯 Direct'))?.[flag] || (isClash ? 'DIRECT' : 'direct') if (isClash) { config.proxies.unshift({ name: Plugin.Proxy, type: 'http', server: '127.0.0.1', port: Plugin.Port }) group.unshift({ name: Plugin.ProxyGroup, type: 'select', proxies: [direct, Plugin.Proxy] }) config.rules.unshift(`PROCESS-NAME,${Plugin.Process},${Plugin.ProxyGroup}`) } else { group.unshift({ tag: Plugin.Proxy, type: 'http', server: '127.0.0.1', server_port: Number(Plugin.Port) }) group.unshift({ tag: Plugin.ProxyGroup, type: 'selector', outbounds: [direct, Plugin.Proxy] }) config.route.rules.unshift({ process_name: Plugin.Process, outbound: Plugin.ProxyGroup }) } return config } /* 触发器 核心启动后 */ const onCoreStarted = async () => { console.log(`[${Plugin.name}]`, 'onCoreStarted 检测是否在运行并切换为解锁模式') const isRunning = await isUnblockMusicRunning() if (isRunning) { await switchTo(1) } return isRunning && 1 } /* 触发器 核心停止后 */ const onCoreStopped = async () => { console.log(`[${Plugin.name}]`, 'onCoreStopped 停止插件并切换为直连模式') if (await isUnblockMusicRunning()) { await stopUnblockMusicService() switchTo(0) return 2 } } /** * 插件菜单项 - 启动服务 */ const Start = async () => { if (await isUnblockMusicRunning()) { Plugins.message.warn('当前插件已经在运行了') return 1 } await startUnblockMusicService() await switchTo(1) Plugins.message.success('✨ 插件启动成功!') return 1 } /** * 插件菜单项 - 停止服务 */ const Stop = async () => { if (await isUnblockMusicRunning()) { await stopUnblockMusicService() } await switchTo(0) Plugins.message.success('✨ 插件停止成功') return 2 } /** * 启动服务 */ const startUnblockMusicService = (config = Plugin) => { return new Promise(async (resolve, reject) => { // 启动超时检测 setTimeout(async () => { if (!(await isUnblockMusicRunning())) { reject('启动超时') } }, 3000) try { const pid = await Plugins.ExecBackground( PROCESS_PATH, ['-p', config.Port + ':' + (Number(config.Port) + 1), '-a', '127.0.0.1', '-o', ...config.Source], async (out) => { console.log(`[${Plugin.name}]`, out) // 保存解锁信息 const data = await Plugins.ignoredError(JSON.parse, out) if (data && data.songName && data.url) { window[Plugin.id].unblockHistory.value.unshift(data) } if (out.includes('Error: ')) { reject(out) } if (out.includes('HTTP Server running')) { await Plugins.WriteFile(PID_FILE, pid.toString()) resolve() } }, () => { console.log(`[${Plugin.name}]`, 'ExecBackground onEnd 进程已结束') window[Plugin.id].onServiceStopped() }, { env: { PATH: await Plugins.AbsolutePath(MUSIC_PATH), // 环境变量路径,没有它就无法调用yt-dlp LOG_LEVEL: 'info', // 日志输出等级。请见〈日志等级〉部分。 LOG_LEVEL=debug info error BLOCK_ADS: 'true', // 屏蔽应用内部分广告 ENABLE_FLAC: String(config.ENABLE_FLAC), // 激活无损音质获取 ENABLE_LOCAL_VIP: String(config.ENABLE_LOCAL_VIP), // 激活本地黑胶 VIP,可选值:true(等同于 CVIP)、cvip 和 svip // LOCAL_VIP_UID: '', // 仅对这些 UID 激活本地黑胶 VIP,默认为对全部用户生效 LOCAL_VIP_UID=123456789,1234,123456 // ENABLE_HTTPDNS: false, // 激活故障的 Netease HTTPDNS 查询(不建议) DISABLE_UPGRADE_CHECK: 'false', // 禁用更新检测 FOLLOW_SOURCE_ORDER: 'true', // 严格按照配置音源的顺序进行查询 JSON_LOG: 'true', // 输出机器可读的 JSON 记录格式 NO_CACHE: 'true', // 停用 cache MIN_BR: config.MIN_BR, // 允许的最低源音质,小于该值将被替换 MIN_BR=320000 SELECT_MAX_BR: String(config.SELECT_MAX_BR), // 选择所有音源中的最高码率替换音频 SELECT_MAX_BR=true // LOG_FILE: 'app.log', // 从 Pino 端设置日志输出的文件位置。也可以用 *sh 的输出重导向功能 (node app.js >> app.log) 代替 LOG_FILE=app.log // YOUTUBE_KEY: '', // Youtube 音源的 Data API v3 Key YOUTUBE_KEY="" // SIGN_CERT: '', // 自定义证书文件 SIGN_CERT="./server.crt" // SIGN_KEY: '', // 自定义密钥文件 SIGN_KEY="./server.key" SEARCH_ALBUM: 'false', // 在其他音源搜索歌曲时携带专辑名称(默认搜索条件 歌曲名 - 歌手,启用后搜索条件 歌曲名 - 歌手 专辑名) SEARCH_ALBUM=true ...config.CookieMap } } ) } catch (error) { reject(error.message || error) } }) } /** * 停止服务 */ const stopUnblockMusicService = async () => { const pid = await Plugins.ignoredError(Plugins.ReadFile, PID_FILE) if (pid && pid !== '0') { await Plugins.KillProcess(Number(pid)) console.log(`[${Plugin.name}]`, 'KillProcess 已杀死进程') window[Plugin.id].onServiceStopped() } } /* * 切换网易云代理,index是onGenerate时添加的顺序 * index: 0切换为直连 * index: 1切换为代理 */ const switchTo = async (index) => { console.log(`[${Plugin.name}]`, '切换为', ['直连模式', '解锁模式'][index]) const kernelApiStore = Plugins.useKernelApiStore() if (!kernelApiStore.running) return const group = kernelApiStore.proxies[Plugin.ProxyGroup] const proxy = group?.all[index] if (group && proxy) { await Plugins.ignoredError(Plugins.handleUseProxy, group, { name: proxy }) } } /** * 检测是否在运行 */ const isUnblockMusicRunning = async () => { const pid = await Plugins.ignoredError(Plugins.ReadFile, PID_FILE) if (pid && pid !== '0') { const name = await Plugins.ignoredError(Plugins.ProcessInfo, Number(pid)) return name === PROCESS_NAME } return false } /** * 安装 */ const installUnblockMusic = async () => { const { env } = Plugins.useEnvStore() if (!['windows', 'linux'].includes(env.os)) throw '该插件暂不支持此操作系统' const isWin = env.os === 'windows' const isX64 = env.arch === 'amd64' const { id } = Plugins.message.info('正在下载...', 999999) try { const BinaryFileUrl = `https://github.com/UnblockNeteaseMusic/server/releases/download/v0.28.0/unblockneteasemusic-${isWin ? 'win' : 'linux'}-${isX64 ? 'x64' : 'arm64'}${isWin ? '.exe' : ''}` const YtDLPFileUrl = `https://github.com/yt-dlp/yt-dlp/releases/download/2025.09.26/yt-dlp${isWin ? '' : '_linux'}${isX64 ? '' : '_x86'}${isWin ? '.exe' : ''}` // 下载1 await Plugins.MakeDir(MUSIC_PATH) await Plugins.Download(BinaryFileUrl, PROCESS_PATH, {}, (c, t) => { Plugins.message.update(id, '正在下载主体程序...' + ((c / t) * 100).toFixed(2) + '%') }) if (!isWin) { await Plugins.Exec('chmod', ['+x', await Plugins.AbsolutePath(PROCESS_PATH)]) } // 下载2 await Plugins.Download(YtDLPFileUrl, YT_DLP_PATH, {}, (c, t) => { Plugins.message.update(id, '正在下载yt-dlp...' + ((c / t) * 100).toFixed(2) + '%') }) if (!isWin) { await Plugins.Exec('chmod', ['+x', await Plugins.AbsolutePath(YT_DLP_PATH)]) } Plugins.message.update(id, '下载完成') } finally { await Plugins.sleep(1000) Plugins.message.destroy(id) } const ca = 'data/.cache/ca.crt' await Plugins.Download('https://raw.githubusercontent.com/UnblockNeteaseMusic/server/enhanced/ca.crt', ca) const path = await Plugins.AbsolutePath(ca) await Plugins.alert( '最后一步', `请手动安装CA证书,该证书来自项目,但我们建议你使用自签证书。\n\n> 证书路径:${ca} [](${path.replaceAll('\\', '/')} "点击安装")\n\n安装教程:[](https://github.com/UnblockNeteaseMusic/server/discussions/426 "https://github.com/UnblockNeteaseMusic/server/discussions/426")`, { type: 'markdown' } ) }