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 }); } // ===== 插件信息 ===== module.exports = { info: { id: 'koneko_ceru_wy', name: '网易云音乐 - Koneko 聆澜', version: '0.0.3', source: 'wy', description: '网易云音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口' }, ext: [], env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜播放接口密钥' }], quality: [ { name: '标准', ui: '标', id: '128k' }, { name: '高品', ui: 'HQ', id: '320k' }, { name: '无损', ui: 'SQ', id: 'flac' } ], supportFunc: ['musicSearch', 'tipSearch', 'hotSearch', 'leaderboard', 'songList', 'singer', 'album', 'getLyric', 'getPic', 'getUrl', 'musicDetail', 'musicInfo'], 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 };