82 KiB
Koneko 插件开发踩坑与最终代码文档
版本:0.0.3 作者:云汀(Miao-moe) 整理日期:2026-06-19 用途:汇总与 QZ Music v2 拓展插件开发相关的全部踩坑点、最终代码、链接/API,便于迁移到其他 AI/工具继续迭代。
一、项目背景与目标
为 QZ Music v2 编写 6 个音乐平台拓展插件:QQ音乐、酷狗音乐、酷我音乐、网易云音乐、咪咕音乐、GIT音源。
核心需求:
- 官方搜索可用
- 多 API 音源测速容灾
- 环境变量读取
global.env - 版本号从 0.0.1 开始,每次修改统一升版
- 仅支持单
.js文件格式(QZ Music v2 实际加载的是.js文件,plugin.json内嵌在代码里)
二、重大踩坑点(按时间顺序)
1. axios 不可用
QZ Music v2 运行环境没有 axios,必须用 Node.js 内置 http/https 模块。
2. 插件格式演变
- 最初直接写
module.exports = { ... },后来发现 app 期望plugin.musicSearch.search(...)这样的对象方法格式。 - 12-4 版本之前尝试过 folder + plugin.json + index.js 结构,但用户最终确认只要单
.js文件。 pluginInfo必须内嵌在module.exports里,包含info、env、ext、quality、supportFunc。
3. 环境变量读取
用 global.env,不是 process.env。
var env = global.env || {}
var CERU_KEY = env.ceru_key || ''
4. Javet / V8 语法兼容性(导致"闪退")
必须使用最保守的 ES5 风格语法:
var代替let/const- 传统
function声明,不用箭头函数 - 不用
async/await,只用 Promise 链式调用 catch必须带参数:catch (e) { },不能写catch { }- 不用
Promise.allSettled,自己用Promise.all + .then/.catch包装 - 不用对象解构等现代语法
5. 搜索结果字段必须严格匹配
app 的 MusicListResponse 要求返回:
{
list: [...],
allPage: number,
limit: number,
total: number,
source: string
}
单首歌曲字段要包含:
id:歌曲唯一标识name:歌名artists:歌手名(用/或、分隔)albumName/albumIdpic、mPic、sPic:封面图interval:时长字符串m:ssqualities:音质大小映射source:平台标识
网易云搜索之前用 { songs: [], total: 0 } 导致报错:Field 'list' is required for type ...。
6. 音质参数格式
HUIBQ 等第三方 API 要求音质参数带 k,如 320k、128k、999k。
mapBr 返回 '320' 时返回的是 fallback 无效 URL,导致所有平台播放失败。
7. 网易云封面图
网易云搜索 API 返回的 album.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'
8. 酷狗搜索接口字段名
mobilecdn.kugou.com 接口返回的字段是 errcode 不是 error_code,且歌曲信息字段为 hash、songname、singername、imgurl。
9. 酷狗封面图
最初错误地用了酷我的 img2.kuwo.cn 域名。应使用酷狗搜索接口返回的 imgurl,替换 {size} 为 400。
10. 测速容灾模式
所有平台 getUrl 采用并发请求多个 API,取第一个成功结果:
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 ''
})
11. 版本号管理
每次修改所有插件统一升级版本号。当前最终版本:0.0.3。
三、环境变量说明
| key | 用途 | 必填 |
|---|---|---|
ceru_key |
聆澜音源 API 密钥 | 否 |
playlist_url |
网易云个人主页链接,用于 userPlaylist |
否 |
cookie |
网易云 Cookie,用于搜索和 ext 功能 | 否 |
四、所有相关链接/API
4.1 文档/参考链接
- QZ Music v2 插件文档(用户自维护,非公开链接)
- CSDN 网易云搜索 API:https://blog.csdn.net/2301_79279502/article/details/135568447
- CSDN 酷狗 API 整理:https://blog.csdn.net/2301_78245299/article/details/140352615
- 旧版插件参考:
Koneko_酷狗音乐_v1.0.9.js - 旧日志:
log_2026-06-19_0.txt
4.2 QQ音乐相关 API
- QQ音乐搜索签名接口:
https://u.y.qq.com/cgi-bin/musics.fcg?sign={zzcSign} - QQ音乐搜索 POST:
https://u.y.qq.com/cgi-bin/musics.fcg?sign= - QQ音乐热词:
https://u.y.qq.com/cgi-bin/musicu.fcg - QQ音乐搜索建议:
https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg - QQ音乐歌手封面:
https://y.gtimg.cn/music/photo_new/T001R500x500M000{singerMid}.jpg - QQ音乐专辑封面:
https://y.gtimg.cn/music/photo_new/T002R500x500M000{albumMid}.jpg - 聆澜 QQ 音源:
https://source.shiqianjiang.cn/api/music/url?source=tx&songId={id}&quality={q} - HUIBQ QQ 音源:
https://lxmusicapi.onrender.com/url/tx/{id}/{q} - 忆音 QQ 音源:
https://music.3e0.cn/?server=tencent&type=url&id={id} - 星海 QQ 音源:
https://music-api.gdstudio.xyz/api.php?types=url&source=tencent&id={id}&br={br} - 收集 QQ 音源:
https://cyapi.top/API/qq_music.php?apikey=...&type=json&mid={id} - 念心 QQ 音源:
https://music.nxinxz.com/kgqq/tx.php?id={id}&level={q}&type=mp3 - 长青 QQ 音源:
http://175.27.166.236/kgqq/qq.php?type=mp3&id={id}&level={q} - 星海备 QQ 音源:
https://music-dl.sayqz.com/api/?source=qq&id={id}&type=url&br={q} - fish QQ 音源:
https://m-api.ceseet.me/url/tx/{id}/{q} - HYW QQ 音源:
https://music.bxa241d4.shop/api/music/url?source=tx&songId={id}&quality={q}
4.3 酷狗音乐相关 API
- 酷狗搜索(mobilecdn):
http://mobilecdn.kugou.com/api/v3/search/song?format=json&keyword={kw}&page={p}&pagesize={n} - 酷狗搜索(songsearch):
https://songsearch.kugou.com/song_search_v2?keyword={kw}&page={p}&pagesize={n}&... - 酷狗搜索建议:
https://searchtip.kugou.com/getSearchTip?MusicTipCount=10&keyword={kw} - 酷狗热词:
http://gateway.kugou.com/api/v3/search/hot_tab?... - 聆澜 酷狗音源:
https://source.shiqianjiang.cn/api/music/url?source=kg&songId={id}&quality={q} - HUIBQ 酷狗音源:
https://lxmusicapi.onrender.com/url/kg/{id}/{q} - 星海 酷狗音源:
https://music-api.gdstudio.xyz/api.php?types=url&source=kugou&id={id}&br={br} - 念心 酷狗音源:
https://music.nxinxz.com/kgqq/kg.php?id={id}&level={q}&type=mp3 - 长青 酷狗音源:
https://music.haitangw.cc/kgqq/kg.php?id={id}&level={q}&type=mp3 - 星海备 酷狗音源:
https://music-dl.sayqz.com/api/?source=kugou&id={id}&type=url&br={q} - fish 酷狗音源:
https://m-api.ceseet.me/url/kg/{id}/{q} - HYW 酷狗音源:
https://music.bxa241d4.shop/api/music/url?source=kg&songId={id}&quality={q}
4.4 酷我音乐相关 API
- 酷我搜索:
http://search.kuwo.cn/r.s?client=kt&all={kw}&pn={p}&rn={n}&... - 酷我搜索建议:
https://tips.kuwo.cn/t.s?...&w={kw}&... - 酷我热词:
http://hotword.kuwo.cn/hotword.s?... - 酷我专辑封面:
https://img2.kuwo.cn/star/albumcover/300/{albumId}.jpg - 酷我歌手封面:
http://artistpicserver.kuwo.cn/pic.web?...&rid={rid} - 聆澜 酷我音源:
https://source.shiqianjiang.cn/api/music/url?source=kw&songId={id}&quality={q} - HUIBQ 酷我音源:
https://lxmusicapi.onrender.com/url/kw/{id}/{q} - 星海 酷我音源:
https://music-api.gdstudio.xyz/api.php?types=url&source=kuwo&id={id}&br={br} - 收集 KW 音源:
https://kw-api.cenguigui.cn/api/song/url?id={id}&quality={q} - 念心 酷我音源:
https://music.nxinxz.com/kgqq/kw.php?id={id}&level={q}&type=mp3 - 长青 酷我音源:
https://musicapi.haitangw.net/music/kw.php?id={id}&level={q}&type=mp3 - 星海备 酷我音源:
https://music-dl.sayqz.com/api/?source=kuwo&id={id}&type=url&br={q} - fish 酷我音源:
https://m-api.ceseet.me/url/kw/{id}/{q} - HYW 酷我音源:
https://music.bxa241d4.shop/api/music/url?source=kw&songId={id}&quality={q}
4.5 网易云音乐相关 API
- 网易云搜索(GET):
https://music.163.com/api/search/get/web?csrf_token=&hlposttag=&s={kw}&type=1&offset={o}&total=true&limit={n} - 网易云搜索(weapi POST,备用):
https://music.163.com/weapi/cloudsearch/get/web - 网易云热词(weapi):
https://music.163.com/weapi/search/hot - 网易云官方播放链接(weapi):
https://music.163.com/weapi/song/enhance/player/url - 网易云用户歌单(weapi):
https://music.163.com/weapi/user/playlist - 网易云每日推荐(weapi):
https://music.163.com/weapi/v1/discovery/recommend/songs - 网易云私人 FM(weapi):
https://music.163.com/weapi/v1/radio/get - 网易云喜欢歌曲(weapi):
https://music.163.com/weapi/song/like/get - 网易云歌曲详情(weapi):
https://music.163.com/weapi/v3/song/detail - 网易云封面图:
https://p2.music.126.net/{base64(picId)}/{picId}.jpg - 聆澜 网易云音源:
https://ceruapi.lol/meting-api-0/?server=netease&type=url&id={id}&auth={key}&br={br} - bb 网易云音源:
https://api.bbdcz.cn/music/netease/url?id={id}&br={br} - lx 网易云音源:
https://lxmusicapi.onrender.com/url/wy/{id}/{br} - ymc 网易云音源:
https://api.ymusic.icu/netease/song?id={id}&quality={br} - unms 网易云音源:
https://unms.zeabur.app/netease/url?id={id}&br={br}
4.6 咪咕音乐相关 API
- 咪咕搜索:
https://jadeite.migu.cn/music_search/v3/search/searchAll?... - 聆澜 咪咕音源:
https://source.shiqianjiang.cn/api/music/url?source=mg&songId={id}&quality={q} - HUIBQ 咪咕音源:
https://lxmusicapi.onrender.com/url/mg/{id}/{q} - 星海 咪咕音源:
https://music-api.gdstudio.xyz/api.php?types=url&source=migu&id={id}&br={br} - 念心 咪咕音源:
https://music.nxinxz.com/kgqq/mg.php?id={id}&level={q}&type=mp3 - 长青 咪咕音源:
https://music.haitangw.cc/musicapi/mg.php?id={id}&level={q}&type=mp3 - 星海备 咪咕音源:
https://music-dl.sayqz.com/api/?source=migu&id={id}&type=url&br={q} - fish 咪咕音源:
https://m-api.ceseet.me/url/mg/{id}/{q} - HYW 咪咕音源:
https://music.bxa241d4.shop/api/music/url?source=mg&songId={id}&quality={q}
4.7 GIT音源相关 API
- 聆澜 GIT 音源:
https://source.shiqianjiang.cn/api/music/url?source=git&songId={id}&quality={q} - HUIBQ GIT 音源:
https://lxmusicapi.onrender.com/url/git/{id}/{q}
4.8 其他提及但未使用/备用
https://music.163.com/weapi系列(weapi 备用)https://m.kugou.com/rank/list&json=truehttp://mobilecdnbj.kugou.com/api/v3/tag/list?pid=0&apiver=2&plat=0
五、最终代码
以下 6 个文件版本号均为 0.0.3,是截至 2026-06-19 的最终可用代码。
Koneko_QQ音乐_v0.0.3
'use strict'
var https = require('https')
var http = require('http')
var crypto = require('crypto')
var env = global.env || {}
var CERU_KEY = env.ceru_key || ''
var HEADERS_COMMON = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
function mapBr(q) {
if (q === '128k' || q === 'standard') return '128k'
if (q === '320k' || q === 'exhigh') return '320k'
return '999k'
}
function httpGet(url, headers, timeout) {
return new Promise(function(resolve, reject) {
var mod = url.indexOf('https') === 0 ? https : http
var req = mod.get(url, { headers: headers || {}, timeout: timeout || 8000 }, function(res) {
var data = ''
res.on('data', function(chunk) { data += chunk })
res.on('end', function() {
try { resolve(JSON.parse(data)) } catch(e) { resolve(data) }
})
})
req.on('error', function(err) { reject(err) })
req.on('timeout', function() { req.destroy(); reject(new Error('timeout')) })
})
}
function httpPost(url, body, headers, timeout) {
return new Promise(function(resolve, reject) {
var mod = url.indexOf('https') === 0 ? https : http
var postData = typeof body === 'string' ? body : JSON.stringify(body)
var opts = {
method: 'POST',
headers: {},
timeout: timeout || 10000
}
if (headers) {
for (var k in headers) { opts.headers[k] = headers[k] }
}
opts.headers['Content-Type'] = 'application/json'
opts.headers['Content-Length'] = Buffer.byteLength(postData)
var req = mod.request(url, opts, function(res) {
var data = ''
res.on('data', function(chunk) { data += chunk })
res.on('end', function() {
try { resolve(JSON.parse(data)) } catch(e) { resolve(data) }
})
})
req.on('error', function(err) { reject(err) })
req.on('timeout', function() { req.destroy(); reject(new Error('timeout')) })
req.write(postData)
req.end()
})
}
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]
function zzcSign(text) {
var hash = crypto.createHash('sha1').update(text).digest('hex')
var part1 = ''
for (var i = 0; i < PART_1_INDEXES.length; i++) { part1 += hash[PART_1_INDEXES[i]] }
var part2 = ''
for (var i = 0; i < PART_2_INDEXES.length; i++) { part2 += hash[PART_2_INDEXES[i]] }
var part3 = []
for (var i = 0; i < SCRAMBLE_VALUES.length; i++) {
part3.push(SCRAMBLE_VALUES[i] ^ parseInt(hash.slice(i * 2, i * 2 + 2), 16))
}
var b64Part = Buffer.from(part3).toString('base64').replace(/[\/+=]/g, '')
return ('zzc' + part1 + b64Part + part2).toLowerCase()
}
function signRequest(data) {
var sign = zzcSign(JSON.stringify(data))
return httpPost('https://u.y.qq.com/cgi-bin/musics.fcg?sign=' + sign, data, {
'User-Agent': 'QQMusic 14090508(android 12)'
})
}
function formatPlayTime(seconds) {
if (!seconds || isNaN(seconds)) return '--/--'
var m = Math.floor(seconds / 60)
var s = seconds % 60
return m + ':' + (s < 10 ? '0' : '') + s
}
function formatSingerName(singers) {
if (!singers || singers.length === 0) return ''
var names = []
for (var i = 0; i < singers.length; i++) {
if (singers[i].name) names.push(singers[i].name)
}
return names.join('、')
}
function formatSize(bytes) {
if (!bytes) return ''
var n = parseFloat(bytes)
if (isNaN(n) || n < 0) return ''
return (n / (1024 * 1024)).toFixed(2) + 'MB'
}
var musicSearch = {
limit: 30,
total: 0,
page: 0,
allPage: 1,
musicSearch: function(str, page, limit, retryNum) {
var self = this
if (retryNum === undefined) retryNum = 0
if (retryNum > 3) return Promise.reject(new Error('搜索失败'))
var data = {
comm: {
ct: '11', cv: '14090508', v: '14090508', tmeAppID: 'qqmusic',
phonetype: 'EBG-AN10', deviceScore: '553.47', devicelevel: '50', newdevicelevel: '20',
rom: 'HuaWei/EMOTION/EmotionUI_14.2.0', os_ver: '12',
OpenUDID: '0', OpenUDID2: '0', QIMEI36: '0', udid: '0',
chid: '0', aid: '0', oaid: '0', taid: '0', tid: '0', wid: '0', uid: '0', sid: '0',
modeSwitch: '6', teenMode: '0', ui_mode: '2', nettype: '1020', v4ip: ''
},
req: {
module: 'music.search.SearchCgiService',
method: 'DoSearchForQQMusicMobile',
param: {
search_type: 0, searchid: Math.random().toString().slice(2),
query: str, page_num: page, num_per_page: limit,
highlight: 0, nqc_flag: 0, multi_zhida: 0, cat: 2, grp: 1, sin: 0, sem: 0
}
}
}
return signRequest(data).then(function(body) {
if (!body || typeof body !== 'object' || body.code !== 0 || !body.req || body.req.code !== 0) {
return self.musicSearch(str, page, limit, retryNum + 1)
}
return body.req.data
})
},
handleResult: function(rawList) {
if (!rawList || rawList.length === 0) return []
var list = []
for (var i = 0; i < rawList.length; i++) {
var item = rawList[i]
if (!item.file || !item.file.media_mid) continue
var albumId = ''
var albumName = ''
if (item.album) { albumName = item.album.name; albumId = item.album.mid }
var picUrl = ''
if (albumId === '' || albumId === '空') {
if (item.singer && item.singer.length) {
picUrl = 'https://y.gtimg.cn/music/photo_new/T001R500x500M000' + item.singer[0].mid + '.jpg'
}
} else {
picUrl = 'https://y.gtimg.cn/music/photo_new/T002R500x500M000' + albumId + '.jpg'
}
var qualities = {}
if (item.file && item.file.size_128mp3) qualities.standard = formatSize(item.file.size_128mp3)
if (item.file && item.file.size_320mp3) qualities.exhigh = formatSize(item.file.size_320mp3)
if (item.file && item.file.size_flac) qualities.lossless = formatSize(item.file.size_flac)
if (item.file && item.file.size_hires) qualities.hires = formatSize(item.file.size_hires)
list.push({
id: String(item.mid),
name: item.name + (item.title_extra || ''),
artists: formatSingerName(item.singer),
source: 'tx',
pic: picUrl,
mPic: picUrl,
sPic: picUrl,
albumName: albumName,
albumId: String(albumId || ''),
interval: String(formatPlayTime(item.interval) || '--/--'),
qualities: qualities
})
}
return list
},
search: function(str, page, limit) {
var self = this
if (!page) page = 1
if (limit == null) limit = this.limit
return this.musicSearch(str, page, limit).then(function(data) {
if (!data || typeof data !== 'object') {
return { list: [], allPage: 1, limit: 30, total: 0, source: 'tx' }
}
var list = self.handleResult(data.body && data.body.item_song ? data.body.item_song : [])
if (!list || list.length === 0) {
return { list: [], allPage: 1, limit: 30, total: 0, source: 'tx' }
}
self.total = data.meta && data.meta.estimate_sum ? data.meta.estimate_sum : 0
self.page = page
self.allPage = Math.ceil(self.total / limit)
return { list: list, allPage: self.allPage, limit: limit, total: self.total, source: 'tx' }
}).catch(function(e) {
return { list: [], allPage: 1, limit: 30, total: 0, source: 'tx' }
})
}
}
var tipSearch = {
getList: function(str) {
return httpGet(
'https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg?is_xml=0&format=json&key=' + encodeURIComponent(str) + '&loginUin=0&hostUin=0&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0',
{ Referer: 'https://y.qq.com/portal/player.html' }
).then(function(body) {
if (!body || typeof body !== 'object' || body.code !== 0) return []
var result = { order: [], songs: [], artists: [], albums: [] }
if (body.data && body.data.song && body.data.song.count > 0) result.order.push('songs')
if (body.data && body.data.singer && body.data.singer.count > 0) result.order.push('artists')
if (body.data && body.data.album && body.data.album.count > 0) result.order.push('albums')
if (body.data && body.data.song && body.data.song.itemlist) {
for (var i = 0; i < body.data.song.itemlist.length; i++) {
var item = body.data.song.itemlist[i]
result.songs.push({ name: item.name, artist: { name: item.singer } })
}
}
if (body.data && body.data.singer && body.data.singer.itemlist) {
for (var i = 0; i < body.data.singer.itemlist.length; i++) {
result.artists.push({ name: body.data.singer.itemlist[i].name })
}
}
if (body.data && body.data.album && body.data.album.itemlist) {
for (var i = 0; i < body.data.album.itemlist.length; i++) {
result.albums.push({ name: body.data.album.itemlist[i].name })
}
}
return result
}).catch(function(e) { return [] })
}
}
var hotSearch = {
getList: function() {
var data = {
comm: { ct: '19', cv: '1803', guid: '0', patch: '118' },
hotkey: {
method: 'GetHotkeyForQQMusicPC',
module: 'tencent_musicsoso_hotkey.HotkeyService',
param: { search_id: '', uin: 0 }
}
}
return httpPost('https://u.y.qq.com/cgi-bin/musicu.fcg', data, {
Referer: 'https://y.qq.com/portal/player.html'
}).then(function(body) {
if (!body || typeof body !== 'object' || body.code !== 0 || !body.hotkey || !body.hotkey.data) return []
var list = []
for (var i = 0; i < body.hotkey.data.vec_hotkey.length; i++) {
list.push(body.hotkey.data.vec_hotkey[i].query)
}
return list
}).catch(function(e) { return [] })
}
}
function buildApis(songId, q) {
var br = mapBr(q)
var apis = []
if (CERU_KEY) {
apis.push({
name: '聆澜',
url: 'https://source.shiqianjiang.cn/api/music/url?source=tx&songId=' + songId + '&quality=' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-API-Key': CERU_KEY },
extract: function(res) { return res && res.code === 200 && res.url ? res.url : null }
})
}
apis.push(
{
name: 'HUIBQ',
url: 'https://lxmusicapi.onrender.com/url/tx/' + songId + '/' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-Request-Key': 'share-v3' },
extract: function(res) { return res && res.code === 0 && res.url ? res.url : null }
},
{
name: '忆音',
url: 'https://music.3e0.cn/?server=tencent&type=url&id=' + songId,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (typeof res === 'string' && res.indexOf('http') === 0) return res
return null
}
},
{
name: '星海',
url: 'https://music-api.gdstudio.xyz/api.php?types=url&source=tencent&id=' + songId + '&br=' + br,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '收集QQ',
url: 'https://cyapi.top/API/qq_music.php?apikey=4d6f7369632d6170692e63656e6775696769692e636f6d&type=json&mid=' + songId,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '念心',
url: 'https://music.nxinxz.com/kgqq/tx.php?id=' + songId + '&level=' + q + '&type=mp3',
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '长青',
url: 'http://175.27.166.236/kgqq/qq.php?type=mp3&id=' + songId + '&level=' + q,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '星海备',
url: 'https://music-dl.sayqz.com/api/?source=qq&id=' + songId + '&type=url&br=' + q,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: 'fish',
url: 'https://m-api.ceseet.me/url/tx/' + songId + '/' + q,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: 'HYW',
url: 'https://music.bxa241d4.shop/api/music/url?source=tx&songId=' + songId + '&quality=' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-Card-Key': 'TF-VSS0-8Y73-U1AW-GEXJ' },
extract: function(res) {
if (res && res.url) return res.url
if (res && res.code === 200 && res.data && res.data.url) return res.data.url
return null
}
}
)
return apis
}
function getUrl(songId, quality) {
var q = quality || '320k'
var apis = buildApis(songId, q)
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) {
console.log('[Koneko QQ音乐] ' + api.name + ' 成功')
return { name: api.name, url: url }
}
throw new Error(api.name + ' 无有效URL')
}).catch(function(err) {
console.error('[Koneko QQ音乐] ' + api.name + ' 失败: ' + err.message)
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
}
console.error('[Koneko QQ音乐] 所有API均失败')
return ''
})
}
var leaderboard = { getList: function() { return Promise.resolve([]) } }
var songList = {
getListDetail: function(id, page, limit) {
return Promise.resolve({ list: [], page: page || 1, limit: limit || 30, total: 0, source: 'tx', info: { name: '', img: '', desc: '', author: '' } })
}
}
var singer = { getInfo: function(id) { return Promise.resolve(null) } }
var album = {
getListDetail: function(id) {
return Promise.resolve({ list: [], page: 1, limit: 1000, total: 0, source: 'tx', info: { name: '', img: '', desc: '', author: '' } })
},
search: function(str, page, limit) { return Promise.resolve([]) }
}
function getLyric(id) { return Promise.resolve('') }
function getPic(songId) { return Promise.resolve('') }
function musicDetail(id) { return Promise.resolve(null) }
function musicInfo(id) { return Promise.resolve(null) }
var pluginInfo = {
info: { id: 'koneko_tx', name: 'QQ音乐 - Koneko', version: '0.0.3', description: 'QQ音乐聚合音源插件,官方搜索+多API音源,自动测速容灾切换' },
env: [{ key: 'ceru_key', name: '聆澜API Key', description: '聆澜音源API密钥,留空则跳过聆澜音源' }],
ext: [],
quality: [
{ name: '标准音质', ui: '标', id: 'standard' },
{ name: '高品音质', ui: 'HQ', id: 'exhigh' },
{ name: '无损音质', ui: 'SQ', id: 'lossless' },
{ name: 'Hi-Res', ui: 'HR', id: 'hires' }
],
supportFunc: []
}
module.exports = {
musicSearch: musicSearch,
tipSearch: tipSearch,
leaderboard: leaderboard,
songList: songList,
hotSearch: hotSearch,
singer: singer,
album: album,
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo,
pluginInfo: pluginInfo
}
Koneko_酷狗音乐_v0.0.3
'use strict'
var https = require('https')
var http = require('http')
var env = global.env || {}
var CERU_KEY = env.ceru_key || ''
var HEADERS_COMMON = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
function mapBr(q) {
if (q === '128k' || q === 'standard') return '128k'
if (q === '320k' || q === 'exhigh') return '320k'
return '999k'
}
function httpGet(url, headers, timeout) {
return new Promise(function(resolve, reject) {
var mod = url.indexOf('https') === 0 ? https : http
var req = mod.get(url, { headers: headers || {}, timeout: timeout || 8000 }, function(res) {
var data = ''
res.on('data', function(chunk) { data += chunk })
res.on('end', function() {
try { resolve(JSON.parse(data)) } catch(e) { resolve(data) }
})
})
req.on('error', function(err) { reject(err) })
req.on('timeout', function() { req.destroy(); reject(new Error('timeout')) })
})
}
function formatPlayTime(seconds) {
if (!seconds || isNaN(seconds)) return '--/--'
var m = Math.floor(seconds / 60)
var s = seconds % 60
return m + ':' + (s < 10 ? '0' : '') + s
}
var musicSearch = {
limit: 30,
total: 0,
page: 0,
allPage: 1,
search: function(str, page, limit, retryNum) {
var self = this
if (retryNum === undefined) retryNum = 0
if (++retryNum > 3) return Promise.reject(new Error('搜索失败'))
if (!page) page = 1
if (limit == null) limit = this.limit
return httpGet(
'http://mobilecdn.kugou.com/api/v3/search/song?format=json&keyword=' + encodeURIComponent(str) + '&page=' + page + '&pagesize=' + limit,
HEADERS_COMMON
).then(function(result) {
if (!result || typeof result !== 'object' || result.errcode !== 0 || !result.data || !result.data.info) {
return self.search(str, page, limit, retryNum)
}
var list = []
for (var i = 0; i < result.data.info.length; i++) {
var item = result.data.info[i]
var picUrl = ''
if (item.imgurl && item.imgurl.indexOf('http') === 0) {
picUrl = item.imgurl.replace('{size}', '400')
}
if (!picUrl && item.album_img) {
picUrl = item.album_img
}
list.push({
id: String(item.hash || ''),
name: String(item.songname || item.song_name || ''),
artists: String(item.singername || item.singer_name || ''),
albumName: String(item.album_name || ''),
albumId: String(item.album_id || ''),
source: 'kg',
interval: String(formatPlayTime(item.duration) || '--/--'),
pic: picUrl,
mPic: picUrl,
sPic: picUrl,
qualities: {}
})
}
self.total = result.data.total
self.page = page
self.allPage = Math.ceil(self.total / limit)
return { list: list, allPage: self.allPage, limit: limit, total: self.total, source: 'kg' }
}).catch(function(e) {
return { list: [], allPage: 1, limit: 30, total: 0, source: 'kg' }
})
}
}
var tipSearch = {
getList: function(str) {
return httpGet(
'https://searchtip.kugou.com/getSearchTip?MusicTipCount=10&keyword=' + encodeURIComponent(str),
{ 'User-Agent': HEADERS_COMMON['User-Agent'], referer: 'https://www.kugou.com/' }
).then(function(body) {
if (!body || !body.data) return []
var result = { order: [], songs: [], artists: [], albums: [] }
if (body.data.songs && body.data.songs.length > 0) result.order.push('songs')
if (body.data.artists && body.data.artists.length > 0) result.order.push('artists')
result.songs = []
if (body.data.songs) {
for (var i = 0; i < body.data.songs.length; i++) {
result.songs.push({ name: body.data.songs[i].name, artist: { name: body.data.songs[i].artist } })
}
}
result.artists = []
if (body.data.artists) {
for (var i = 0; i < body.data.artists.length; i++) {
result.artists.push({ name: body.data.artists[i].name })
}
}
return result
}).catch(function(e) { return [] })
}
}
var hotSearch = {
getList: function() {
return httpGet(
'http://gateway.kugou.com/api/v3/search/hot_tab?signature=ee44edb9d7155821412d220bcaf509dd&appid=1005&clientver=10026&plat=0',
HEADERS_COMMON
).then(function(body) {
if (!body || body.error_code !== 0 || !body.data) return []
var list = []
for (var i = 0; i < body.data.length; i++) {
list.push(body.data[i].keyword || body.data[i].searchword || '')
}
return list
}).catch(function(e) { return [] })
}
}
function buildApis(songId, q) {
var br = mapBr(q)
var apis = []
if (CERU_KEY) {
apis.push({
name: '聆澜',
url: 'https://source.shiqianjiang.cn/api/music/url?source=kg&songId=' + songId + '&quality=' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-API-Key': CERU_KEY },
extract: function(res) { return res && res.code === 200 && res.url ? res.url : null }
})
}
apis.push(
{
name: 'HUIBQ',
url: 'https://lxmusicapi.onrender.com/url/kg/' + songId + '/' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-Request-Key': 'share-v3' },
extract: function(res) { return res && res.code === 0 && res.url ? res.url : null }
},
{
name: '星海',
url: 'https://music-api.gdstudio.xyz/api.php?types=url&source=kugou&id=' + songId + '&br=' + br,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '念心',
url: 'https://music.nxinxz.com/kgqq/kg.php?id=' + songId + '&level=' + q + '&type=mp3',
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '长青',
url: 'https://music.haitangw.cc/kgqq/kg.php?id=' + songId + '&level=' + q + '&type=mp3',
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '星海备',
url: 'https://music-dl.sayqz.com/api/?source=kugou&id=' + songId + '&type=url&br=' + q,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: 'fish',
url: 'https://m-api.ceseet.me/url/kg/' + songId + '/' + q,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: 'HYW',
url: 'https://music.bxa241d4.shop/api/music/url?source=kg&songId=' + songId + '&quality=' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-Card-Key': 'TF-VSS0-8Y73-U1AW-GEXJ' },
extract: function(res) {
if (res && res.url) return res.url
if (res && res.code === 200 && res.data && res.data.url) return res.data.url
return null
}
}
)
return apis
}
function getUrl(songId, quality) {
var q = quality || '320k'
var apis = buildApis(songId, q)
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) {
console.log('[Koneko 酷狗音乐] ' + api.name + ' 成功')
return { name: api.name, url: url }
}
throw new Error(api.name + ' 无有效URL')
}).catch(function(err) {
console.error('[Koneko 酷狗音乐] ' + api.name + ' 失败: ' + err.message)
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
}
console.error('[Koneko 酷狗音乐] 所有API均失败')
return ''
})
}
var leaderboard = { getList: function() { return Promise.resolve([]) } }
var songList = {
getListDetail: function(id, page, limit) {
return Promise.resolve({ list: [], page: page || 1, limit: limit || 30, total: 0, source: 'kg', info: { name: '', img: '', desc: '', author: '' } })
}
}
var singer = { getInfo: function(id) { return Promise.resolve(null) } }
var album = {
getListDetail: function(id) {
return Promise.resolve({ list: [], page: 1, limit: 1000, total: 0, source: 'kg', info: { name: '', img: '', desc: '', author: '' } })
},
search: function(str, page, limit) { return Promise.resolve([]) }
}
function getLyric(id) { return Promise.resolve('') }
function getPic(songId) { return Promise.resolve('') }
function musicDetail(id) { return Promise.resolve(null) }
function musicInfo(id) { return Promise.resolve(null) }
var pluginInfo = {
info: { id: 'koneko_kg', name: '酷狗音乐 - Koneko', version: '0.0.3', description: '酷狗音乐聚合音源插件,官方搜索+多API音源,自动测速容灾切换' },
env: [{ key: 'ceru_key', name: '聆澜API Key', description: '聆澜音源API密钥,留空则跳过聆澜音源' }],
ext: [],
quality: [
{ name: '标准音质', ui: '标', id: 'standard' },
{ name: '高品音质', ui: 'HQ', id: 'exhigh' },
{ name: '无损音质', ui: 'SQ', id: 'lossless' },
{ name: 'Hi-Res', ui: 'HR', id: 'hires' }
],
supportFunc: []
}
module.exports = {
musicSearch: musicSearch,
tipSearch: tipSearch,
leaderboard: leaderboard,
songList: songList,
hotSearch: hotSearch,
singer: singer,
album: album,
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo,
pluginInfo: pluginInfo
}
Koneko_酷我音乐_v0.0.3
'use strict'
var https = require('https')
var http = require('http')
var env = global.env || {}
var CERU_KEY = env.ceru_key || ''
var HEADERS_COMMON = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
function mapBr(q) {
if (q === '128k' || q === 'standard') return '128k'
if (q === '320k' || q === 'exhigh') return '320k'
return '999k'
}
function httpGet(url, headers, timeout) {
return new Promise(function(resolve, reject) {
var mod = url.indexOf('https') === 0 ? https : http
var req = mod.get(url, { headers: headers || {}, timeout: timeout || 8000 }, function(res) {
var data = ''
res.on('data', function(chunk) { data += chunk })
res.on('end', function() {
try { resolve(JSON.parse(data)) } catch(e) { resolve(data) }
})
})
req.on('error', function(err) { reject(err) })
req.on('timeout', function() { req.destroy(); reject(new Error('timeout')) })
})
}
function formatPlayTime(seconds) {
if (!seconds || isNaN(seconds)) return '--/--'
var m = Math.floor(seconds / 60)
var s = seconds % 60
return m + ':' + (s < 10 ? '0' : '') + s
}
var musicSearch = {
limit: 30,
total: 0,
page: 0,
allPage: 1,
search: function(str, page, limit, retryNum) {
var self = this
if (retryNum === undefined) retryNum = 0
if (++retryNum > 3) return Promise.reject(new Error('搜索失败'))
if (!page) page = 1
if (limit == null) limit = this.limit
return httpGet(
'http://search.kuwo.cn/r.s?client=kt&all=' + encodeURIComponent(str) + '&pn=' + (page - 1) + '&rn=' + limit + '&uid=794762570&ver=kwplayer_ar_9.2.2.1&vipver=1&show_copyright_off=1&newver=1&ft=music&cluster=0&strategy=2012&encoding=utf8&rformat=json&vermerge=1&mobi=1&issubtitle=1',
HEADERS_COMMON
).then(function(result) {
if (!result || !result.abslist || result.abslist.length === 0) {
return self.search(str, page, limit, retryNum)
}
var list = []
for (var i = 0; i < result.abslist.length; i++) {
var info = result.abslist[i]
var songId = (info.MUSICRID || '').replace('MUSIC_', '')
var qualities = {}
if (info.N_MINFO) {
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]
}
}
}
var picUrl = ''
if (info.ALBUMID) {
picUrl = 'https://img2.kuwo.cn/star/albumcover/300/' + info.ALBUMID + '.jpg'
} else {
picUrl = 'http://artistpicserver.kuwo.cn/pic.web?corp=kuwo&type=rid_pic&pictype=500&size=500&rid=' + songId
}
var artistStr = ''
if (info.ARTIST) artistStr = info.ARTIST.replace(/&/g, '、')
var duration = parseInt(info.DURATION)
list.push({
id: String(songId),
name: String(info.SONGNAME || ''),
artists: artistStr,
source: 'kw',
pic: picUrl,
mPic: picUrl,
sPic: picUrl,
albumName: String(info.ALBUM || ''),
albumId: String(info.ALBUMID || ''),
interval: isNaN(duration) ? '--/--' : formatPlayTime(duration),
qualities: qualities
})
}
self.total = parseInt(result.TOTAL) || 0
self.page = page
self.allPage = Math.ceil(self.total / limit)
return { list: list, allPage: self.allPage, limit: limit, total: self.total, source: 'kw' }
}).catch(function(e) {
return { list: [], allPage: 1, limit: 30, total: 0, source: 'kw' }
})
}
}
var tipSearch = {
getList: function(str) {
return httpGet(
'https://tips.kuwo.cn/t.s?corp=kuwo&newver=3&p2p=1¬race=0&c=mbox&w=' + encodeURIComponent(str) + '&encoding=utf8&rformat=json',
{ 'User-Agent': HEADERS_COMMON['User-Agent'], Referer: 'http://www.kuwo.cn/' }
).then(function(body) {
if (!body || !body.abs) return []
var result = { order: [], songs: [] }
if (body.abs.length > 0) result.order.push('songs')
result.songs = []
for (var i = 0; i < body.abs.length; i++) {
result.songs.push({ name: body.abs[i].name, artist: { name: body.abs[i].artist } })
}
return result
}).catch(function(e) { return [] })
}
}
var hotSearch = {
getList: function() {
return httpGet(
'http://hotword.kuwo.cn/hotword.s?prod=kwplayer_ar_9.3.0.1&corp=kuwo&newver=2&vipver=9.3.0.1&source=kwplayer_ar_9.3.0.1_40.apk&p2p=1¬race=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1',
{ 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)' }
).then(function(body) {
if (!body || !body.data) return []
var list = []
for (var i = 0; i < body.data.length; i++) {
list.push(body.data[i].keyword || body.data[i].searchWord || body.data[i].name || '')
}
return list
}).catch(function(e) { return [] })
}
}
function buildApis(songId, q) {
var br = mapBr(q)
var apis = []
if (CERU_KEY) {
apis.push({
name: '聆澜',
url: 'https://source.shiqianjiang.cn/api/music/url?source=kw&songId=' + songId + '&quality=' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-API-Key': CERU_KEY },
extract: function(res) { return res && res.code === 200 && res.url ? res.url : null }
})
}
apis.push(
{
name: 'HUIBQ',
url: 'https://lxmusicapi.onrender.com/url/kw/' + songId + '/' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-Request-Key': 'share-v3' },
extract: function(res) { return res && res.code === 0 && res.url ? res.url : null }
},
{
name: '星海',
url: 'https://music-api.gdstudio.xyz/api.php?types=url&source=kuwo&id=' + songId + '&br=' + br,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '收集KW',
url: 'https://kw-api.cenguigui.cn/api/song/url?id=' + songId + '&quality=' + q,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '念心',
url: 'https://music.nxinxz.com/kgqq/kw.php?id=' + songId + '&level=' + q + '&type=mp3',
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '长青',
url: 'https://musicapi.haitangw.net/music/kw.php?id=' + songId + '&level=' + q + '&type=mp3',
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '星海备',
url: 'https://music-dl.sayqz.com/api/?source=kuwo&id=' + songId + '&type=url&br=' + q,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: 'fish',
url: 'https://m-api.ceseet.me/url/kw/' + songId + '/' + q,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: 'HYW',
url: 'https://music.bxa241d4.shop/api/music/url?source=kw&songId=' + songId + '&quality=' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-Card-Key': 'TF-VSS0-8Y73-U1AW-GEXJ' },
extract: function(res) {
if (res && res.url) return res.url
if (res && res.code === 200 && res.data && res.data.url) return res.data.url
return null
}
}
)
return apis
}
function getUrl(songId, quality) {
var q = quality || '320k'
var apis = buildApis(songId, q)
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) {
console.log('[Koneko 酷我音乐] ' + api.name + ' 成功')
return { name: api.name, url: url }
}
throw new Error(api.name + ' 无有效URL')
}).catch(function(err) {
console.error('[Koneko 酷我音乐] ' + api.name + ' 失败: ' + err.message)
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
}
console.error('[Koneko 酷我音乐] 所有API均失败')
return ''
})
}
var leaderboard = { getList: function() { return Promise.resolve([]) } }
var songList = {
getListDetail: function(id, page, limit) {
return Promise.resolve({ list: [], page: page || 1, limit: limit || 30, total: 0, source: 'kw', info: { name: '', img: '', desc: '', author: '' } })
}
}
var singer = { getInfo: function(id) { return Promise.resolve(null) } }
var album = {
getListDetail: function(id) {
return Promise.resolve({ list: [], page: 1, limit: 1000, total: 0, source: 'kw', info: { name: '', img: '', desc: '', author: '' } })
},
search: function(str, page, limit) { return Promise.resolve([]) }
}
function getLyric(id) { return Promise.resolve('') }
function getPic(songId) { return Promise.resolve('') }
function musicDetail(id) { return Promise.resolve(null) }
function musicInfo(id) { return Promise.resolve(null) }
var pluginInfo = {
info: { id: 'koneko_kw', name: '酷我音乐 - Koneko', version: '0.0.3', description: '酷我音乐聚合音源插件,官方搜索+多API音源,自动测速容灾切换' },
env: [{ key: 'ceru_key', name: '聆澜API Key', description: '聆澜音源API密钥,留空则跳过聆澜音源' }],
ext: [],
quality: [
{ name: '标准音质', ui: '标', id: 'standard' },
{ name: '高品音质', ui: 'HQ', id: 'exhigh' },
{ name: '无损音质', ui: 'SQ', id: 'lossless' },
{ name: 'Hi-Res', ui: 'HR', id: 'hires' }
],
supportFunc: []
}
module.exports = {
musicSearch: musicSearch,
tipSearch: tipSearch,
leaderboard: leaderboard,
songList: songList,
hotSearch: hotSearch,
singer: singer,
album: album,
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo,
pluginInfo: pluginInfo
}
Koneko_网易云音乐_v0.0.3
'use strict'
var https = require('https')
var http = require('http')
var crypto = require('crypto')
var env = global.env || {}
var CERU_KEY = env.ceru_key || ''
var WY_COOKIE = env.cookie || ''
var HEADERS_COMMON = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
function mapBr(q) {
if (q === '128k' || q === 'standard') return '128000'
if (q === '320k' || q === 'exhigh') return '320000'
if (q === '999k' || q === 'lossless') return '999000'
if (q === 'hires') return '999000'
return '320000'
}
function httpGet(url, headers, timeout) {
return new Promise(function(resolve, reject) {
var mod = url.indexOf('https') === 0 ? https : http
var req = mod.get(url, { headers: headers || {}, timeout: timeout || 8000 }, function(res) {
var data = ''
res.on('data', function(chunk) { data += chunk })
res.on('end', function() {
try { resolve(JSON.parse(data)) } catch(e) { resolve(data) }
})
})
req.on('error', function(err) { reject(err) })
req.on('timeout', function() { req.destroy(); reject(new Error('timeout')) })
})
}
function httpPost(url, body, headers, timeout) {
return new Promise(function(resolve, reject) {
var mod = url.indexOf('https') === 0 ? https : http
var postData = typeof body === 'string' ? body : JSON.stringify(body)
var opts = {
method: 'POST',
headers: {},
timeout: timeout || 10000
}
if (headers) {
for (var k in headers) { opts.headers[k] = headers[k] }
}
opts.headers['Content-Type'] = 'application/x-www-form-urlencoded'
opts.headers['Content-Length'] = Buffer.byteLength(postData)
var req = mod.request(url, opts, function(res) {
var data = ''
res.on('data', function(chunk) { data += chunk })
res.on('end', function() {
try { resolve(JSON.parse(data)) } catch(e) { resolve(data) }
})
})
req.on('error', function(err) { reject(err) })
req.on('timeout', function() { req.destroy(); reject(new Error('timeout')) })
req.write(postData)
req.end()
})
}
var EAPI_KEY = 'e82ckenh8dichen8'
function aesEncryptEcb(text, key) {
var cipher = crypto.createCipheriv('aes-128-ecb', key, '')
cipher.setAutoPadding(true)
var encrypted = cipher.update(text, 'utf8', 'hex')
encrypted += cipher.final('hex')
return encrypted
}
function eapiEncrypt(url, text) {
var message = 'nobody' + url + 'use' + text + 'md5forencrypt'
var digest = crypto.createHash('md5').update(message).digest('hex')
var data = url + '-36cd479b6b5-' + text + '-36cd479b6b5-' + digest
return aesEncryptEcb(data, EAPI_KEY)
}
function eapiRequest(url, params) {
var text = JSON.stringify(params)
var enc = eapiEncrypt(url, text)
var body = 'params=' + encodeURIComponent(enc)
var headers = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; SM-G960U) AppleWebKit/537.36',
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': 'https://music.163.com',
'Cookie': WY_COOKIE || 'os=android; appver=8.9.0;'
}
return httpPost('https://music.163.com' + url, body, headers, 15000)
}
var WEAPI_IV = '0102030405060708'
var WEAPI_PRESET_KEY = '0CoJUm6Qyw8W8jud'
var WEAPI_RSA_KEY = '010001'
var WEAPI_RSA_MODULUS = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
var WEAPI_BASE62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
function aesEncryptCbc(text, key, iv) {
var cipher = crypto.createCipheriv('aes-128-cbc', key, iv)
cipher.setAutoPadding(true)
var encrypted = cipher.update(text, 'utf8', 'base64')
encrypted += cipher.final('base64')
return encrypted
}
function rsaEncrypt(text, exponent, modulus) {
var reversed = text.split('').reverse().join('')
var bigInt = BigInt('0x' + Buffer.from(reversed).toString('hex'))
var exp = BigInt('0x' + exponent)
var mod = BigInt('0x' + modulus)
var result = bigInt ** exp % mod
var hex = result.toString(16).padStart(256, '0')
return hex
}
function weapiEncrypt(text) {
var secretKey = ''
for (var i = 0; i < 16; i++) {
secretKey += WEAPI_BASE62.charAt(Math.floor(Math.random() * 62))
}
var firstEnc = aesEncryptCbc(text, WEAPI_PRESET_KEY, WEAPI_IV)
var secondEnc = aesEncryptCbc(firstEnc, secretKey, WEAPI_IV)
var rsa = rsaEncrypt(secretKey, WEAPI_RSA_KEY, WEAPI_RSA_MODULUS)
return {
params: secondEnc,
encSecKey: rsa
}
}
function weapiRequest(url, params) {
var text = JSON.stringify(params)
var enc = weapiEncrypt(text)
var body = 'params=' + encodeURIComponent(enc.params) + '&encSecKey=' + encodeURIComponent(enc.encSecKey)
var headers = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; SM-G960U) AppleWebKit/537.36',
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': 'https://music.163.com',
'Cookie': WY_COOKIE || 'os=android; appver=8.9.0;'
}
return httpPost('https://music.163.com' + url, body, headers, 15000)
}
function formatPlayTime(ms) {
if (!ms || isNaN(ms)) return '--/--'
var totalSec = Math.floor(ms / 1000)
var m = Math.floor(totalSec / 60)
var s = totalSec % 60
return m + ':' + (s < 10 ? '0' : '') + s
}
function wySearch(keyword, page, limit) {
if (!page) page = 1
if (!limit) limit = 30
var offset = (page - 1) * limit
var url = 'https://music.163.com/api/search/get/web?csrf_token=&hlposttag=&s=' + encodeURIComponent(keyword) + '&type=1&offset=' + offset + '&total=true&limit=' + limit
return httpGet(url, {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://music.163.com',
'Cookie': WY_COOKIE || 'os=pc; appver=2.10.0;'
}, 10000).then(function(res) {
if (!res || !res.result || !res.result.songs) {
return { list: [], allPage: 1, limit: limit, total: 0, source: 'wy' }
}
var list = []
for (var i = 0; i < res.result.songs.length; i++) {
var s = res.result.songs[i]
var artists = []
if (s.artists) {
for (var j = 0; j < s.artists.length; j++) {
artists.push(s.artists[j].name)
}
}
var album = s.album ? s.album.name : ''
var albumId = s.album ? String(s.album.id) : ''
var pic = ''
if (s.album && s.album.picId) {
var picIdStr = String(s.album.picId)
var picIdB64 = Buffer.from(picIdStr).toString('base64').replace(/=/g, '')
pic = 'https://p2.music.126.net/' + picIdB64 + '/' + picIdStr + '.jpg'
}
list.push({
id: s.id ? String(s.id) : '',
name: s.name || '',
artists: artists.join('、'),
albumName: album,
albumId: albumId,
source: 'wy',
pic: pic,
mPic: pic,
sPic: pic,
interval: formatPlayTime(s.duration),
qualities: {}
})
}
var total = res.result.songCount || 0
var allPage = Math.ceil(total / limit)
return { list: list, allPage: allPage, limit: limit, total: total, source: 'wy' }
}).catch(function(e) {
return { list: [], allPage: 1, limit: 30, total: 0, source: 'wy' }
})
}
function wyTipSearch(keyword) {
var url = 'https://music.163.com/api/search/get/web?csrf_token=&hlposttag=&s=' + encodeURIComponent(keyword) + '&type=1&offset=0&total=true&limit=10'
return httpGet(url, {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://music.163.com',
'Cookie': WY_COOKIE || 'os=pc; appver=2.10.0;'
}, 10000).then(function(res) {
if (!res || !res.result || !res.result.songs) return []
var tips = []
for (var i = 0; i < res.result.songs.length; i++) {
var s = res.result.songs[i]
tips.push(s.name + ' - ' + (s.artists && s.artists[0] ? s.artists[0].name : '未知'))
}
return tips
}).catch(function(e) {
return []
})
}
function wyHotSearch() {
return weapiRequest('/weapi/search/hot', { type: 1111 }).then(function(res) {
if (!res || !res.result || !res.result.hots) return []
var hots = []
for (var i = 0; i < res.result.hots.length; i++) {
hots.push(res.result.hots[i].first)
}
return hots
}).catch(function(e) {
return []
})
}
function wyGetUrl(songId, quality) {
var br = mapBr(quality)
var params = {
ids: '[' + songId + ']',
br: br,
csrf_token: ''
}
return weapiRequest('/weapi/song/enhance/player/url', params).then(function(res) {
if (res && res.data && res.data[0] && res.data[0].url) {
return { url: res.data[0].url, platform: 'wy' }
}
throw new Error('no url')
})
}
function wyUserPlaylist() {
var url = env.playlist_url || ''
var uid = ''
var match = url.match(/id=(\d+)/)
if (match) uid = match[1]
if (!uid) {
match = url.match(/(\d+)/)
if (match) uid = match[1]
}
if (!uid) return Promise.resolve({ playlists: [] })
var params = { uid: uid, limit: 1000, offset: 0 }
return weapiRequest('/weapi/user/playlist', params).then(function(res) {
if (!res || !res.playlist) return { playlists: [] }
var list = []
for (var i = 0; i < res.playlist.length; i++) {
var p = res.playlist[i]
list.push({
id: String(p.id),
name: p.name,
picUrl: p.coverImgUrl,
trackCount: p.trackCount,
description: p.description || ''
})
}
return { playlists: list }
}).catch(function(e) {
return { playlists: [] }
})
}
function wyDailyRecommend() {
return weapiRequest('/weapi/v1/discovery/recommend/songs', {}).then(function(res) {
if (!res || !res.data || !res.data.dailySongs) return { songs: [] }
var songs = []
for (var i = 0; i < res.data.dailySongs.length; i++) {
var s = res.data.dailySongs[i]
var artists = []
if (s.ar) {
for (var j = 0; j < s.ar.length; j++) {
artists.push(s.ar[j].name)
}
}
songs.push({
id: String(s.id),
name: s.name,
artist: artists.join('/'),
album: s.al ? s.al.name : '',
picUrl: s.al ? s.al.picUrl : '',
duration: s.dt || 0,
platform: 'wy'
})
}
return { songs: songs }
}).catch(function(e) {
return { songs: [] }
})
}
function wyPersonalFm() {
return weapiRequest('/weapi/v1/radio/get', {}).then(function(res) {
if (!res || !res.data) return { songs: [] }
var songs = []
for (var i = 0; i < res.data.length; i++) {
var s = res.data[i]
var artists = []
if (s.ar) {
for (var j = 0; j < s.ar.length; j++) {
artists.push(s.ar[j].name)
}
}
songs.push({
id: String(s.id),
name: s.name,
artist: artists.join('/'),
album: s.al ? s.al.name : '',
picUrl: s.al ? s.al.picUrl : '',
duration: s.dt || 0,
platform: 'wy'
})
}
return { songs: songs }
}).catch(function(e) {
return { songs: [] }
})
}
function wyMyLikedSongs() {
return weapiRequest('/weapi/song/like/get', {}).then(function(res) {
if (!res || !res.data || !res.data.checkPoint) return { songs: [] }
var ids = res.data.checkPoint
if (!ids || ids.length === 0) return { songs: [] }
var idStr = ''
for (var i = 0; i < ids.length; i++) {
if (i > 0) idStr += ','
idStr += ids[i]
}
var params = { ids: '[' + idStr + ']', csrf_token: '' }
return weapiRequest('/weapi/v3/song/detail', params).then(function(detail) {
if (!detail || !detail.songs) return { songs: [] }
var songs = []
for (var i = 0; i < detail.songs.length; i++) {
var s = detail.songs[i]
var artists = []
if (s.ar) {
for (var j = 0; j < s.ar.length; j++) {
artists.push(s.ar[j].name)
}
}
songs.push({
id: String(s.id),
name: s.name,
artist: artists.join('/'),
album: s.al ? s.al.name : '',
picUrl: s.al ? s.al.picUrl : '',
duration: s.dt || 0,
platform: 'wy'
})
}
return { songs: songs }
})
}).catch(function(e) {
return { songs: [] }
})
}
function ceruGetUrl(songId, quality) {
if (!CERU_KEY) return Promise.reject(new Error('no key'))
var br = mapBr(quality)
var url = 'https://ceruapi.lol/meting-api-0/?server=netease&type=url&id=' + encodeURIComponent(songId) + '&auth=' + encodeURIComponent(CERU_KEY) + '&br=' + br
return httpGet(url, HEADERS_COMMON, 10000).then(function(res) {
if (res && res.url) return { url: res.url, platform: 'wy' }
throw new Error('no url')
})
}
function bbGetUrl(songId, quality) {
var br = mapBr(quality)
var url = 'https://api.bbdcz.cn/music/netease/url?id=' + encodeURIComponent(songId) + '&br=' + br
return httpGet(url, HEADERS_COMMON, 10000).then(function(res) {
if (res && res.data && res.data.url) return { url: res.data.url, platform: 'wy' }
if (res && res.url) return { url: res.url, platform: 'wy' }
throw new Error('no url')
})
}
function lxGetUrl(songId, quality) {
var br = mapBr(quality)
var url = 'https://lxmusicapi.onrender.com/url/wy/' + encodeURIComponent(songId) + '/' + br
return httpGet(url, HEADERS_COMMON, 15000).then(function(res) {
if (res && res.url) return { url: res.url, platform: 'wy' }
throw new Error('no url')
})
}
function ymcGetUrl(songId, quality) {
var br = mapBr(quality)
var url = 'https://api.ymusic.icu/netease/song?id=' + encodeURIComponent(songId) + '&quality=' + br
return httpGet(url, HEADERS_COMMON, 10000).then(function(res) {
if (res && res.data && res.data.url) return { url: res.data.url, platform: 'wy' }
if (res && res.url) return { url: res.url, platform: 'wy' }
throw new Error('no url')
})
}
function unmsGetUrl(songId, quality) {
var br = mapBr(quality)
var url = 'https://unms.zeabur.app/netease/url?id=' + encodeURIComponent(songId) + '&br=' + br
return httpGet(url, HEADERS_COMMON, 10000).then(function(res) {
if (res && res.url) return { url: res.url, platform: 'wy' }
throw new Error('no url')
})
}
function getUrl(songId, quality) {
var apis = []
apis.push({ name: 'ceru', fn: ceruGetUrl })
apis.push({ name: 'bb', fn: bbGetUrl })
apis.push({ name: 'lx', fn: lxGetUrl })
apis.push({ name: 'ymc', fn: ymcGetUrl })
apis.push({ name: 'unms', fn: unmsGetUrl })
apis.push({ name: 'official', fn: wyGetUrl })
var promises = []
for (var i = 0; i < apis.length; i++) {
var api = apis[i]
promises.push(
api.fn(songId, quality).then(function(result) {
return { status: 'fulfilled', value: result }
}).catch(function(err) {
return { status: 'rejected', reason: err }
})
)
}
return new Promise(function(resolve, reject) {
Promise.all(promises).then(function(results) {
for (var i = 0; i < results.length; i++) {
if (results[i].status === 'fulfilled') {
resolve(results[i].value)
return
}
}
reject(new Error('all apis failed'))
}).catch(function(e) {
reject(e)
})
})
}
var leaderboard = { getList: function() { return Promise.resolve([]) } }
var songList = {
getListDetail: function(id, page, limit) {
return Promise.resolve({ list: [], page: page || 1, limit: limit || 30, total: 0, source: 'wy', info: { name: '', img: '', desc: '', author: '' } })
}
}
var singer = { getInfo: function(id) { return Promise.resolve(null) } }
var album = {
getListDetail: function(id) {
return Promise.resolve({ list: [], page: 1, limit: 1000, total: 0, source: 'wy', info: { name: '', img: '', desc: '', author: '' } })
},
search: function(str, page, limit) { return Promise.resolve([]) }
}
function getLyric(id) { return Promise.resolve('') }
function getPic(songId) { return Promise.resolve('') }
function musicDetail(id) { return Promise.resolve(null) }
function musicInfo(id) { return Promise.resolve(null) }
var pluginInfo = {
info: { id: 'koneko_wy', name: '网易云音乐 - Koneko', version: '0.0.3', description: '网易云音乐聚合音源插件,官方搜索+多API音源,自动测速容灾切换' },
env: [
{ key: 'ceru_key', name: '聆澜API Key', description: '聆澜音源API密钥,留空则跳过聆澜音源' },
{ key: 'playlist_url', name: '个人主页链接', description: '网易云音乐个人主页链接,用于获取个人歌单' },
{ key: 'cookie', name: 'Cookie', description: '网易云音乐Cookie,用于搜索/每日推荐/私人FM/我喜欢的音乐' }
],
ext: [
{ name: '个人歌单', description: '通过分享链接获取个人歌单', entry: 'plugin.userPlaylist()', type: 'playlists' },
{ name: '每日推荐', description: '获取每日推荐歌曲', entry: 'plugin.dailyRecommend()', type: 'songs' },
{ name: '私人FM', description: '获取私人FM歌曲', entry: 'plugin.personalFm()', type: 'songs' },
{ name: '我喜欢的音乐', description: '获取我喜欢的音乐列表', entry: 'plugin.myLikedSongs()', type: 'songs' }
],
quality: [
{ name: '标准音质', ui: '标', id: 'standard' },
{ name: '高品音质', ui: 'HQ', id: 'exhigh' },
{ name: '无损音质', ui: 'SQ', id: 'lossless' },
{ name: 'Hi-Res', ui: 'HR', id: 'hires' },
{ name: '高清环绕声', ui: 'DB', id: 'jyeffect' },
{ name: '沉浸环绕声', ui: 'SK', id: 'sky' },
{ name: '超清母带', ui: 'MT', id: 'jymaster' }
],
supportFunc: []
}
var musicSearch = {
search: function(keyword, page, limit) {
return wySearch(keyword, page, limit).catch(function(e) {
return { list: [], allPage: 1, limit: 30, total: 0, source: 'wy' }
})
},
tipSearch: function(keyword) {
return wyTipSearch(keyword).catch(function(e) {
return []
})
},
hotSearch: function() {
return wyHotSearch().catch(function(e) {
return []
})
}
}
var tipSearch = {
getList: function(str) {
return wyTipSearch(str).catch(function(e) {
return []
})
}
}
var hotSearch = {
getList: function() {
return wyHotSearch().catch(function(e) {
return []
})
}
}
module.exports = {
musicSearch: musicSearch,
tipSearch: tipSearch,
leaderboard: leaderboard,
songList: songList,
hotSearch: hotSearch,
singer: singer,
album: album,
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo,
pluginInfo: pluginInfo,
userPlaylist: function() {
return wyUserPlaylist().catch(function(e) {
return { playlists: [] }
})
},
dailyRecommend: function() {
return wyDailyRecommend().catch(function(e) {
return { songs: [] }
})
},
personalFm: function() {
return wyPersonalFm().catch(function(e) {
return { songs: [] }
})
},
myLikedSongs: function() {
return wyMyLikedSongs().catch(function(e) {
return { songs: [] }
})
}
}
Koneko_咪咕音乐_v0.0.3
'use strict'
var https = require('https')
var http = require('http')
var crypto = require('crypto')
var env = global.env || {}
var CERU_KEY = env.ceru_key || ''
var HEADERS_COMMON = {
'User-Agent': 'Mozilla/5.0 (Linux; U; Android 11.0.0; zh-cn; MI 11 Build/OPR1.170623.032) AppleWebKit/534.30'
}
function mapBr(q) {
if (q === '128k' || q === 'standard') return '128k'
if (q === '320k' || q === 'exhigh') return '320k'
return '999k'
}
function httpGet(url, headers, timeout) {
return new Promise(function(resolve, reject) {
var mod = url.indexOf('https') === 0 ? https : http
var req = mod.get(url, { headers: headers || {}, timeout: timeout || 8000 }, function(res) {
var data = ''
res.on('data', function(chunk) { data += chunk })
res.on('end', function() {
try { resolve(JSON.parse(data)) } catch(e) { resolve(data) }
})
})
req.on('error', function(err) { reject(err) })
req.on('timeout', function() { req.destroy(); reject(new Error('timeout')) })
})
}
function formatPlayTime(seconds) {
if (!seconds || isNaN(seconds)) return '--/--'
var m = Math.floor(seconds / 60)
var s = seconds % 60
return m + ':' + (s < 10 ? '0' : '') + s
}
function formatSize(bytes) {
if (!bytes) return ''
var n = parseFloat(bytes)
if (isNaN(n) || n < 0) return ''
return (n / (1024 * 1024)).toFixed(2) + 'MB'
}
function createSignature(time, str) {
var deviceId = '963B7AA0D21511ED807EE5846EC87D20'
var signatureMd5 = '6cdc72a439cef99a3418d2a78aa28c73'
var sign = crypto.createHash('md5').update(str + signatureMd5 + 'yyapp2d16148780a1dcc7408e06336b98cfd50' + deviceId + time).digest('hex')
return { sign: sign, deviceId: deviceId }
}
var musicSearch = {
limit: 30,
total: 0,
page: 0,
allPage: 1,
search: function(str, page, limit, retryNum) {
var self = this
if (retryNum === undefined) retryNum = 0
if (++retryNum > 3) return Promise.reject(new Error('搜索失败'))
if (!page) page = 1
if (limit == null) limit = this.limit
var time = Date.now().toString()
var signData = createSignature(time, str)
var searchSwitch = encodeURIComponent('{"song":1,"album":0,"singer":0,"tagSong":1,"mvSong":0,"bestShow":1,"songlist":0,"lyricSong":0}')
return httpGet(
'https://jadeite.migu.cn/music_search/v3/search/searchAll?isCorrect=0&isCopyright=1&searchSwitch=' + searchSwitch + '&pageSize=' + limit + '&text=' + encodeURIComponent(str) + '&pageNo=' + page + '&sort=0&sid=USS',
{
'User-Agent': HEADERS_COMMON['User-Agent'],
uiVersion: 'A_music_3.6.1',
deviceId: signData.deviceId,
timestamp: time,
sign: signData.sign,
channel: '0146921'
}
).then(function(result) {
if (!result || typeof result !== 'object' || result.code !== '000000' || !result.songResultData) {
return self.search(str, page, limit, retryNum)
}
var songData = result.songResultData || { resultList: [], totalCount: 0 }
var ids = {}
var list = []
var resultList = songData.resultList || []
for (var i = 0; i < resultList.length; i++) {
var item = resultList[i]
if (!Array.isArray(item)) continue
for (var j = 0; j < item.length; j++) {
var data = item[j]
if (!data.songId || !data.copyrightId || ids[data.copyrightId]) continue
ids[data.copyrightId] = true
var qualities = {}
if (data.audioFormats) {
for (var k = 0; k < data.audioFormats.length; k++) {
var type = data.audioFormats[k]
var size = formatSize(type.asize || type.isize)
if (type.formatType === 'PQ') qualities.standard = size
else if (type.formatType === 'HQ') qualities.exhigh = size
else if (type.formatType === 'SQ') qualities.lossless = size
else if (type.formatType === 'ZQ24') qualities.hires = size
}
}
var img = data.img3 || data.img2 || data.img1 || ''
if (img && img.indexOf('http') !== 0) img = 'https://d.musicapp.migu.cn' + img
var artists = ''
if (data.singerList) {
var names = []
for (var k = 0; k < data.singerList.length; k++) {
if (data.singerList[k].name) names.push(data.singerList[k].name)
}
artists = names.join('、')
}
list.push({
artists: artists,
name: data.name || '',
albumName: data.album || '',
albumId: data.albumId || '',
id: data.contentId || '',
source: 'mg',
interval: formatPlayTime(data.duration),
pic: img,
mPic: img,
sPic: img,
qualities: qualities
})
}
}
self.total = parseInt(songData.totalCount) || 0
self.page = page
self.allPage = Math.ceil(self.total / limit)
return { list: list, allPage: self.allPage, limit: limit, total: self.total, source: 'mg' }
}).catch(function(e) {
return { list: [], allPage: 1, limit: 30, total: 0, source: 'mg' }
})
}
}
var tipSearch = { getList: function(str) { return Promise.resolve([]) } }
var hotSearch = { getList: function() { return Promise.resolve([]) } }
function buildApis(songId, q) {
var br = mapBr(q)
var apis = []
if (CERU_KEY) {
apis.push({
name: '聆澜',
url: 'https://source.shiqianjiang.cn/api/music/url?source=mg&songId=' + songId + '&quality=' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-API-Key': CERU_KEY },
extract: function(res) { return res && res.code === 200 && res.url ? res.url : null }
})
}
apis.push(
{
name: 'HUIBQ',
url: 'https://lxmusicapi.onrender.com/url/mg/' + songId + '/' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-Request-Key': 'share-v3' },
extract: function(res) { return res && res.code === 0 && res.url ? res.url : null }
},
{
name: '星海',
url: 'https://music-api.gdstudio.xyz/api.php?types=url&source=migu&id=' + songId + '&br=' + br,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '念心',
url: 'https://music.nxinxz.com/kgqq/mg.php?id=' + songId + '&level=' + q + '&type=mp3',
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '长青',
url: 'https://music.haitangw.cc/musicapi/mg.php?id=' + songId + '&level=' + q + '&type=mp3',
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: '星海备',
url: 'https://music-dl.sayqz.com/api/?source=migu&id=' + songId + '&type=url&br=' + q,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: 'fish',
url: 'https://m-api.ceseet.me/url/mg/' + songId + '/' + q,
headers: HEADERS_COMMON,
extract: function(res) {
if (res && res.url) return res.url
if (res && res.data && res.data.url) return res.data.url
return null
}
},
{
name: 'HYW',
url: 'https://music.bxa241d4.shop/api/music/url?source=mg&songId=' + songId + '&quality=' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-Card-Key': 'TF-VSS0-8Y73-U1AW-GEXJ' },
extract: function(res) {
if (res && res.url) return res.url
if (res && res.code === 200 && res.data && res.data.url) return res.data.url
return null
}
}
)
return apis
}
function getUrl(songId, quality) {
var q = quality || '320k'
var apis = buildApis(songId, q)
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) {
console.log('[Koneko 咪咕音乐] ' + api.name + ' 成功')
return { name: api.name, url: url }
}
throw new Error(api.name + ' 无有效URL')
}).catch(function(err) {
console.error('[Koneko 咪咕音乐] ' + api.name + ' 失败: ' + err.message)
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
}
console.error('[Koneko 咪咕音乐] 所有API均失败')
return ''
})
}
var leaderboard = { getList: function() { return Promise.resolve([]) } }
var songList = {
getListDetail: function(id, page, limit) {
return Promise.resolve({ list: [], page: page || 1, limit: limit || 30, total: 0, source: 'mg', info: { name: '', img: '', desc: '', author: '' } })
}
}
var singer = { getInfo: function(id) { return Promise.resolve(null) } }
var album = {
getListDetail: function(id) {
return Promise.resolve({ list: [], page: 1, limit: 1000, total: 0, source: 'mg', info: { name: '', img: '', desc: '', author: '' } })
},
search: function(str, page, limit) { return Promise.resolve([]) }
}
function getLyric(id) { return Promise.resolve('') }
function getPic(songId) { return Promise.resolve('') }
function musicDetail(id) { return Promise.resolve(null) }
function musicInfo(id) { return Promise.resolve(null) }
var pluginInfo = {
info: { id: 'koneko_mg', name: '咪咕音乐 - Koneko', version: '0.0.3', description: '咪咕音乐聚合音源插件,官方搜索+多API音源,自动测速容灾切换' },
env: [{ key: 'ceru_key', name: '聆澜API Key', description: '聆澜音源API密钥,留空则跳过聆澜音源' }],
ext: [],
quality: [
{ name: '标准音质', ui: '标', id: 'standard' },
{ name: '高品音质', ui: 'HQ', id: 'exhigh' },
{ name: '无损音质', ui: 'SQ', id: 'lossless' },
{ name: 'Hi-Res', ui: 'HR', id: 'hires' }
],
supportFunc: []
}
module.exports = {
musicSearch: musicSearch,
tipSearch: tipSearch,
leaderboard: leaderboard,
songList: songList,
hotSearch: hotSearch,
singer: singer,
album: album,
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo,
pluginInfo: pluginInfo
}
Koneko_GIT音源_v0.0.3
'use strict'
var https = require('https')
var http = require('http')
var env = global.env || {}
var CERU_KEY = env.ceru_key || ''
var HEADERS_COMMON = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
function httpGet(url, headers, timeout) {
return new Promise(function(resolve, reject) {
var mod = url.indexOf('https') === 0 ? https : http
var req = mod.get(url, { headers: headers || {}, timeout: timeout || 8000 }, function(res) {
var data = ''
res.on('data', function(chunk) { data += chunk })
res.on('end', function() {
try { resolve(JSON.parse(data)) } catch(e) { resolve(data) }
})
})
req.on('error', function(err) { reject(err) })
req.on('timeout', function() { req.destroy(); reject(new Error('timeout')) })
})
}
function buildApis(songId, q) {
var apis = []
if (CERU_KEY) {
apis.push({
name: '聆澜',
url: 'https://source.shiqianjiang.cn/api/music/url?source=git&songId=' + songId + '&quality=' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-API-Key': CERU_KEY },
extract: function(res) { return res && res.code === 200 && res.url ? res.url : null }
})
}
apis.push({
name: 'HUIBQ',
url: 'https://lxmusicapi.onrender.com/url/git/' + songId + '/' + q,
headers: { 'User-Agent': HEADERS_COMMON['User-Agent'], 'X-Request-Key': 'share-v3' },
extract: function(res) { return res && res.code === 0 && res.url ? res.url : null }
})
return apis
}
function getUrl(songId, quality) {
var q = quality || '320k'
var apis = buildApis(songId, q)
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) {
console.log('[Koneko GIT音源] ' + api.name + ' 成功')
return { name: api.name, url: url }
}
throw new Error(api.name + ' 无有效URL')
}).catch(function(err) {
console.error('[Koneko GIT音源] ' + api.name + ' 失败: ' + err.message)
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
}
console.error('[Koneko GIT音源] 所有API均失败')
return ''
})
}
var musicSearch = {
search: function(str, page, limit) {
return Promise.resolve({ list: [], total: 0, page: page || 1, limit: limit || 30, allPage: 0, source: 'koneko_git' })
}
}
var tipSearch = { getList: function(str) { return Promise.resolve([]) } }
var leaderboard = { getList: function() { return Promise.resolve([]) } }
var songList = {
getListDetail: function(id, page, limit) {
return Promise.resolve({ list: [], page: page || 1, limit: limit || 30, total: 0, source: 'koneko_git', info: { name: '', img: '', desc: '', author: '' } })
}
}
var hotSearch = { getList: function() { return Promise.resolve([]) } }
var singer = { getInfo: function(id) { return Promise.resolve(null) } }
var album = {
getListDetail: function(id) {
return Promise.resolve({ list: [], page: 1, limit: 1000, total: 0, source: 'koneko_git', info: { name: '', img: '', desc: '', author: '' } })
},
search: function(str, page, limit) { return Promise.resolve([]) }
}
function getLyric(id) { return Promise.resolve('') }
function getPic(songId) { return Promise.resolve('') }
function musicDetail(id) { return Promise.resolve(null) }
function musicInfo(id) { return Promise.resolve(null) }
var pluginInfo = {
info: { id: 'koneko_git', name: 'GIT音源 - Koneko', version: '0.0.3', description: 'GIT音源聚合插件,聚合2+API音源,自动测速容灾切换' },
env: [{ key: 'ceru_key', name: '聆澜API Key', description: '聆澜音源API密钥,留空则跳过聆澜音源' }],
ext: [],
quality: [
{ name: '标准音质', ui: '标', id: 'standard' },
{ name: '高品音质', ui: 'HQ', id: 'exhigh' },
{ name: '无损音质', ui: 'SQ', id: 'lossless' },
{ name: 'Hi-Res', ui: 'HR', id: 'hires' }
],
supportFunc: []
}
module.exports = {
musicSearch: musicSearch,
tipSearch: tipSearch,
leaderboard: leaderboard,
songList: songList,
hotSearch: hotSearch,
singer: singer,
album: album,
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo,
pluginInfo: pluginInfo
}