Add Koneko Ceru plugins v0.0.3 (6 platforms, unified Ceru playback API)

This commit is contained in:
TRAE Bot
2026-07-05 09:10:16 +00:00
parent b19eac46cc
commit 494acf1db1
6 changed files with 1462 additions and 0 deletions

View File

@@ -0,0 +1,212 @@
var https = require('https');
var http = require('http');
var crypto = require('crypto');
var querystring = require('querystring');
// ===== 通用HTTP请求 =====
function doRequest(protocol, options, postData) {
return new Promise(function(resolve, reject) {
var client = protocol === 'https' ? https : http;
var req = client.request(options, function(res) {
var chunks = [];
res.on('data', function(chunk) { chunks.push(chunk); });
res.on('end', function() {
var buf = Buffer.concat(chunks);
var body;
try {
body = JSON.parse(buf.toString('utf8'));
} catch (e) {
body = buf.toString('utf8');
}
resolve({ body: body, statusCode: res.statusCode, headers: res.headers });
});
});
req.on('error', function(err) { reject(err); });
req.setTimeout(15000, function() { req.destroy(); reject(new Error('timeout')); });
if (postData) req.write(postData);
req.end();
});
}
function requestUrl(urlStr, method, headers, body) {
var parsed = new URL(urlStr);
var protocol = parsed.protocol.replace(':', '');
var options = {
hostname: parsed.hostname,
port: parsed.port || (protocol === 'https' ? 443 : 80),
path: parsed.pathname + parsed.search,
method: method || 'GET',
headers: headers || {}
};
var postData = null;
if (body) {
if (typeof body === 'string') {
postData = Buffer.from(body, 'utf8');
} else if (Buffer.isBuffer(body)) {
postData = body;
} else {
postData = Buffer.from(JSON.stringify(body), 'utf8');
if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json';
}
if (postData) options.headers['Content-Length'] = postData.length;
}
return doRequest(protocol, options, postData);
}
function formatPlayTime(seconds) {
if (!seconds) return '00:00';
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
// ===== Git音源配置 =====
var GITCODE_CONFIG = { owner: 'ikun_0014', repo: 'music', token: 'WzsER9knWNgC_4tjeJCtHKcN' };
function getGitcodeUrl() {
return 'https://api.gitcode.com/api/v5/repos/' + GITCODE_CONFIG.owner + '/' + GITCODE_CONFIG.repo + '/raw/audio_database.json?access_token=' + GITCODE_CONFIG.token;
}
var cachedList = null;
var cacheTime = 0;
var CACHE_TTL = 5 * 60 * 1000;
function fetchAllGitMusic() {
var now = Date.now();
if (cachedList && now - cacheTime < CACHE_TTL) return Promise.resolve(cachedList);
return requestUrl(getGitcodeUrl(), 'GET', { 'User-Agent': 'Mozilla/5.0' }).then(function(res) {
var body = Array.isArray(res.body) ? res.body : [];
var list = [];
for (var i = 0; i < body.length; i++) list.push(formatGitItem(body[i]));
cachedList = list;
cacheTime = now;
return list;
});
}
function formatGitItem(item) {
var types = [];
var _types = {};
var fmt = (item.format || '').toLowerCase();
if (fmt === 'flac') { types.push({ type: 'flac', size: item.filesize || '' }); _types.flac = { size: item.filesize || '' }; }
else if (fmt === 'mp3') { types.push({ type: '320k', size: item.filesize || '' }); _types['320k'] = { size: item.filesize || '' }; }
else { types.push({ type: '128k', size: item.filesize || '' }); _types['128k'] = { size: item.filesize || '' }; }
return {
singer: item.artist || '未知歌手',
name: item.title || item.filename || '',
albumName: item.album || '未知专辑',
albumId: '',
songmid: item.relative_path || '',
source: 'git',
interval: '',
img: item.img || '',
lrc: item.lyrics || null,
types: types, _types: _types, typeUrl: {},
_gitcodeData: item
};
}
// ===== 搜索实现 =====
function musicSearch(str, page, limit) {
return fetchAllGitMusic().then(function(list) {
var regexStr = str.split('').map(function(c) { return c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }).join('.*');
var regex = new RegExp(regexStr, 'i');
var filtered = [];
for (var i = 0; i < list.length; i++) {
var item = list[i];
if (regex.test(item.singer) || regex.test(item.name) || regex.test(item.albumName)) filtered.push(item);
}
var start = (page - 1) * limit;
return { data: filtered.slice(start, start + limit), total: filtered.length };
});
}
function handleSearchResult(result) {
return result.data || [];
}
function search(str, page, limit, retryNum) {
if (retryNum === undefined) retryNum = 0;
if (++retryNum > 3) return Promise.reject(new Error('try max num'));
if (!limit) limit = 30;
return musicSearch(str, page, limit).then(function(result) {
var list = handleSearchResult(result);
return { list: list, allPage: Math.ceil(result.total / limit), limit: limit, total: result.total, source: 'git' };
});
}
// ===== 播放链接 =====
function getUrl(songInfo, type) {
var q = type || '128k';
var songId = songInfo.songmid || '';
if (!songId) return Promise.resolve('');
var ceruKey = process.env.ceru_key || '';
var headers = { 'User-Agent': 'QZMusic/2.0' };
if (ceruKey) headers['X-API-Key'] = ceruKey;
return requestUrl('https://source.shiqianjiang.cn/api/music/url?source=git&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(q), 'GET', headers)
.then(function(res) {
if (res.body && typeof res.body === 'object' && res.body.url) return res.body.url;
if (res.body && typeof res.body === 'object' && res.body.data && res.body.data.url) return res.body.data.url;
if (typeof res.body === 'string' && res.body.indexOf('http') === 0) return res.body;
return '';
}).catch(function() { return ''; });
}
// ===== 封面图 =====
function getPic(songInfo) {
if (songInfo.img) return Promise.resolve(songInfo.img);
return Promise.resolve('');
}
// ===== 歌词 =====
function getLyric(songInfo) {
if (songInfo.lrc) return Promise.resolve(songInfo.lrc);
return Promise.resolve('');
}
// ===== 音乐详情 =====
function musicDetail(songmid) {
return Promise.resolve(null);
}
function musicInfo(songmid) { return musicDetail(songmid); }
// ===== 热搜 =====
function hotSearch() { return Promise.resolve({ source: 'git', list: [] }); }
// ===== 搜索建议 =====
function tipSearch(str) { return Promise.resolve({ order: [], songs: [], artists: [], albums: [], playlists: [] }); }
// ===== 榜单 =====
function leaderboard() { return Promise.resolve([]); }
// ===== 歌单 =====
function songList() { return Promise.resolve({ list: [], total: 0, page: 1, source: 'git' }); }
// ===== 歌手 =====
function singer() { return Promise.resolve({ source: 'git', list: [], total: 0 }); }
// ===== 专辑 =====
function album() { return Promise.resolve({ source: 'git', list: [], total: 0 }); }
// ===== 插件信息 =====
var pluginInfo = {
id: 'koneko_ceru_git',
name: 'GIT音源 - Koneko 聆澜',
version: '0.0.3',
source: 'git',
description: 'GIT音源基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
};
module.exports = {
pluginInfo: pluginInfo,
musicSearch: { search: search },
tipSearch: { search: tipSearch },
hotSearch: { getList: hotSearch },
leaderboard: { getBoards: leaderboard },
songList: { search: songList, getTags: function() { return Promise.resolve({ tags: [], hotTag: [], source: 'git' }); }, getList: songList },
singer: { getInfo: singer, getSongList: singer, getAlbumList: singer },
album: { getInfo: album, getSongList: album },
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo
};

View File

@@ -0,0 +1,309 @@
var https = require('https');
var http = require('http');
var crypto = require('crypto');
var querystring = require('querystring');
// ===== 通用HTTP请求 =====
function doRequest(protocol, options, postData) {
return new Promise(function(resolve, reject) {
var client = protocol === 'https' ? https : http;
var req = client.request(options, function(res) {
var chunks = [];
res.on('data', function(chunk) { chunks.push(chunk); });
res.on('end', function() {
var buf = Buffer.concat(chunks);
var body;
try {
body = JSON.parse(buf.toString('utf8'));
} catch (e) {
body = buf.toString('utf8');
}
resolve({ body: body, statusCode: res.statusCode, headers: res.headers });
});
});
req.on('error', function(err) { reject(err); });
req.setTimeout(15000, function() { req.destroy(); reject(new Error('timeout')); });
if (postData) req.write(postData);
req.end();
});
}
function requestUrl(urlStr, method, headers, body) {
var parsed = new URL(urlStr);
var protocol = parsed.protocol.replace(':', '');
var options = {
hostname: parsed.hostname,
port: parsed.port || (protocol === 'https' ? 443 : 80),
path: parsed.pathname + parsed.search,
method: method || 'GET',
headers: headers || {}
};
var postData = null;
if (body) {
if (typeof body === 'string') {
postData = Buffer.from(body, 'utf8');
} else if (Buffer.isBuffer(body)) {
postData = body;
} else {
postData = Buffer.from(JSON.stringify(body), 'utf8');
if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json';
}
if (postData) options.headers['Content-Length'] = postData.length;
}
return doRequest(protocol, options, postData);
}
// ===== QQ音乐zzcSign =====
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 hashSHA1(text) {
return crypto.createHash('sha1').update(Buffer.from(text, 'utf-8')).digest().toString('hex').toUpperCase();
}
function pickHashByIdx(hash, indexes) {
var out = '';
for (var i = 0; i < indexes.length; i++) out += hash[indexes[i]];
return out;
}
function base64Encode(data) {
var buf;
if (typeof data === 'string') buf = Buffer.from(data);
else if (Array.isArray(data)) buf = Buffer.from(data);
else buf = data;
return buf.toString('base64').replace(/[\\/+=]/g, '');
}
function zzcSign(text) {
var hash = hashSHA1(text);
var part1 = pickHashByIdx(hash, PART_1_INDEXES);
var part2 = pickHashByIdx(hash, PART_2_INDEXES);
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 = base64Encode(part3).replace(/[\\/+=]/g, '');
return ('zzc' + part1 + b64Part + part2).toLowerCase();
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getSearchId() {
var e = randomInt(1, 20);
var t = Number(e * Number('18014398509481984').toFixed());
var n = randomInt(0, 4194304) * 4294967296;
var a = Date.now();
var r = Math.round(a * 1000) % (24 * 60 * 60 * 1000);
return String(t + n + r);
}
function signRequest(data) {
var sign = zzcSign(JSON.stringify(data));
return requestUrl('https://u.y.qq.com/cgi-bin/musics.fcg?sign=' + encodeURIComponent(sign), 'POST', {
'User-Agent': 'QQMusic 14090508(android 12)',
'Content-Type': 'application/json'
}, JSON.stringify(data));
}
function formatPlayTime(seconds) {
if (!seconds) return '00:00';
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
// ===== 搜索实现 =====
function musicSearch(str, page, limit) {
var body = {
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: getSearchId(), 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(body).then(function(res) {
if (res.body && res.body.code === 0 && res.body.req && res.body.req.code === 0) return res.body.req.data;
throw new Error('tx search failed');
});
}
function handleSearchResult(rawList) {
var list = [];
for (var i = 0; i < rawList.length; i++) {
var item = rawList[i];
if (!item.file || !item.file.media_mid) continue;
var types = [];
var _types = {};
var file = item.file;
if (file.size_128mp3 !== 0) { types.push({ type: '128k', size: '' }); _types['128k'] = { size: '' }; }
if (file.size_320mp3 !== 0) { types.push({ type: '320k', size: '' }); _types['320k'] = { size: '' }; }
if (file.size_flac !== 0) { types.push({ type: 'flac', size: '' }); _types.flac = { size: '' }; }
if (file.size_hires !== 0) { types.push({ type: 'hires', size: '' }); _types.hires = { size: '' }; }
var albumId = '';
var albumName = '';
if (item.album) { albumName = item.album.name; albumId = item.album.mid; }
var singerName = '';
if (item.singer && item.singer.length) {
var ns = [];
for (var j = 0; j < item.singer.length; j++) ns.push(item.singer[j].name);
singerName = ns.join('、');
}
list.push({
singer: singerName,
name: item.name + (item.title_extra || ''),
albumName: albumName,
albumId: albumId,
source: 'tx',
interval: formatPlayTime(item.interval),
songmid: item.mid,
songId: item.id,
img: albumId ? 'https://y.gtimg.cn/music/photo_new/T002R500x500M000' + albumId + '.jpg' : (item.singer && item.singer.length ? 'https://y.gtimg.cn/music/photo_new/T001R500x500M000' + item.singer[0].mid + '.jpg' : ''),
lrc: null,
types: types, _types: _types, typeUrl: {}
});
}
return list;
}
function search(str, page, limit, retryNum) {
if (retryNum === undefined) retryNum = 0;
if (++retryNum > 3) return Promise.reject(new Error('try max num'));
if (!limit) limit = 30;
return musicSearch(str, page, limit).then(function(data) {
if (!data || !data.body || !data.body.item_song) return search(str, page, limit, retryNum);
var list = handleSearchResult(data.body.item_song);
var meta = data.meta || {};
var total = meta.estimate_sum || 0;
return { list: list, allPage: Math.ceil(total / limit), limit: limit, total: total, source: 'tx' };
});
}
// ===== 播放链接 =====
function getUrl(songInfo, type) {
var q = type || '128k';
var songId = songInfo.songmid || songInfo.id || '';
var ceruKey = process.env.ceru_key || '';
var headers = { 'User-Agent': 'QZMusic/2.0' };
if (ceruKey) headers['X-API-Key'] = ceruKey;
return requestUrl('https://source.shiqianjiang.cn/api/music/url?source=tx&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(q), 'GET', headers)
.then(function(res) {
if (res.body && typeof res.body === 'object' && res.body.url) return res.body.url;
if (res.body && typeof res.body === 'object' && res.body.data && res.body.data.url) return res.body.data.url;
if (typeof res.body === 'string' && res.body.indexOf('http') === 0) return res.body;
return '';
}).catch(function() { return ''; });
}
// ===== 封面图 =====
function getPic(songInfo) {
if (songInfo.img) return Promise.resolve(songInfo.img);
var albumId = songInfo.albumId || '';
if (albumId) return Promise.resolve('https://y.gtimg.cn/music/photo_new/T002R500x500M000' + albumId + '.jpg');
return Promise.resolve('');
}
// ===== 歌词 =====
function getLyric(songInfo) {
var songmid = songInfo.songmid || '';
return requestUrl('https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?format=json&nobase64=1&songmid=' + encodeURIComponent(songmid), 'GET', {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)',
'Referer': 'https://y.qq.com/portal/player.html'
}).then(function(res) {
if (typeof res.body === 'string') {
try { var obj = JSON.parse(res.body.replace(/^MusicJsonCallback\(/, '').replace(/\)$/, '')); return obj.lyric || ''; } catch(e) {}
}
if (res.body && res.body.lyric) return res.body.lyric;
return '';
}).catch(function() { return ''; });
}
// ===== 音乐详情 =====
function musicDetail(songmid) {
var data = {
comm: { ct: '11', cv: '14090508', v: '14090508', tmeAppID: 'qqmusic' },
req: { module: 'music.pf_song_detail_svr', method: 'get_song_detail_yqq', param: { song_mid: songmid } }
};
return signRequest(data).then(function(res) {
if (res.body && res.body.req && res.body.req.data && res.body.req.data.info && res.body.req.data.info.track_info) {
var t = res.body.req.data.info.track_info;
var albumId = t.album ? t.album.mid : '';
var singerName = '';
if (t.singer && t.singer.length) { var ns = []; for (var j = 0; j < t.singer.length; j++) ns.push(t.singer[j].name); singerName = ns.join('、'); }
return {
singer: singerName, name: t.name,
albumName: t.album ? t.album.name : '', albumId: albumId,
source: 'tx', interval: formatPlayTime(t.interval),
songmid: t.mid, songId: t.id,
img: albumId ? 'https://y.gtimg.cn/music/photo_new/T002R500x500M000' + albumId + '.jpg' : '',
lrc: null, types: [], _types: {}, typeUrl: {}
};
}
return null;
}).catch(function() { return null; });
}
function musicInfo(songmid) { return musicDetail(songmid); }
// ===== 热搜 =====
function hotSearch() {
var data = {
comm: { ct: '19', cv: '1803', guid: '0', patch: '118', psrf_access_token_expiresAt: 0, psrf_qqaccess_token: '', psrf_qqopenid: '', psrf_qqunionid: '', tmeAppID: 'qqmusic', tmeLoginType: 0, uin: '0', wid: '0' },
hotkey: { method: 'GetHotkeyForQQMusicPC', module: 'tencent_musicsoso_hotkey.HotkeyService', param: { search_id: '', uin: 0 } }
};
return requestUrl('https://u.y.qq.com/cgi-bin/musicu.fcg', 'POST', {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)',
'Referer': 'https://y.qq.com/portal/player.html',
'Content-Type': 'application/json'
}, JSON.stringify(data)).then(function(res) {
var list = [];
if (res.body && res.body.hotkey && res.body.hotkey.data && res.body.hotkey.data.vec_hotkey) {
var arr = res.body.hotkey.data.vec_hotkey;
for (var i = 0; i < arr.length; i++) list.push(arr[i].query);
}
return { source: 'tx', list: list };
}).catch(function() { return { source: 'tx', list: [] }; });
}
// ===== 搜索建议 =====
function tipSearch(str) {
return Promise.resolve({ order: [], songs: [], artists: [], albums: [], playlists: [] });
}
// ===== 榜单 =====
function leaderboard() {
return Promise.resolve([]);
}
// ===== 歌单 =====
function songList() {
return Promise.resolve({ list: [], total: 0, page: 1, source: 'tx' });
}
// ===== 歌手 =====
function singer() {
return Promise.resolve({ source: 'tx', list: [], total: 0 });
}
// ===== 专辑 =====
function album() {
return Promise.resolve({ source: 'tx', list: [], total: 0 });
}
// ===== 插件信息 =====
var pluginInfo = {
id: 'koneko_ceru_tx',
name: 'QQ音乐 - Koneko 聆澜',
version: '0.0.3',
source: 'tx',
description: 'QQ音乐音源基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
};
module.exports = {
pluginInfo: pluginInfo,
musicSearch: { search: search },
tipSearch: { search: tipSearch },
hotSearch: { getList: hotSearch },
leaderboard: { getBoards: leaderboard },
songList: { search: songList, getTags: function() { return Promise.resolve({ tags: [], hotTag: [], source: 'tx' }); }, getList: songList },
singer: { getInfo: singer, getSongList: singer, getAlbumList: singer },
album: { getInfo: album, getSongList: album },
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo
};

View File

@@ -0,0 +1,201 @@
var https = require('https');
var http = require('http');
var crypto = require('crypto');
var querystring = require('querystring');
// ===== 通用HTTP请求 =====
function doRequest(protocol, options, postData) {
return new Promise(function(resolve, reject) {
var client = protocol === 'https' ? https : http;
var req = client.request(options, function(res) {
var chunks = [];
res.on('data', function(chunk) { chunks.push(chunk); });
res.on('end', function() {
var buf = Buffer.concat(chunks);
var body;
try {
body = JSON.parse(buf.toString('utf8'));
} catch (e) {
body = buf.toString('utf8');
}
resolve({ body: body, statusCode: res.statusCode, headers: res.headers });
});
});
req.on('error', function(err) { reject(err); });
req.setTimeout(15000, function() { req.destroy(); reject(new Error('timeout')); });
if (postData) req.write(postData);
req.end();
});
}
function requestUrl(urlStr, method, headers, body) {
var parsed = new URL(urlStr);
var protocol = parsed.protocol.replace(':', '');
var options = {
hostname: parsed.hostname,
port: parsed.port || (protocol === 'https' ? 443 : 80),
path: parsed.pathname + parsed.search,
method: method || 'GET',
headers: headers || {}
};
var postData = null;
if (body) {
if (typeof body === 'string') {
postData = Buffer.from(body, 'utf8');
} else if (Buffer.isBuffer(body)) {
postData = body;
} else {
postData = Buffer.from(JSON.stringify(body), 'utf8');
if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json';
}
if (postData) options.headers['Content-Length'] = postData.length;
}
return doRequest(protocol, options, postData);
}
function formatPlayTime(seconds) {
if (!seconds) return '00:00';
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
// ===== 搜索实现 =====
function musicSearch(str, page, limit) {
var url = 'https://jadeite.migu.cn/music_search/v3/search?text=' + encodeURIComponent(str) + '&pageNo=' + page + '&pageSize=' + limit + '&searchSwitch={"song":1}';
return requestUrl(url, 'GET', {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)',
'Referer': 'https://jadeite.migu.cn/music_search.html?keyword=' + encodeURIComponent(str),
'By': '7242a87651f953c07d759b1d08c62b74'
}).then(function(res) {
if (res.body && res.body.musics) return res.body.musics;
throw new Error('mg search failed');
});
}
function handleSearchResult(rawList) {
var list = [];
for (var i = 0; i < rawList.length; i++) {
var item = rawList[i];
var types = [];
var _types = {};
types.push({ type: '128k', size: '' }); _types['128k'] = { size: '' };
if (item.newRateFormats) {
for (var j = 0; j < item.newRateFormats.length; j++) {
var fmt = item.newRateFormats[j];
if (fmt.formatType === 'SQ') { types.push({ type: 'flac', size: '' }); _types.flac = { size: '' }; }
if (fmt.formatType === 'HQ') { types.push({ type: '320k', size: '' }); _types['320k'] = { size: '' }; }
}
}
var img = item.img || (item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : '');
if (img && img.startsWith('/')) img = 'https://d.musicapp.migu.cn' + img;
list.push({
singer: item.artist || item.singerName || '',
name: item.title || item.songName || '',
albumName: item.album || item.albumName || '',
albumId: item.albumId || '',
source: 'mg',
interval: item.length ? item.length.replace(/.*(\d\d:\d\d)$/, '$1') : '',
songmid: item.copyrightId || item.id || '',
songId: item.copyrightId || item.id || '',
img: img,
lrc: null,
types: types, _types: _types, typeUrl: {}
});
}
return list;
}
function search(str, page, limit, retryNum) {
if (retryNum === undefined) retryNum = 0;
if (++retryNum > 3) return Promise.reject(new Error('try max num'));
if (!limit) limit = 30;
return musicSearch(str, page, limit).then(function(rawList) {
var list = handleSearchResult(rawList);
return { list: list, allPage: page + 1, limit: limit, total: list.length * 10, source: 'mg' };
});
}
// ===== 播放链接 =====
function getUrl(songInfo, type) {
var q = type || '128k';
var songId = songInfo.songmid || songInfo.id || '';
var ceruKey = process.env.ceru_key || '';
var headers = { 'User-Agent': 'QZMusic/2.0' };
if (ceruKey) headers['X-API-Key'] = ceruKey;
return requestUrl('https://source.shiqianjiang.cn/api/music/url?source=mg&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(q), 'GET', headers)
.then(function(res) {
if (res.body && typeof res.body === 'object' && res.body.url) return res.body.url;
if (res.body && typeof res.body === 'object' && res.body.data && res.body.data.url) return res.body.data.url;
if (typeof res.body === 'string' && res.body.indexOf('http') === 0) return res.body;
return '';
}).catch(function() { return ''; });
}
// ===== 封面图 =====
function getPic(songInfo) {
if (songInfo.img) return Promise.resolve(songInfo.img);
return Promise.resolve('');
}
// ===== 歌词 =====
function getLyric(songInfo) {
var copyrightId = songInfo.songmid || '';
if (!copyrightId) return Promise.resolve('');
return requestUrl('http://music.migu.cn/v3/api/music/audioPlayer/getLyric?copyrightId=' + encodeURIComponent(copyrightId), 'GET', {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)',
'Referer': 'http://music.migu.cn/v3/music/player/audio?from=migu'
}).then(function(res) {
if (res.body && res.body.lyric) return res.body.lyric;
return '';
}).catch(function() { return ''; });
}
// ===== 音乐详情 =====
function musicDetail(songmid) {
return Promise.resolve(null);
}
function musicInfo(songmid) { return musicDetail(songmid); }
// ===== 热搜 =====
function hotSearch() { return Promise.resolve({ source: 'mg', list: [] }); }
// ===== 搜索建议 =====
function tipSearch(str) { return Promise.resolve({ order: [], songs: [], artists: [], albums: [], playlists: [] }); }
// ===== 榜单 =====
function leaderboard() { return Promise.resolve([]); }
// ===== 歌单 =====
function songList() { return Promise.resolve({ list: [], total: 0, page: 1, source: 'mg' }); }
// ===== 歌手 =====
function singer() { return Promise.resolve({ source: 'mg', list: [], total: 0 }); }
// ===== 专辑 =====
function album() { return Promise.resolve({ source: 'mg', list: [], total: 0 }); }
// ===== 插件信息 =====
var pluginInfo = {
id: 'koneko_ceru_mg',
name: '咪咕音乐 - Koneko 聆澜',
version: '0.0.3',
source: 'mg',
description: '咪咕音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
};
module.exports = {
pluginInfo: pluginInfo,
musicSearch: { search: search },
tipSearch: { search: tipSearch },
hotSearch: { getList: hotSearch },
leaderboard: { getBoards: leaderboard },
songList: { search: songList, getTags: function() { return Promise.resolve({ tags: [], hotTag: [], source: 'mg' }); }, getList: songList },
singer: { getInfo: singer, getSongList: singer, getAlbumList: singer },
album: { getInfo: album, getSongList: album },
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo
};

View File

@@ -0,0 +1,353 @@
var https = require('https');
var http = require('http');
var crypto = require('crypto');
var querystring = require('querystring');
// ===== 通用HTTP请求 =====
function doRequest(protocol, options, postData) {
return new Promise(function(resolve, reject) {
var client = protocol === 'https' ? https : http;
var req = client.request(options, function(res) {
var chunks = [];
res.on('data', function(chunk) { chunks.push(chunk); });
res.on('end', function() {
var buf = Buffer.concat(chunks);
var body;
try {
body = JSON.parse(buf.toString('utf8'));
} catch (e) {
body = buf.toString('utf8');
}
resolve({ body: body, statusCode: res.statusCode, headers: res.headers });
});
});
req.on('error', function(err) { reject(err); });
req.setTimeout(15000, function() { req.destroy(); reject(new Error('timeout')); });
if (postData) req.write(postData);
req.end();
});
}
function requestUrl(urlStr, method, headers, body) {
var parsed = new URL(urlStr);
var protocol = parsed.protocol.replace(':', '');
var options = {
hostname: parsed.hostname,
port: parsed.port || (protocol === 'https' ? 443 : 80),
path: parsed.pathname + parsed.search,
method: method || 'GET',
headers: headers || {}
};
var postData = null;
if (body) {
if (typeof body === 'string') {
postData = Buffer.from(body, 'utf8');
} else if (Buffer.isBuffer(body)) {
postData = body;
} else {
postData = Buffer.from(JSON.stringify(body), 'utf8');
if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json';
}
if (postData) options.headers['Content-Length'] = postData.length;
}
return doRequest(protocol, options, postData);
}
// ===== 网易云eapi加密 =====
var EAPI_KEY = 'e82ckenh8dichen8';
function aesEncryptEapi(text) {
var cipher = crypto.createCipheriv('aes-128-ecb', EAPI_KEY, '');
var encrypted = cipher.update(text, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
}
function eapi(url, params) {
var payload = JSON.stringify({
method: 'POST',
url: url,
params: params
});
return { params: aesEncryptEapi(payload) };
}
// ===== weapi加密简化版用于歌词等 =====
var WEAPI_IV = '0102030405060708';
var WEAPI_PRESET_KEY = '0CoJUm6Qyw8W8jud';
var WEAPI_RSA_PUBKEY = '010001';
var WEAPI_RSA_MODULUS = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7';
function aesEncryptWeapi(text, key) {
var cipher = crypto.createCipheriv('aes-128-cbc', key, WEAPI_IV);
var encrypted = cipher.update(text, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
}
function rsaEncryptWeapi(text) {
var reversed = text.split('').reverse().join('');
var input = Buffer.from(reversed, 'utf8').toString('hex');
var bigInput = BigInt('0x' + input);
var bigExp = BigInt('0x' + WEAPI_RSA_PUBKEY);
var bigMod = BigInt('0x' + WEAPI_RSA_MODULUS);
var bigResult = 1n;
var e = bigExp;
var b = bigInput;
while (e > 0n) {
if (e % 2n === 1n) bigResult = (bigResult * b) % bigMod;
b = (b * b) % bigMod;
e = e / 2n;
}
var result = bigResult.toString(16).padStart(256, '0');
return result;
}
function weapi(params) {
var text = JSON.stringify(params);
var secretKey = '';
for (var i = 0; i < 16; i++) secretKey += String.fromCharCode(97 + Math.floor(Math.random() * 25));
var encrypted = aesEncryptWeapi(aesEncryptWeapi(text, WEAPI_PRESET_KEY), secretKey);
var encSecKey = rsaEncryptWeapi(secretKey);
return { params: encrypted, encSecKey: encSecKey };
}
// ===== 格式化辅助 =====
function formatPlayTime(seconds) {
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
function formatSingerName(arr, key) {
if (!arr || !arr.length) return '';
var names = [];
for (var i = 0; i < arr.length; i++) names.push(arr[i][key] || arr[i]);
return names.join('、');
}
// ===== 搜索实现 =====
function musicSearch(str, page, limit) {
var form = eapi('/api/search/song/list/page', {
keyword: str,
needCorrect: '1',
channel: 'typing',
offset: limit * (page - 1),
scene: 'normal',
total: page == 1,
limit: limit
});
return requestUrl('http://interface3.music.163.com/eapi/search/song/list/page', 'POST', {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
'Origin': 'https://music.163.com',
'Content-Type': 'application/x-www-form-urlencoded'
}, querystring.stringify(form)).then(function(res) {
return res.body;
});
}
function handleSearchResult(rawList) {
if (!rawList) return [];
var list = [];
for (var i = 0; i < rawList.length; i++) {
var item = rawList[i];
if (!item.baseInfo || !item.baseInfo.simpleSongData) continue;
var s = item.baseInfo.simpleSongData;
var types = [];
var _types = {};
if (s.h && s.h.br >= 320000) { types.push({ type: '320k', size: '' }); _types['320k'] = { size: '' }; }
else if (s.m && s.m.br >= 192000) { types.push({ type: '192k', size: '' }); _types['192k'] = { size: '' }; }
else if (s.l) { types.push({ type: '128k', size: '' }); _types['128k'] = { size: '' }; }
if (s.sq) { types.push({ type: 'flac', size: '' }); _types.flac = { size: '' }; }
if (s.hr) { types.push({ type: 'hires', size: '' }); _types.hires = { size: '' }; }
list.push({
singer: formatSingerName(s.ar, 'name'),
name: s.name,
albumName: s.al && s.al.name ? s.al.name : '',
albumId: s.al && s.al.id ? s.al.id : '',
source: 'wy',
interval: formatPlayTime((s.dt || 0) / 1000),
songmid: s.id,
img: s.al && s.al.picUrl ? s.al.picUrl : '',
lrc: null,
types: types,
_types: _types,
typeUrl: {}
});
}
return list;
}
function search(str, page, limit, retryNum) {
if (retryNum === undefined) retryNum = 0;
if (++retryNum > 3) return Promise.reject(new Error('try max num'));
if (!limit) limit = 30;
return musicSearch(str, page, limit).then(function(result) {
if (!result || result.code !== 200) return search(str, page, limit, retryNum);
var list = handleSearchResult(result.data && result.data.resources ? result.data.resources : []);
if (!list || list.length === 0) return search(str, page, limit, retryNum);
var total = result.data && result.data.totalCount ? result.data.totalCount : 0;
return {
list: list,
allPage: Math.ceil(total / limit),
limit: limit,
total: total,
source: 'wy'
};
});
}
// ===== 播放链接 =====
function getUrl(songInfo, type) {
var q = type || '128k';
var songId = songInfo.songmid || songInfo.id || '';
var ceruKey = process.env.ceru_key || '';
var headers = { 'User-Agent': 'QZMusic/2.0' };
if (ceruKey) headers['X-API-Key'] = ceruKey;
return requestUrl('https://source.shiqianjiang.cn/api/music/url?source=wy&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(q), 'GET', headers)
.then(function(res) {
if (res.body && typeof res.body === 'object' && res.body.url) return res.body.url;
if (res.body && typeof res.body === 'object' && res.body.data && res.body.data.url) return res.body.data.url;
if (typeof res.body === 'string' && res.body.indexOf('http') === 0) return res.body;
return '';
})
.catch(function() { return ''; });
}
// ===== 封面图 =====
function getPic(songInfo) {
if (songInfo.img) return Promise.resolve(songInfo.img);
return requestUrl('https://music.163.com/weapi/v3/song/detail', 'POST', {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
'Referer': 'https://music.163.com/song?id=' + songInfo.songmid,
'Origin': 'https://music.163.com',
'Content-Type': 'application/x-www-form-urlencoded'
}, querystring.stringify(weapi({ c: '[{"id":' + songInfo.songmid + '}]', ids: '[' + songInfo.songmid + ']' })))
.then(function(res) {
if (res.body && res.body.songs && res.body.songs.length) return res.body.songs[0].al.picUrl;
return '';
})
.catch(function() { return ''; });
}
// ===== 歌词 =====
function getLyric(songInfo) {
var songmid = songInfo.songmid || '';
return requestUrl('https://music.163.com/api/song/lyric?id=' + songmid + '&lv=1&kv=1&tv=-1', 'GET', {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
'Referer': 'https://music.163.com/song?id=' + songmid
}).then(function(res) {
if (res.body && res.body.lrc && res.body.lrc.lyric) return res.body.lrc.lyric;
return '';
}).catch(function() { return ''; });
}
// ===== 音乐详情 =====
function musicDetail(songmid) {
return requestUrl('https://music.163.com/weapi/v3/song/detail', 'POST', {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
'Origin': 'https://music.163.com',
'Content-Type': 'application/x-www-form-urlencoded'
}, querystring.stringify(weapi({ c: '[{"id":' + songmid + '}]', ids: '[' + songmid + ']' })))
.then(function(res) {
if (res.body && res.body.songs && res.body.songs.length) {
var s = res.body.songs[0];
return {
singer: formatSingerName(s.ar, 'name'),
name: s.name,
albumName: s.al && s.al.name ? s.al.name : '',
albumId: s.al && s.al.id ? s.al.id : '',
source: 'wy',
interval: formatPlayTime((s.dt || 0) / 1000),
songmid: s.id,
img: s.al && s.al.picUrl ? s.al.picUrl : '',
lrc: null,
types: [],
_types: {},
typeUrl: {}
};
}
return null;
})
.catch(function() { return null; });
}
function musicInfo(songmid) {
return musicDetail(songmid);
}
// ===== 热搜 =====
function hotSearch() {
return requestUrl('https://music.163.com/weapi/search/hot', 'POST', {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
'Origin': 'https://music.163.com',
'Content-Type': 'application/x-www-form-urlencoded'
}, querystring.stringify(weapi({ type: 1111 })))
.then(function(res) {
var list = [];
if (res.body && res.body.result && res.body.result.hots) {
for (var i = 0; i < res.body.result.hots.length; i++) list.push(res.body.result.hots[i].first);
}
return { source: 'wy', list: list };
})
.catch(function() { return { source: 'wy', list: [] }; });
}
// ===== 搜索建议 =====
function tipSearch(str) {
return requestUrl('https://music.163.com/weapi/search/suggest/web', 'POST', {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
'Origin': 'https://music.163.com',
'Content-Type': 'application/x-www-form-urlencoded'
}, querystring.stringify(weapi({ s: str, limit: 10 })))
.then(function(res) {
var result = { order: [], songs: [], artists: [], albums: [], playlists: [] };
if (res.body && res.body.result) {
var r = res.body.result;
if (r.songs) { result.order.push('songs'); result.songs = r.songs.map(function(s) { return { name: s.name, id: s.id, source: 'wy' }; }); }
if (r.artists) { result.order.push('artists'); result.artists = r.artists.map(function(a) { return { name: a.name, id: a.id, source: 'wy' }; }); }
}
return result;
})
.catch(function() { return { order: [], songs: [], artists: [], albums: [], playlists: [] }; });
}
// ===== 榜单 =====
function leaderboard() {
return Promise.resolve([]);
}
// ===== 歌单 =====
function songList() {
return Promise.resolve({ list: [], total: 0, page: 1, source: 'wy' });
}
// ===== 歌手 =====
function singer() {
return Promise.resolve({ source: 'wy', list: [], total: 0 });
}
// ===== 专辑 =====
function album() {
return Promise.resolve({ source: 'wy', list: [], total: 0 });
}
// ===== 插件信息 =====
var pluginInfo = {
id: 'koneko_ceru_wy',
name: '网易云音乐 - Koneko 聆澜',
version: '0.0.3',
source: 'wy',
description: '网易云音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
};
module.exports = {
pluginInfo: pluginInfo,
musicSearch: { search: search },
tipSearch: { search: tipSearch },
hotSearch: { getList: hotSearch },
leaderboard: { getBoards: leaderboard },
songList: { search: songList, getTags: function() { return Promise.resolve({ tags: [], hotTag: [], source: 'wy' }); }, getList: songList },
singer: { getInfo: singer, getSongList: singer, getAlbumList: singer },
album: { getInfo: album, getSongList: album },
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo
};

View File

@@ -0,0 +1,188 @@
var https = require('https');
var http = require('http');
var crypto = require('crypto');
var querystring = require('querystring');
// ===== 通用HTTP请求 =====
function doRequest(protocol, options, postData) {
return new Promise(function(resolve, reject) {
var client = protocol === 'https' ? https : http;
var req = client.request(options, function(res) {
var chunks = [];
res.on('data', function(chunk) { chunks.push(chunk); });
res.on('end', function() {
var buf = Buffer.concat(chunks);
var body;
try {
body = JSON.parse(buf.toString('utf8'));
} catch (e) {
body = buf.toString('utf8');
}
resolve({ body: body, statusCode: res.statusCode, headers: res.headers });
});
});
req.on('error', function(err) { reject(err); });
req.setTimeout(15000, function() { req.destroy(); reject(new Error('timeout')); });
if (postData) req.write(postData);
req.end();
});
}
function requestUrl(urlStr, method, headers, body) {
var parsed = new URL(urlStr);
var protocol = parsed.protocol.replace(':', '');
var options = {
hostname: parsed.hostname,
port: parsed.port || (protocol === 'https' ? 443 : 80),
path: parsed.pathname + parsed.search,
method: method || 'GET',
headers: headers || {}
};
var postData = null;
if (body) {
if (typeof body === 'string') {
postData = Buffer.from(body, 'utf8');
} else if (Buffer.isBuffer(body)) {
postData = body;
} else {
postData = Buffer.from(JSON.stringify(body), 'utf8');
if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json';
}
if (postData) options.headers['Content-Length'] = postData.length;
}
return doRequest(protocol, options, postData);
}
function formatPlayTime(seconds) {
if (!seconds) return '00:00';
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
// ===== 搜索实现 =====
function musicSearch(str, page, limit) {
var url = 'https://kuwo-api.vercel.app/api/search?key=' + encodeURIComponent(str) + '&page=' + page + '&limit=' + limit;
return requestUrl(url, 'GET', { 'User-Agent': 'Mozilla/5.0 (Linux; Android 10)' }).then(function(res) {
if (res.body && res.body.data && res.body.data.list) return res.body.data.list;
throw new Error('kw search failed');
});
}
function handleSearchResult(rawList) {
var list = [];
for (var i = 0; i < rawList.length; i++) {
var item = rawList[i];
var types = [];
var _types = {};
types.push({ type: '128k', size: '' }); _types['128k'] = { size: '' };
types.push({ type: '320k', size: '' }); _types['320k'] = { size: '' };
list.push({
singer: item.artist || '',
name: item.name || '',
albumName: item.album || '',
albumId: item.albumid || '',
source: 'kw',
interval: item.duration ? formatPlayTime(item.duration) : '',
songmid: item.id || item.rid || '',
songId: item.id || item.rid || '',
img: item.pic || '',
lrc: null,
types: types, _types: _types, typeUrl: {}
});
}
return list;
}
function search(str, page, limit, retryNum) {
if (retryNum === undefined) retryNum = 0;
if (++retryNum > 3) return Promise.reject(new Error('try max num'));
if (!limit) limit = 30;
return musicSearch(str, page, limit).then(function(rawList) {
var list = handleSearchResult(rawList);
return { list: list, allPage: page + 1, limit: limit, total: list.length * 10, source: 'kw' };
});
}
// ===== 播放链接 =====
function getUrl(songInfo, type) {
var q = type || '128k';
var songId = songInfo.songmid || songInfo.id || '';
var ceruKey = process.env.ceru_key || '';
var headers = { 'User-Agent': 'QZMusic/2.0' };
if (ceruKey) headers['X-API-Key'] = ceruKey;
return requestUrl('https://source.shiqianjiang.cn/api/music/url?source=kw&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(q), 'GET', headers)
.then(function(res) {
if (res.body && typeof res.body === 'object' && res.body.url) return res.body.url;
if (res.body && typeof res.body === 'object' && res.body.data && res.body.data.url) return res.body.data.url;
if (typeof res.body === 'string' && res.body.indexOf('http') === 0) return res.body;
return '';
}).catch(function() { return ''; });
}
// ===== 封面图 =====
function getPic(songInfo) {
if (songInfo.img) return Promise.resolve(songInfo.img);
return Promise.resolve('');
}
// ===== 歌词 =====
function getLyric(songInfo) {
var rid = songInfo.songmid || '';
if (!rid) return Promise.resolve('');
return requestUrl('https://kuwo-api.vercel.app/api/lyric?id=' + encodeURIComponent(rid), 'GET', {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)'
}).then(function(res) {
if (res.body && res.body.data && res.body.data.lrc) return res.body.data.lrc;
return '';
}).catch(function() { return ''; });
}
// ===== 音乐详情 =====
function musicDetail(songmid) {
return Promise.resolve(null);
}
function musicInfo(songmid) { return musicDetail(songmid); }
// ===== 热搜 =====
function hotSearch() { return Promise.resolve({ source: 'kw', list: [] }); }
// ===== 搜索建议 =====
function tipSearch(str) { return Promise.resolve({ order: [], songs: [], artists: [], albums: [], playlists: [] }); }
// ===== 榜单 =====
function leaderboard() { return Promise.resolve([]); }
// ===== 歌单 =====
function songList() { return Promise.resolve({ list: [], total: 0, page: 1, source: 'kw' }); }
// ===== 歌手 =====
function singer() { return Promise.resolve({ source: 'kw', list: [], total: 0 }); }
// ===== 专辑 =====
function album() { return Promise.resolve({ source: 'kw', list: [], total: 0 }); }
// ===== 插件信息 =====
var pluginInfo = {
id: 'koneko_ceru_kw',
name: '酷我音乐 - Koneko 聆澜',
version: '0.0.3',
source: 'kw',
description: '酷我音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
};
module.exports = {
pluginInfo: pluginInfo,
musicSearch: { search: search },
tipSearch: { search: tipSearch },
hotSearch: { getList: hotSearch },
leaderboard: { getBoards: leaderboard },
songList: { search: songList, getTags: function() { return Promise.resolve({ tags: [], hotTag: [], source: 'kw' }); }, getList: songList },
singer: { getInfo: singer, getSongList: singer, getAlbumList: singer },
album: { getInfo: album, getSongList: album },
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo
};

View File

@@ -0,0 +1,199 @@
var https = require('https');
var http = require('http');
var crypto = require('crypto');
var querystring = require('querystring');
// ===== 通用HTTP请求 =====
function doRequest(protocol, options, postData) {
return new Promise(function(resolve, reject) {
var client = protocol === 'https' ? https : http;
var req = client.request(options, function(res) {
var chunks = [];
res.on('data', function(chunk) { chunks.push(chunk); });
res.on('end', function() {
var buf = Buffer.concat(chunks);
var body;
try {
body = JSON.parse(buf.toString('utf8'));
} catch (e) {
body = buf.toString('utf8');
}
resolve({ body: body, statusCode: res.statusCode, headers: res.headers });
});
});
req.on('error', function(err) { reject(err); });
req.setTimeout(15000, function() { req.destroy(); reject(new Error('timeout')); });
if (postData) req.write(postData);
req.end();
});
}
function requestUrl(urlStr, method, headers, body) {
var parsed = new URL(urlStr);
var protocol = parsed.protocol.replace(':', '');
var options = {
hostname: parsed.hostname,
port: parsed.port || (protocol === 'https' ? 443 : 80),
path: parsed.pathname + parsed.search,
method: method || 'GET',
headers: headers || {}
};
var postData = null;
if (body) {
if (typeof body === 'string') {
postData = Buffer.from(body, 'utf8');
} else if (Buffer.isBuffer(body)) {
postData = body;
} else {
postData = Buffer.from(JSON.stringify(body), 'utf8');
if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json';
}
if (postData) options.headers['Content-Length'] = postData.length;
}
return doRequest(protocol, options, postData);
}
function formatPlayTime(seconds) {
if (!seconds) return '00:00';
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
// ===== 搜索实现 =====
function musicSearch(str, page, limit) {
var url = 'http://mobilecdn.kugou.com/api/v3/search/song?showtype=14&highlight=em&pagesize=' + limit + '&page=' + page + '&keyword=' + encodeURIComponent(str);
return requestUrl(url, 'GET', { 'User-Agent': 'Mozilla/5.0 (Linux; Android 10)' }).then(function(res) {
if (res.body && res.body.data && res.body.data.info) return res.body.data.info;
throw new Error('kg search failed');
});
}
function handleSearchResult(rawList) {
var list = [];
for (var i = 0; i < rawList.length; i++) {
var item = rawList[i];
var types = [];
var _types = {};
types.push({ type: '128k', size: '' }); _types['128k'] = { size: '' };
types.push({ type: '320k', size: '' }); _types['320k'] = { size: '' };
list.push({
singer: item.singername || '',
name: item.songname || '',
albumName: item.album_name || '',
albumId: item.album_id || '',
source: 'kg',
interval: item.duration ? formatPlayTime(item.duration) : '',
songmid: item.hash || item.audio_id || '',
songId: item.hash || item.audio_id || '',
img: item.img || '',
lrc: null,
types: types, _types: _types, typeUrl: {}
});
}
return list;
}
function search(str, page, limit, retryNum) {
if (retryNum === undefined) retryNum = 0;
if (++retryNum > 3) return Promise.reject(new Error('try max num'));
if (!limit) limit = 30;
return musicSearch(str, page, limit).then(function(rawList) {
var list = handleSearchResult(rawList);
return { list: list, allPage: page + 1, limit: limit, total: list.length * 10, source: 'kg' };
});
}
// ===== 播放链接 =====
function getUrl(songInfo, type) {
var q = type || '128k';
var songId = songInfo.songmid || songInfo.id || '';
var ceruKey = process.env.ceru_key || '';
var headers = { 'User-Agent': 'QZMusic/2.0' };
if (ceruKey) headers['X-API-Key'] = ceruKey;
return requestUrl('https://source.shiqianjiang.cn/api/music/url?source=kg&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(q), 'GET', headers)
.then(function(res) {
if (res.body && typeof res.body === 'object' && res.body.url) return res.body.url;
if (res.body && typeof res.body === 'object' && res.body.data && res.body.data.url) return res.body.data.url;
if (typeof res.body === 'string' && res.body.indexOf('http') === 0) return res.body;
return '';
}).catch(function() { return ''; });
}
// ===== 封面图 =====
function getPic(songInfo) {
if (songInfo.img) return Promise.resolve(songInfo.img);
return Promise.resolve('');
}
// ===== 歌词 =====
function getLyric(songInfo) {
var hash = songInfo.songmid || '';
if (!hash) return Promise.resolve('');
return requestUrl('https://krcs.kugou.com/search?ver=1&man=yes&client=mobi&keyword=&duration=&hash=' + encodeURIComponent(hash) + '&album_audio_id=', 'GET', {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)'
}).then(function(res) {
if (res.body && res.body.candidates && res.body.candidates.length) {
var id = res.body.candidates[0].id;
var accesskey = res.body.candidates[0].accesskey;
return requestUrl('https://krcs.kugou.com/download?ver=1&client=pc&id=' + encodeURIComponent(id) + '&accesskey=' + encodeURIComponent(accesskey) + '&fmt=lrc&charset=utf8', 'GET', {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)'
}).then(function(res2) {
if (res2.body && res2.body.content) {
try { return Buffer.from(res2.body.content, 'base64').toString('utf8'); } catch(e) { return ''; }
}
return '';
});
}
return '';
}).catch(function() { return ''; });
}
// ===== 音乐详情 =====
function musicDetail(songmid) {
return Promise.resolve(null);
}
function musicInfo(songmid) { return musicDetail(songmid); }
// ===== 热搜 =====
function hotSearch() { return Promise.resolve({ source: 'kg', list: [] }); }
// ===== 搜索建议 =====
function tipSearch(str) { return Promise.resolve({ order: [], songs: [], artists: [], albums: [], playlists: [] }); }
// ===== 榜单 =====
function leaderboard() { return Promise.resolve([]); }
// ===== 歌单 =====
function songList() { return Promise.resolve({ list: [], total: 0, page: 1, source: 'kg' }); }
// ===== 歌手 =====
function singer() { return Promise.resolve({ source: 'kg', list: [], total: 0 }); }
// ===== 专辑 =====
function album() { return Promise.resolve({ source: 'kg', list: [], total: 0 }); }
// ===== 插件信息 =====
var pluginInfo = {
id: 'koneko_ceru_kg',
name: '酷狗音乐 - Koneko 聆澜',
version: '0.0.3',
source: 'kg',
description: '酷狗音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
};
module.exports = {
pluginInfo: pluginInfo,
musicSearch: { search: search },
tipSearch: { search: tipSearch },
hotSearch: { getList: hotSearch },
leaderboard: { getBoards: leaderboard },
songList: { search: songList, getTags: function() { return Promise.resolve({ tags: [], hotTag: [], source: 'kg' }); }, getList: songList },
singer: { getInfo: singer, getSongList: singer, getAlbumList: singer },
album: { getInfo: album, getSongList: album },
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo
};