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

10 KiB
Raw Permalink Blame History

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 插件导出格式

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 替代方案

// ❌ 不支持
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 正确的搜索返回格式

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

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]

封面图规则

  • 有专辑IDhttps://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

// ✅ 正确
if (result.errcode !== 0) { ... }

// ❌ 错误
if (result.error_code !== 0) { ... }

封面图:搜索结果自带 imgurl 字段,替换 {size}400

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;...

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 编码后拼接

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的

加密接口

  • eapiAES-128-ECBkey = e82ckenh8dichen8
  • weapiAES-128-CBC + RSA用于需要登录的接口

ext 功能

  • userPlaylist:需要 playlist_url 环境变量(网易云个人主页链接)
  • dailyRecommend:每日推荐
  • personalFm私人FM
  • myLikedSongs:我喜欢的音乐

4.5 咪咕音乐 (mg)

搜索签名MD5 拼接

var sign = crypto.createHash('md5').update(
  str + signatureMd5 + 'yyapp2d16148780a1dcc7408e06336b98cfd50' + deviceId + time
).digest('hex')

封面图:搜索结果可能返回相对路径,需要拼接域名

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取第一个成功的结果。

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 用 320kwy 用 320000
封面图不显示 URL 格式错误或跨域 检查各平台封面图拼接规则

八、环境变量

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 容灾