🎧 QZ Music v2 插件开发指南
📜 概述
QZ Music v2 的插件系统旨在提供灵活的音乐数据获取和功能扩展能力。插件分为以下两种类型:
- 基础插件(Base Plugin):
- 用途:用于提供完整的核心接口平台,以获取歌单、歌词、歌曲信息等数据。
- 不包括:音频 URL/数据和其他拓展功能。
- 拓展插件(Extension Plugin):
- 用途:嵌入基础插件,实现多种拓展功能,如接入音源、拉取用户歌单列表等。
- 特点:用途多样,拓展性强。
本文档将详细介绍如何开发这两种类型的插件。
💻 文件要求与运行环境
| 要求类型 | 规范 | 备注 |
|---|---|---|
| 编码格式 | UTF-8 | |
| 编程语言 | JavaScript (NodeJS) | 仅支持 CommonJS 语法(因 Javet 兼容性原因) |
| 文件扩展名 | 拓展插件: .js / 基础插件: .zip | |
| 运行环境 | V8 + Node | 运行时实现为编译后的 libnode-xxx.so |
📦 基础插件开发
基础插件开发难度较高,其核心在于提供信息获取通道,不内置音频源。音频源/拓展功能开发请参考下方的拓展插件。
📁 zip 内文件示例结构
一个基础插件最终需要打包成一个 .zip 文件,其内部结构示例如下:
Plugin.zip
├── utils/
│ └── http.js // 例如: 封装的请求库
├── node_modules/ // NodeJS 依赖库(必须包含,例如 npm i axios 后的产物)
├── index.js // 导出主入口文件
├── package.json // NodeJS 自带的配置文件
├── package-lock.json // NodeJS 自带的依赖声明文件
└── plugin.json // QZ Music v2 插件信息声明文件 (核心)📝 index.js 主入口
index.js 必须实现并按名称导出所有支持的基础功能。
// 示例 index.js
const leaderboard = require('./utils/musicSdk/wy/leaderboard.js');
// ... 引入其他基础功能模块
const { getUrl } = require('./ext.js'); // 拓展性插件函数(可选)
// ...
const moduleObj = {
tipSearch, // 搜索建议
leaderboard, // 排行榜
musicSearch, // 音乐搜索
songList, // 歌单
hotSearch, // 热门搜索
singer, // 歌手信息
getLyric, // 获取歌词
// 基础插件提供获取图片链接的方法
getPic(songId) {
const requestObj = getMusicInfo(songId)
// 确保返回的是 Promise
return requestObj.promise.then((info) => info.al.picUrl)
},
getUrl, // 音频 URL 获取(通常由拓展插件实现并引入)
musicDetail, // 歌曲详情
}
module.exports = moduleObj📌 PS:
所有导出的方法都必须返回一个
Promise。 具体的函数参数和返回格式请参考群文件中的任意基础插件示例。
⚙️ plugin.json 插件信息声明
plugin.json 用于声明插件的基本信息、支持的功能以及用户可配置的变量。
| 字段 | 类型 | 说明 | 示例值 |
|---|---|---|---|
info | Object | 插件基本信息。 | |
info.id | String | 插件唯一 ID,关乎数据获取插件。 | "wy" |
info.name | String | 插件名称。 | "小芸音乐插件" |
info.version | String | 插件版本号。 | "1.0.0" |
env_var | Array<Object> | 用户可配置的环境变量列表。 | |
env_var[].key | String | 变量的键名,用于代码中引用。 | "playlist_url" |
env_var[].name | String | 变量在 UI 中显示的名称。 | "个人主页链接" |
env_var[].nullable | Boolean | 是否可为空。 | true |
support | Array<String> | 基础插件支持的核心功能列表,对应 index.js 导出的方法名。 | ["tipSearch", "getLyric", "getUrl"] |
ext_fun | Array<Object> | 拓展功能列表,用于 UI 显示。 | |
ext_fun[].name | String | 拓展功能在 UI 中显示的名称。 | "个人歌单" |
ext_fun[].entry | String | 拓展插件中实现此功能的函数名(应在拓展插件的 module.exports 中)。 | |
ext_fun[].var_map | Array<Object> | 拓展函数所需的参数映射。 | |
var_map[].key_id | String | 映射到 env_var 中定义的键名。 | "playlist_url" |
var_map[].arg_id | String | 映射到拓展函数中的参数名。 | "url" |
📌 示例
plugin.json结构:json{ "info": { ... }, "env_var": [ ... ], "support": ["tipSearch", "leaderBoard", "musicSearch", "songList", "hotSearch", "getLyric", "getPic", "getUrl"], "ext_fun": [ { "name": "个人歌单", "entry": "playlist", // 假设拓展插件导出了 playlist 函数 "var_map": [ { "key_id": "playlist_url", "arg_id": "url" } ] } ] }
🔗 拓展插件开发
拓展插件通常用于嵌入基础插件,实现额外的拓展功能,如获取音频 URL(音源)或用户歌单列表。
📂 基本结构 (ext.js)
拓展插件是一个独立的 .js 文件,通过 module.exports 导出功能函数。
// 示例 ext.js
const crypto = require('crypto') // 支持引入内置库
const axios = require('axios')
// 实现核心功能:获取音频 URL
function getUrl(songId, quality){
return axios.get(`https://api.xxxxxx.cn/music/${songId}/${quality}`).then((response) => {
if(!response.data.url || !response.data.url.startsWith('http')) {
throw new Error("No url found.")
}
return response.data.data.url // 返回音频 URL
})
}
// 实现拓展功能:获取用户歌单列表
async function playlist(url){
return axios.get(`https://music-api.lgfzer.com/music/extra?source=wy&url=${url}`).then((response) => {
if(!response.data.success) {
throw new Error("Unexpected response.");
}
const data = response.data.data;
return {
"type":"playlists",
"data":data // 返回规范化的歌单数据
}
})
}
module.exports = {
playlist, // 对应 plugin.json 中的 ext_fun
getUrl, // 对应 plugin.json 中的 support
}🔊 支持的音质 Quality
拓展插件在实现 getUrl(songId, quality) 时,需要处理以下常见的音质参数:
| 参数 | 说明 |
|---|---|
128k | 128kbps 标准音质 |
320k | 320kbps 高音质 |
flac | FLAC 无损音质 |
flac24bit | 24bit FLAC |
hires | Hi-Res 高解析度 |
atmos | 杜比全景声 |
master | 母带音质 |
提示:本 APP 支持通过基础插件提供自定义音质表,具体实现请参考需要嵌入的基础插件。
🚨 错误处理
良好的错误处理是插件稳定运行的关键。所有方法都应返回 Promise,并在其中捕获和抛出错误。
🌟 最佳实践
async function musicUrl(source, musicInfo, quality) {
try {
// 1. 参数验证
if (!musicInfo || !musicInfo.id) {
throw new Error('音乐信息不完整')
}
// 2. API 调用
const result = await axios.get(url,options)
// 3. 结果验证
if (!result || result.statusCode !== 200) {
throw new Error(`API 请求失败: ${result?.statusCode || 'Unknown'}`)
}
if (!result.body || !result.body.url) {
throw new Error('返回数据格式错误')
}
return result.body.url
} catch (error) {
// 4. 记录错误日志
console.error(`[${source}] 获取音乐链接失败:`, error.message)
// 5. 重新抛出错误供上层处理
throw new Error(`获取 ${source} 音乐链接失败: ${error.message}`)
}
}🧩 常见错误类型
- 网络错误: 请求超时、连接失败等。
- API 错误: 接口返回错误状态码(如 403, 404, 500)。
- 数据错误: 返回数据结构不正确或缺少关键字段。
- 参数错误: 传入参数不完整或格式错误。
🛠️ 调试技巧
1. 使用 console.log 进行调试
插件的运行环境支持标准的 console 方法:
console.log('[插件名] 调试信息:', data)
console.warn('[插件名] 警告信息:', warning)
console.error('[插件名] 错误信息:', error)2. 错误捕获
建议全局捕获未处理的 Promise 拒绝:
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason)
})🚀 性能优化
1. 请求缓存 (已在 APP 中实现)
开发者无需再实现缓存逻辑,应用已内置支持。
2. 并发控制
如果插件需要向同一接口发起大量请求,可以考虑使用信号量(Semaphore)进行并发控制,防止请求过多导致 API 限制。
// 注: 需自行 npm i async-mutex 并 require 导入
const semaphore = new Semaphore(3) // 最多3个并发请求
async function limitedRequest(url, options) {
await semaphore.acquire()
try {
return await axios.get(url, options)
} finally {
semaphore.release()
}
}🔒 安全注意事项
1. 输入验证
始终对用户输入或外部传入的参数进行验证:
function validateMusicInfo(musicInfo) {
if (!musicInfo || typeof musicInfo !== 'object') {
throw new Error('音乐信息格式错误')
}
if (!musicInfo.id || typeof musicInfo.id !== 'string') {
throw new Error('音乐 ID 无效')
}
return true
}2. URL 验证
在返回或使用 URL 时,验证其有效性和协议:
function isValidUrl(url) {
try {
const urlObj = new URL(url)
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:'
} catch {
return false
}
}3. 敏感信息保护
不要在日志中输出敏感信息,如密钥、Token 或密码。
console.log('请求参数:', {
...params,
token: '***', // 隐藏敏感信息
password: '***'
})📤 插件发布
1. 代码检查清单
发布前请确保满足以下要求:
- [ ] 插件信息 (
plugin.json) 注释完整 - [ ] 错误处理(
try...catch)完善 - [ ] 性能优化(如并发控制)合理
- [ ] 输入和 URL 安全验证到位
- [ ] 核心功能测试覆盖充分
2. 版本管理
请使用语义化版本号 (Major.Minor.Patch):
- 主版本 (Major):进行了不兼容的 API 修改。
- 次版本 (Minor):新增了向下兼容的功能。
- 修订版本 (Patch):进行了向下兼容的问题修正。
❓ 常见问题 (FAQ)
Q: 插件加载失败怎么办?
A: 检查以下几点:
- 文件编码是否为 UTF-8
- 插件信息注释 (
plugin.json) 格式是否正确 - JavaScript 语法是否有错误,是否使用了 CommonJS 语法
- 是否正确导出了必需的方法(在
index.js或拓展插件中)
Q: 如何处理跨域请求?
A: QZ Music v2 的插件运行环境不受浏览器跨域限制,可以直接请求任何域名的 API。
Q: 插件如何更新?
A: 暂未支持自动更新,需要手动替换插件文件。
🤝 技术支持
如有问题或建议,请通过 Q 群私信 联系。