From e58beff507dac52a53a739649de30ae7117c8320 Mon Sep 17 00:00:00 2001 From: Koneko Date: Sun, 12 Jul 2026 18:06:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20v0.1.5=20=E4=BF=AE=E5=A4=8D=E7=BD=91?= =?UTF-8?q?=E6=98=93=E4=BA=91/=E9=85=B7=E7=8B=97/=E5=92=AA=E5=92=95?= =?UTF-8?q?=E6=90=9C=E7=B4=A2API=E7=A8=B3=E5=AE=9A=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 网易云: 回退到 api/search/get/web 稳定接口 - 酷狗: 回退到 format=json 不带 showtype=14 的接口 - 咪咕: 使用 searchAll 接口,兼容多种数据结构 - 统一版本号 v0.1.5,更新 README --- README.md | 14 +- 聆澜/Koneko_聆澜_咪咕音乐_v0.1.5_QZv2.js | 233 +++++++++++++++++++ 聆澜/Koneko_聆澜_网易云音乐_v0.1.5_QZv2.js | 253 +++++++++++++++++++++ 聆澜/Koneko_聆澜_酷狗音乐_v0.1.5_QZv2.js | 222 ++++++++++++++++++ 4 files changed, 715 insertions(+), 7 deletions(-) create mode 100644 聆澜/Koneko_聆澜_咪咕音乐_v0.1.5_QZv2.js create mode 100644 聆澜/Koneko_聆澜_网易云音乐_v0.1.5_QZv2.js create mode 100644 聆澜/Koneko_聆澜_酷狗音乐_v0.1.5_QZv2.js diff --git a/README.md b/README.md index f08f6a1..ae65f94 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ QZ Music v2/v3 音源插件集合。每个插件仅包含一个平台,官方 | 文件 | 平台 | 版本 | 说明 | |------|------|------|------| -| `Koneko_QQ音乐_v0.0.2.js` | QQ音乐 | 0.0.2 | 官方搜索 + 10路API音源容灾 | -| `Koneko_酷狗音乐_v0.0.2.js` | 酷狗音乐 | 0.0.2 | 官方搜索 + 8路API音源容灾 | -| `Koneko_酷我音乐_v0.0.2.js` | 酷我音乐 | 0.0.2 | 官方搜索 + 9路API音源容灾 | -| `Koneko_网易云音乐_v0.0.2.js` | 网易云音乐 | 0.0.2 | 官方搜索 + 6路API音源容灾 + Cookie全功能 | -| `Koneko_咪咕音乐_v0.0.2.js` | 咪咕音乐 | 0.0.2 | 官方搜索 + 8路API音源容灾 | -| `Koneko_GIT音源_v0.0.2.js` | GIT音源 | 0.0.2 | 纯音源 + 2路API音源容灾 | +| `Koneko_聆澜_QQ音乐_v0.1.4_QZv2.js` | QQ音乐 | 0.1.4 | 官方搜索 + 聆澜音源 | +| `Koneko_聆澜_酷狗音乐_v0.1.5_QZv2.js` | 酷狗音乐 | 0.1.5 | 官方搜索 + 聆澜音源(修复搜索API) | +| `Koneko_聆澜_酷我音乐_v0.1.4_QZv2.js` | 酷我音乐 | 0.1.4 | 官方搜索 + 聆澜音源 | +| `Koneko_聆澜_网易云音乐_v0.1.5_QZv2.js` | 网易云音乐 | 0.1.5 | 官方搜索 + 聆澜音源 + Cookie全功能(修复搜索API) | +| `Koneko_聆澜_咪咕音乐_v0.1.5_QZv2.js` | 咪咕音乐 | 0.1.5 | 官方搜索 + 聆澜音源(修复搜索API) | +| `Koneko_聆澜_GIT音源_v0.1.4_QZv2.js` | GIT音源 | 0.1.4 | 纯音源 + 聆澜音源 | ## 环境变量 @@ -34,7 +34,7 @@ QZ Music v2/v3 音源插件集合。每个插件仅包含一个平台,官方 ## 容灾机制 -`getUrl` 采用并发请求多个 API,取第一个成功返回的 URL。聆澜 API 需要配置 `ceru_key`,未配置时自动跳过。 +`getUrl` 使用聆澜 API 获取音源。配置 `ceru_key` 以获取更高音质。 ## 版权声明 diff --git a/聆澜/Koneko_聆澜_咪咕音乐_v0.1.5_QZv2.js b/聆澜/Koneko_聆澜_咪咕音乐_v0.1.5_QZv2.js new file mode 100644 index 0000000..eb5097e --- /dev/null +++ b/聆澜/Koneko_聆澜_咪咕音乐_v0.1.5_QZv2.js @@ -0,0 +1,233 @@ +var env = global.env || {}; +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(time) { + if (!time) return '--/--'; + var m = Math.floor(time / 60); + var s = Math.floor(time % 60); + if (m === 0 && s === 0) return '--/--'; + return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s); +} + +function sizeFormate(bytes) { + if (typeof bytes === 'string') bytes = parseFloat(bytes.replace(/[^0-9.]/g, '')); + var n = parseFloat(bytes); + if (isNaN(n) || n < 0) return '0.00MB'; + var mb = n / (1024 * 1024); + return mb.toFixed(2) + 'MB'; +} + +// ===== 搜索实现(使用稳定版searchAll API) ===== +function musicSearch(str, page, limit) { + var searchSwitch = encodeURIComponent('{"song":1,"album":0,"singer":0,"tagSong":0,"mvSong":0,"songlist":0,"bestShow":1}'); + var url = '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'; + 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.songResultData && res.body.songResultData.result) { + return res.body.songResultData.result; + } + if (res.body && res.body.musics) return res.body.musics; + throw new Error('mg search failed: ' + JSON.stringify(res.body).slice(0, 200)); + }); +} + +function handleSearchResult(rawList) { + var list = []; + if (!rawList) return list; + for (var i = 0; i < rawList.length; i++) { + var item = rawList[i]; + var qualities = {}; + if (item.newRateFormats) { + for (var j = 0; j < item.newRateFormats.length; j++) { + var fmt = item.newRateFormats[j]; + if (fmt.formatType === 'LQ' && fmt.size) qualities['128k'] = sizeFormate(fmt.size); + if (fmt.formatType === 'HQ' && fmt.size) qualities['320k'] = sizeFormate(fmt.size); + if (fmt.formatType === 'SQ' && fmt.size) qualities['flac'] = sizeFormate(fmt.size); + } + } else if (item.rateFormats) { + for (var j = 0; j < item.rateFormats.length; j++) { + var fmt = item.rateFormats[j]; + if (fmt.formatType === 'LQ' && fmt.size) qualities['128k'] = sizeFormate(fmt.size); + if (fmt.formatType === 'HQ' && fmt.size) qualities['320k'] = sizeFormate(fmt.size); + if (fmt.formatType === 'SQ' && fmt.size) qualities['flac'] = sizeFormate(fmt.size); + } + } + + var picUrl = item.img || (item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : ''); + if (picUrl && picUrl.indexOf('/') === 0) picUrl = 'https://d.musicapp.migu.cn' + picUrl; + + var intervalStr = item.length ? String(item.length).replace(/.*(\d\d:\d\d)$/, '$1') : '--/--'; + if (!intervalStr) intervalStr = '--/--'; + + list.push({ + id: String(item.copyrightId || item.id || ''), + name: item.title || item.songName || '', + artists: item.artist || item.singerName || '', + source: 'mg', + pic: picUrl, mPic: picUrl, sPic: picUrl, + albumName: item.album || item.albumName || '', + albumId: String(item.albumId || ''), + interval: intervalStr, + qualities: qualities + }); + } + 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' }; + }).catch(function(err) { + if (retryNum < 3) return search(str, page, limit, retryNum); + return Promise.reject(err); + }); +} + +// ===== 热歌/提示搜索 ===== +function hotSearch() { + return Promise.resolve({ source: 'mg', list: [] }); +} + +function tipSearch(str) { + return Promise.resolve({ order: [], songs: [], artists: [], albums: [], playlists: [] }); +} + +// ===== getUrl(仅保留聆澜API) ===== +function getUrl(songId, quality) { + if (quality === 'standard') quality = '128k'; + else if (quality === 'exhigh') quality = '320k'; + else if (quality === 'lossless' || quality === 'hires' || quality === 'jymaster') quality = 'flac'; + songId = String(songId || ''); + quality = String(quality || '128k'); + var envNow = global.env || {}; + var ceruKey = envNow.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(quality), '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 getLrc(songId) { return Promise.resolve(''); } +function getPic(songId) { return Promise.resolve(''); } + +// ===== 插件信息 ===== +var pluginInfo = { + info: { + name: '咪咕音乐', + author: 'Koneko', + version: '0.1.5', + description: '咪咕音乐搜索 + 聆澜音源', + source: 'mg', + icon: 'https://m.music.migu.cn/favicon.ico' + }, + ext: { + name: '咪咕音乐', + type: 'music', + action: 'mg', + id: 'mg', + ext: 'mg', + active: true, + useMessage: { + success: '咪咕音乐已激活', + fail: '咪咕音乐激活失败', + loading: '咪咕音乐加载中...' + }, + description: { + basic: '咪咕音乐官方搜索 + 聆澜音源', + update: '修复搜索API稳定性', + author: 'Koneko' + }, + entry: { + js: 'Koneko_聆澜_咪咕音乐_v0.1.5_QZv2.js' + } + }, + env: [ + { name: 'ceru_key', description: '聆澜音源API密钥(可选)' } + ], + quality: { + '128k': { name: '标准音质', size: '' }, + '320k': { name: '高品音质', size: '' }, + 'flac': { name: '无损音质', size: '' } + }, + supportFunc: ['search_song', 'search_playlist', 'playlist', 'album', 'lyric'] +}; + +module.exports = { + musicSearch: { search: search }, + tipSearch: { search: tipSearch }, + hotSearch: { getList: hotSearch }, + songList: { getList: function() { return Promise.resolve([]); } }, + album: { getList: function() { return Promise.resolve([]); } }, + leaderboard: { getList: function() { return Promise.resolve([]); } }, + getUrl: getUrl, + getLrc: getLrc, + getPic: getPic, + pluginInfo: pluginInfo +}; diff --git a/聆澜/Koneko_聆澜_网易云音乐_v0.1.5_QZv2.js b/聆澜/Koneko_聆澜_网易云音乐_v0.1.5_QZv2.js new file mode 100644 index 0000000..847915d --- /dev/null +++ b/聆澜/Koneko_聆澜_网易云音乐_v0.1.5_QZv2.js @@ -0,0 +1,253 @@ +var env = global.env || {}; +var WY_COOKIE = env.cookie || ''; +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(time) { + if (!time) return '--/--'; + var m = Math.floor(time / 60); + var s = Math.floor(time % 60); + if (m === 0 && s === 0) return '--/--'; + return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s); +} + +function sizeFormate(bytes) { + if (typeof bytes === 'string') bytes = parseFloat(bytes.replace(/[^0-9.]/g, '')); + var n = parseFloat(bytes); + if (isNaN(n) || n < 0) return '0.00MB'; + var mb = n / (1024 * 1024); + return mb.toFixed(2) + 'MB'; +} + +// ===== 搜索实现(使用稳定版web API) ===== +function musicSearch(str, page, limit) { + var offset = limit * (page - 1); + var url = 'https://music.163.com/api/search/get/web?csrf_token=&hlposttag=&s=' + encodeURIComponent(str) + '&type=1&offset=' + offset + '&total=true&limit=' + limit; + return requestUrl(url, 'GET', { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Referer': 'https://music.163.com/', + 'Cookie': WY_COOKIE + }).then(function(res) { + if (res.body && res.body.code === 200 && res.body.result) return res.body.result; + if (res.body && res.body.result && res.body.result.songs) return res.body.result; + throw new Error('wy search failed: ' + JSON.stringify(res.body).slice(0, 200)); + }); +} + +function handleSearchResult(result) { + if (!result || !result.songs) return []; + var rawList = result.songs; + var list = []; + for (var i = 0; i < rawList.length; i++) { + var item = rawList[i]; + var qualities = {}; + if (item.l && item.l.size) qualities['128k'] = sizeFormate(item.l.size); + if (item.m && item.m.size) qualities['128k'] = sizeFormate(item.m.size); + if (item.h && item.h.size) qualities['320k'] = sizeFormate(item.h.size); + if (item.sq && item.sq.size) qualities['flac'] = sizeFormate(item.sq.size); + if (item.hr && item.hr.size) qualities['hires'] = sizeFormate(item.hr.size); + + var picUrl = ''; + if (item.album && item.album.picUrl) picUrl = item.album.picUrl; + else if (item.al && item.al.picUrl) picUrl = item.al.picUrl; + + var artists = []; + if (item.artists) { + for (var j = 0; j < item.artists.length; j++) { + if (item.artists[j].name) artists.push(item.artists[j].name); + } + } else if (item.ar) { + for (var j = 0; j < item.ar.length; j++) { + if (item.ar[j].name) artists.push(item.ar[j].name); + } + } + + list.push({ + id: String(item.id), + name: item.name || '', + artists: artists.join('、'), + source: 'wy', + pic: picUrl, mPic: picUrl, sPic: picUrl, + albumName: (item.album && item.album.name) || (item.al && item.al.name) || '', + albumId: String((item.album && item.album.id) || (item.al && item.al.id) || ''), + interval: String(formatPlayTime((item.duration || item.dt || 0) / 1000) || '--/--'), + qualities: qualities + }); + } + 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) { + var list = handleSearchResult(result); + var total = result.songCount || list.length * 10; + return { list: list, allPage: Math.ceil(total / limit), limit: limit, total: total, source: 'wy' }; + }).catch(function(err) { + if (retryNum < 3) return search(str, page, limit, retryNum); + return Promise.reject(err); + }); +} + +// ===== 热歌/提示搜索 ===== +function hotSearch() { + return requestUrl('https://music.163.com/weapi/search/hot', 'POST', { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cookie': WY_COOKIE + }, querystring.stringify({ type: 1111 })).then(function(res) { + if (res.body && res.body.data) return { source: 'wy', list: res.body.data }; + return { source: 'wy', 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 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cookie': WY_COOKIE + }, querystring.stringify({ s: str })).then(function(res) { + if (res.body && res.body.result) return res.body.result; + return { order: [], songs: [], artists: [], albums: [], playlists: [] }; + }).catch(function() { return { order: [], songs: [], artists: [], albums: [], playlists: [] }; }); +} + +// ===== getUrl(仅保留聆澜API) ===== +function getUrl(songId, quality) { + if (quality === 'standard') quality = '128k'; + else if (quality === 'exhigh') quality = '320k'; + else if (quality === 'lossless' || quality === 'hires' || quality === 'jymaster') quality = 'flac'; + songId = String(songId || ''); + quality = String(quality || '128k'); + var envNow = global.env || {}; + var ceruKey = envNow.ceru_key || ''; + var wyCookie = envNow.cookie || ''; + var headers = { 'User-Agent': 'QZMusic/2.0' }; + if (ceruKey) headers['X-API-Key'] = ceruKey; + if (wyCookie) headers['Cookie'] = wyCookie; + return requestUrl('https://source.shiqianjiang.cn/api/music/url?source=wy&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(quality), '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 getLrc(songId) { return Promise.resolve(''); } +function getPic(songId) { return Promise.resolve(''); } + +// ===== 插件信息 ===== +var pluginInfo = { + info: { + name: '网易云音乐', + author: 'Koneko', + version: '0.1.5', + description: '网易云音乐搜索 + 聆澜音源', + source: 'wy', + icon: 'https://s1.music.126.net/style/favicon.ico' + }, + ext: { + name: '网易云音乐', + type: 'music', + action: 'wy', + id: 'wy', + ext: 'wy', + active: true, + useMessage: { + success: '网易云音乐已激活', + fail: '网易云音乐激活失败', + loading: '网易云音乐加载中...' + }, + description: { + basic: '网易云音乐官方搜索 + 聆澜音源', + update: '修复搜索API稳定性', + author: 'Koneko' + }, + entry: { + js: 'Koneko_聆澜_网易云音乐_v0.1.5_QZv2.js' + } + }, + env: [ + { name: 'ceru_key', description: '聆澜音源API密钥(可选)' }, + { name: 'cookie', description: '网易云Cookie,用于搜索/每日推荐/私人FM/我喜欢的音乐等' }, + { name: 'playlist_url', description: '网易云个人主页链接,用于获取个人歌单' } + ], + quality: { + '128k': { name: '标准音质', size: '' }, + '320k': { name: '高品音质', size: '' }, + 'flac': { name: '无损音质', size: '' }, + 'hires': { name: 'Hi-Res', size: '' } + }, + supportFunc: ['search_song', 'search_playlist', 'playlist', 'album', 'lyric'] +}; + +module.exports = { + musicSearch: { search: search }, + tipSearch: { search: tipSearch }, + hotSearch: { getList: hotSearch }, + songList: { getList: function() { return Promise.resolve([]); } }, + album: { getList: function() { return Promise.resolve([]); } }, + leaderboard: { getList: function() { return Promise.resolve([]); } }, + getUrl: getUrl, + getLrc: getLrc, + getPic: getPic, + pluginInfo: pluginInfo +}; diff --git a/聆澜/Koneko_聆澜_酷狗音乐_v0.1.5_QZv2.js b/聆澜/Koneko_聆澜_酷狗音乐_v0.1.5_QZv2.js new file mode 100644 index 0000000..89721a0 --- /dev/null +++ b/聆澜/Koneko_聆澜_酷狗音乐_v0.1.5_QZv2.js @@ -0,0 +1,222 @@ +var env = global.env || {}; +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(time) { + if (!time) return '--/--'; + var m = Math.floor(time / 60); + var s = Math.floor(time % 60); + if (m === 0 && s === 0) return '--/--'; + return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s); +} + +function sizeFormate(bytes) { + if (typeof bytes === 'string') bytes = parseFloat(bytes.replace(/[^0-9.]/g, '')); + var n = parseFloat(bytes); + if (isNaN(n) || n < 0) return '0.00MB'; + var mb = n / (1024 * 1024); + return mb.toFixed(2) + 'MB'; +} + +function stripEm(s) { + if (!s) return ''; + return String(s).replace(/<\/?em>/g, '').replace(/ /g, ' ').replace(/&/g, '&').trim(); +} + +// ===== 搜索实现(使用稳定版API) ===== +function musicSearch(str, page, limit) { + var url = 'http://mobilecdn.kugou.com/api/v3/search/song?format=json&keyword=' + encodeURIComponent(str) + '&page=' + page + '&pagesize=' + limit; + return requestUrl(url, 'GET', { + 'User-Agent': 'Mozilla/5.0 (Linux; Android 10)', + 'Referer': 'http://m.kugou.com/' + }).then(function(res) { + if (res.body && res.body.data && res.body.data.info) return res.body.data.info; + throw new Error('kg search failed: ' + JSON.stringify(res.body).slice(0, 200)); + }); +} + +function handleSearchResult(rawList) { + var list = []; + if (!rawList) return list; + for (var i = 0; i < rawList.length; i++) { + var item = rawList[i]; + var qualities = {}; + if (item.filesize) qualities['128k'] = sizeFormate(item.filesize); + if (item['320filesize']) qualities['320k'] = sizeFormate(item['320filesize']); + if (item.sqfilesize) qualities['flac'] = sizeFormate(item.sqfilesize); + + var picUrl = ''; + if (item.imgurl && typeof item.imgurl === 'string' && item.imgurl.indexOf('http') === 0) { + picUrl = item.imgurl.replace('{size}', '400'); + } + if (!picUrl && item.album_img) picUrl = item.album_img; + if (!picUrl && item.img) picUrl = item.img; + + list.push({ + id: String(item.hash || item.audio_id || ''), + name: stripEm(item.songname || item.song_name || ''), + artists: stripEm(item.singername || item.singer_name || ''), + source: 'kg', + pic: picUrl, mPic: picUrl, sPic: picUrl, + albumName: stripEm(item.album_name || ''), + albumId: String(item.album_id || ''), + interval: String(formatPlayTime(item.duration) || '--/--'), + qualities: qualities + }); + } + 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' }; + }).catch(function(err) { + if (retryNum < 3) return search(str, page, limit, retryNum); + return Promise.reject(err); + }); +} + +// ===== 热歌/提示搜索 ===== +function hotSearch() { + return Promise.resolve({ source: 'kg', list: [] }); +} + +function tipSearch(str) { + return Promise.resolve({ order: [], songs: [], artists: [], albums: [], playlists: [] }); +} + +// ===== getUrl(仅保留聆澜API) ===== +function getUrl(songId, quality) { + if (quality === 'standard') quality = '128k'; + else if (quality === 'exhigh') quality = '320k'; + else if (quality === 'lossless' || quality === 'hires' || quality === 'jymaster') quality = 'flac'; + songId = String(songId || ''); + quality = String(quality || '128k'); + var envNow = global.env || {}; + var ceruKey = envNow.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(quality), '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 getLrc(songId) { return Promise.resolve(''); } +function getPic(songId) { return Promise.resolve(''); } + +// ===== 插件信息 ===== +var pluginInfo = { + info: { + name: '酷狗音乐', + author: 'Koneko', + version: '0.1.5', + description: '酷狗音乐搜索 + 聆澜音源', + source: 'kg', + icon: 'https://www.kugou.com/favicon.ico' + }, + ext: { + name: '酷狗音乐', + type: 'music', + action: 'kg', + id: 'kg', + ext: 'kg', + active: true, + useMessage: { + success: '酷狗音乐已激活', + fail: '酷狗音乐激活失败', + loading: '酷狗音乐加载中...' + }, + description: { + basic: '酷狗音乐官方搜索 + 聆澜音源', + update: '修复搜索API稳定性', + author: 'Koneko' + }, + entry: { + js: 'Koneko_聆澜_酷狗音乐_v0.1.5_QZv2.js' + } + }, + env: [ + { name: 'ceru_key', description: '聆澜音源API密钥(可选)' } + ], + quality: { + '128k': { name: '标准音质', size: '' }, + '320k': { name: '高品音质', size: '' }, + 'flac': { name: '无损音质', size: '' } + }, + supportFunc: ['search_song', 'search_playlist', 'playlist', 'album', 'lyric'] +}; + +module.exports = { + musicSearch: { search: search }, + tipSearch: { search: tipSearch }, + hotSearch: { getList: hotSearch }, + songList: { getList: function() { return Promise.resolve([]); } }, + album: { getList: function() { return Promise.resolve([]); } }, + leaderboard: { getList: function() { return Promise.resolve([]); } }, + getUrl: getUrl, + getLrc: getLrc, + getPic: getPic, + pluginInfo: pluginInfo +};