Files
Koneko_api_for_QZ-Music/6a2d8996cc974039d1dfbbf7_Koneko插件开发避坑指南_v0.0.3.md

360 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Koneko QZ Music v2 插件开发避坑指南
> 版本: 0.0.3 | 作者: 云汀(Miao-moe) | 目标: 支持到别的 AI 继续开发
---
## 一、项目背景
QZ Music v2 是一款 Android 音乐播放器,支持通过**拓展插件**接入多平台音源。插件系统基于 Node.js 运行时Javet/V8每个插件是一个单独的 `.js` 文件,通过 `module.exports` 导出接口。
### 1.1 插件加载机制
- canary 12-4 版本之前:直接加载 `.js` 文件
- **canary 12-4 及之后**:需要 `文件夹 + plugin.json + index.js` 结构(但用户要求只用 `.js`,所以当前版本是单文件)
- 运行时环境变量通过 `global.env` 访问,**不是** `process.env`
### 1.2 插件导出格式
```js
module.exports = {
musicSearch: { search: fn, tipSearch: fn, hotSearch: fn },
tipSearch: { getList: fn },
hotSearch: { getList: fn },
getUrl: fn,
pluginInfo: { info: {...}, env: [...], ext: [...], quality: [...], supportFunc: [] },
// 网易云特有
userPlaylist: fn,
dailyRecommend: fn,
personalFm: fn,
myLikedSongs: fn
}
```
---
## 二、Javet/V8 兼容性大坑(最重要)
QZ Music 使用 Javet 作为 JS 运行时(基于 V8**不支持现代 ES 语法**,必须用保守写法:
| 语法 | 是否支持 | 正确写法 |
|------|---------|---------|
| `let` / `const` | ❌ | `var` |
| 箭头函数 `() => {}` | ❌ | `function() {}` |
| `async` / `await` | ❌ | `Promise` 链式调用 |
| `catch { }`(无参数)| ❌ | `catch (e) { }` |
| `Promise.allSettled` | ❌ | `Promise.all` + 手动包装 |
| `Object.entries` / `Object.values` | ❌ | `for...in` 遍历 |
| `Array.prototype.includes` | ❌ | `indexOf(...) !== -1` |
| `String.prototype.startsWith` | ❌ | `indexOf(...) === 0` |
| `BigInt` 字面量 | ⚠️ 慎用 | `BigInt('0x' + hex)` |
| `class` | ❌ | 对象字面量 |
| 模板字符串 `${}` | ✅ | 可用 |
| `Buffer` | ✅ | Node.js 内置 |
| `require` | ✅ | CommonJS |
### 2.1 Promise.allSettled 替代方案
```js
// ❌ 不支持
Promise.allSettled(promises)
// ✅ 正确写法
Promise.all(promises.map(function(p) {
return p.then(function(v) {
return { status: 'fulfilled', value: v }
}).catch(function(e) {
return { status: 'rejected', reason: e }
})
})).then(function(results) {
for (var i = 0; i < results.length; i++) {
if (results[i].status === 'fulfilled') {
return results[i].value
}
}
throw new Error('all failed')
})
```
---
## 三、搜索结果格式大坑
App 的 `MusicListResponse` 反序列化要求**必须有 `list` 字段**,不是 `songs`
### 3.1 正确的搜索返回格式
```js
return {
list: [
{
id: '歌曲ID字符串',
name: '歌曲名',
artists: '歌手1、歌手2',
albumName: '专辑名',
albumId: '专辑ID',
source: 'tx', // tx/kg/kw/wy/mg/git
pic: '封面大图URL',
mPic: '封面中图URL',
sPic: '封面小图URL',
interval: '3:45', // 播放时长 m:ss
qualities: {
standard: '3.21MB',
exhigh: '7.85MB',
lossless: '25.3MB',
hires: '48.2MB'
}
}
],
allPage: 5, // 总页数
limit: 30, // 每页条数
total: 150, // 总条数
source: 'tx' // 平台标识
}
```
### 3.2 字段名对照表
| 含义 | 正确字段名 | 错误字段名 |
|------|----------|----------|
| 歌手 | `artists` | `artist` |
| 封面图 | `pic` / `mPic` / `sPic` | `picUrl` |
| 时长 | `interval` (字符串 m:ss) | `duration` |
| 歌曲列表 | `list` | `songs` |
---
## 四、各平台 API 踩坑记录
### 4.1 QQ音乐 (tx)
**搜索签名**`zzcSign` = SHA1 + 自定义索引提取 + XOR 混淆 + base64
```js
var PART_1_INDEXES = [23, 14, 6, 36, 16, 40, 7, 19]
var PART_2_INDEXES = [16, 1, 32, 12, 19, 27, 8, 5]
var SCRAMBLE_VALUES = [89, 39, 179, 150, 218, 82, 58, 252, 177, 52, 186, 123, 120, 64, 242, 133, 143, 161, 121, 179]
```
**封面图规则**
- 有专辑ID`https://y.gtimg.cn/music/photo_new/T002R500x500M000{albumId}.jpg`
- 无专辑ID歌手图`https://y.gtimg.cn/music/photo_new/T001R500x500M000{singerMid}.jpg`
**getUrl 音质参数**API 需要带 `k`,如 `320k`,不是 `320`
### 4.2 酷狗音乐 (kg)
**搜索接口**`http://mobilecdn.kugou.com/api/v3/search/song`
**重要**:返回字段是 `errcode`(不是 `error_code`
```js
// ✅ 正确
if (result.errcode !== 0) { ... }
// ❌ 错误
if (result.error_code !== 0) { ... }
```
**封面图**:搜索结果自带 `imgurl` 字段,替换 `{size}``400`
```js
var picUrl = item.imgurl ? item.imgurl.replace('{size}', '400') : ''
```
**getUrl 音质参数**`128k` / `320k` / `999k`
### 4.3 酷我音乐 (kw)
**搜索接口**`http://search.kuwo.cn/r.s`
**封面图**`https://img2.kuwo.cn/star/albumcover/300/{ALBUMID}.jpg`
**音质信息**:在 `N_MINFO` 字段中,格式为 `level:xxx,bitrate:xxx,format:xxx,size:xxx;...`
```js
var parts = info.N_MINFO.split(';')
for (var j = 0; j < parts.length; j++) {
var m = parts[j].match(/level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/)
if (m) {
if (m[2] === '20900') qualities.jymaster = m[4]
else if (m[2] === '4000') qualities.hires = m[4]
else if (m[2] === '2000') qualities.lossless = m[4]
else if (m[2] === '320') qualities.exhigh = m[4]
else if (m[2] === '128') qualities.standard = m[4]
}
}
```
**getUrl 音质参数**`128k` / `320k` / `999k`
### 4.4 网易云音乐 (wy)
**搜索接口**`https://music.163.com/api/search/get/web`(简单 GET不需要 weapi 加密)
**封面图**`picId` 需要 Base64 编码后拼接
```js
var picIdStr = String(s.album.picId)
var picIdB64 = Buffer.from(picIdStr).toString('base64').replace(/=/g, '')
var pic = 'https://p2.music.126.net/' + picIdB64 + '/' + picIdStr + '.jpg'
```
**getUrl 音质参数**:数字格式 `128000` / `320000` / `999000`不是带k的
**加密接口**
- `eapi`AES-128-ECBkey = `e82ckenh8dichen8`
- `weapi`AES-128-CBC + RSA用于需要登录的接口
**ext 功能**
- `userPlaylist`:需要 `playlist_url` 环境变量(网易云个人主页链接)
- `dailyRecommend`:每日推荐
- `personalFm`私人FM
- `myLikedSongs`:我喜欢的音乐
### 4.5 咪咕音乐 (mg)
**搜索签名**MD5 拼接
```js
var sign = crypto.createHash('md5').update(
str + signatureMd5 + 'yyapp2d16148780a1dcc7408e06336b98cfd50' + deviceId + time
).digest('hex')
```
**封面图**:搜索结果可能返回相对路径,需要拼接域名
```js
var img = data.img3 || data.img2 || data.img1 || ''
if (img && img.indexOf('http') !== 0) img = 'https://d.musicapp.migu.cn' + img
```
**getUrl 音质参数**`128k` / `320k` / `999k`
### 4.6 GIT音源 (git)
- 无搜索功能(返回空列表)
- 纯音源插件,只有 `getUrl`
- getUrl 音质参数:`128k` / `320k` / `999k`
---
## 五、getUrl 测速容灾逻辑
所有平台统一使用**并发测速**模式:同时请求多个 API取第一个成功的结果。
```js
function getUrl(songId, quality) {
var apis = buildApis(songId, quality)
var promises = []
for (var i = 0; i < apis.length; i++) {
(function(api) {
promises.push(
httpGet(api.url, api.headers, 8000).then(function(res) {
var url = api.extract(res)
if (url) return { name: api.name, url: url }
throw new Error('no url')
}).catch(function(err) {
throw err
})
)
})(apis[i])
}
return Promise.all(promises.map(function(p) {
return p.then(function(v) { return { status: 'fulfilled', value: v } })
.catch(function(e) { return { status: 'rejected', reason: e } })
})).then(function(results) {
for (var i = 0; i < results.length; i++) {
if (results[i].status === 'fulfilled') return results[i].value.url
}
return ''
})
}
```
### 5.1 音源 API 列表
| API | 支持平台 | 特点 |
|-----|---------|------|
| 聆澜 | 全部 | 需要 `ceru_key`,最稳定 |
| HUIBQ (lxmusicapi) | 全部 | `X-Request-Key: share-v3` |
| 星海 | 全部 | 聚合接口 |
| 念心 | tx/kg/kw/mg | 个人维护 |
| 长青 | tx/kg/kw/mg | 个人维护 |
| 星海备 | 全部 | 备用 |
| fish | 全部 | 个人维护 |
| HYW | 全部 | 需要 `X-Card-Key` |
| 忆音 | tx | 直接返回 URL |
| 收集QQ | tx | 专用 |
| 收集KW | kw | 专用 |
| bb | wy | 网易云专用 |
| ymc | wy | 网易云专用 |
| unms | wy | 网易云专用 |
| 官方 | wy | 网易云官方 weapi |
---
## 六、版本号管理规范
**所有平台统一版本号**,每次修改全部升级:
- 当前版本:`0.0.3`
- 下次修改:`0.0.4`
- 再下次:`0.0.5`
文件名格式:`Koneko_{平台名}_v{版本号}.js`
---
## 七、常见报错与解决
| 报错 | 原因 | 解决 |
|------|------|------|
| `Cannot find module 'axios'` | 用了 axios | 改用 `http`/`https` 内置模块 |
| `String cannot be converted to JSONObject` | 搜索返回了非对象/字符串 | 加 `.catch()` 兜底返回正确格式 |
| `Field 'list' is required` | 搜索返回 `songs` 而非 `list` | 改字段名为 `list` |
| `SyntaxError: Invalid or unexpected token` | 用了 `catch { }` | 改为 `catch (e) { }` |
| `FileNotFoundException: plugin.json` | 12-4版本需要文件夹结构 | 创建 `plugin.json` + `index.js` |
| 搜索无结果 | 字段名不匹配 | 检查 `errcode` vs `error_code` |
| 播放失败 | `mapBr` 返回格式不对 | QQ/kg/kw/mg/git 用 `320k`wy 用 `320000` |
| 封面图不显示 | URL 格式错误或跨域 | 检查各平台封面图拼接规则 |
---
## 八、环境变量
```js
var env = global.env || {}
var CERU_KEY = env.ceru_key || '' // 聆澜API密钥
var WY_COOKIE = env.cookie || '' // 网易云Cookie
var PLAYLIST_URL = env.playlist_url || '' // 网易云个人主页链接
```
在 QZ Music 设置中配置环境变量,插件通过 `global.env` 读取。
---
## 九、完整代码参考
6个平台的完整代码见产物目录
- `Koneko_QQ音乐_v0.0.3.js`
- `Koneko_酷狗音乐_v0.0.3.js`
- `Koneko_酷我音乐_v0.0.3.js`
- `Koneko_网易云音乐_v0.0.3.js`
- `Koneko_咪咕音乐_v0.0.3.js`
- `Koneko_GIT音源_v0.0.3.js`
---
## 十、后续开发建议
1. **每次修改全部平台统一升级版本号**
2. **先在浏览器/ curl 测试 API 是否可用**
3. **注意 Javet 兼容性,避免现代 JS 语法**
4. **搜索返回务必包含 `list` 字段**
5. **getUrl 注意各平台音质参数格式差异**
6. **封面图 URL 确保可访问(注意跨域和防盗链)**
7. **所有异步操作加 `.catch()` 兜底**
8. **优先使用官方搜索接口,音源用第三方 API 容灾**