Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d10f45fac9 | ||
|
|
0918766e69 | ||
|
|
e58beff507 | ||
|
|
abc700ecc4 | ||
|
|
b019b0e296 | ||
|
|
2c5b21e869 | ||
|
|
89ab94b276 | ||
|
|
89d0c1557c | ||
|
|
85bb259e36 | ||
|
|
6025f74a6d |
14
README.md
14
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` 以获取更高音质。
|
||||
|
||||
## 版权声明
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
var env = global.env || {};
|
||||
var CERU_KEY = env.ceru_key || '';
|
||||
var https = require('https');
|
||||
var http = require('http');
|
||||
var crypto = require('crypto');
|
||||
@@ -102,7 +101,7 @@ function fetchAllGitMusic() {
|
||||
cachedLyricMap = lyricMap;
|
||||
cacheTime = now;
|
||||
return list;
|
||||
});
|
||||
}).catch(function() { return []; });
|
||||
}
|
||||
|
||||
function formatGitItem(item) {
|
||||
@@ -123,9 +122,7 @@ function formatGitItem(item) {
|
||||
return {
|
||||
id: String(songId),
|
||||
name: item.title || item.filename || '',
|
||||
artists: item.artist || '未知歌手',
|
||||
source: 'git',
|
||||
pic: picUrl,
|
||||
artists: item.artist || '未知歌手', pic: picUrl,
|
||||
mPic: picUrl,
|
||||
sPic: picUrl,
|
||||
albumName: item.album || '未知专辑',
|
||||
@@ -155,21 +152,21 @@ function handleSearchResult(result) {
|
||||
}
|
||||
|
||||
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' };
|
||||
});
|
||||
return Promise.resolve({ list: [], allPage: 1, limit: limit || 30, total: 0, source: 'git' });
|
||||
}
|
||||
|
||||
// ===== 播放链接 =====
|
||||
function getUrl(songId, quality) {
|
||||
// Map QZ v2 quality names to Ceru API format
|
||||
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');
|
||||
if (!songId) return Promise.resolve('');
|
||||
var ceruKey = CERU_KEY || '';
|
||||
// Read env FRESH each call (dynamic refresh)
|
||||
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=git&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(quality), 'GET', headers)
|
||||
@@ -227,16 +224,17 @@ var pluginInfo = {
|
||||
info: {
|
||||
id: 'koneko_ceru_git',
|
||||
name: 'GIT音源 - Koneko 聆澜',
|
||||
version: '0.1.2',
|
||||
source: 'git',
|
||||
description: 'GIT音源,基于 GitCode audio_database.json 本地搜索 + Ceru 播放接口'
|
||||
description: 'GIT音源,基于 GitCode audio_database.json 本地搜索 + Ceru 播放接口',
|
||||
version: '0.1.4'
|
||||
},
|
||||
ext: [],
|
||||
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜播放接口密钥' }],
|
||||
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜付费密钥。留空=公益模式(仅128k);填写=付费模式(高音质)。修改后下次播放自动生效,也可点扩展按钮激活' }],
|
||||
quality: [
|
||||
{ name: '标准音质', ui: '标', id: '128k' },
|
||||
{ name: '高品音质', ui: 'HQ', id: '320k' },
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' }
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' },
|
||||
{ name: '高解析度无损(Hi-Res)', ui: 'HR', id: 'hires' },
|
||||
{ name: '至臻母带', ui: 'MT', id: 'master' }
|
||||
],
|
||||
supportFunc: ['search_song', 'search_playlist', 'playlist', 'album', 'lyric']
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
var env = global.env || {};
|
||||
var CERU_KEY = env.ceru_key || '';
|
||||
var https = require('https');
|
||||
var http = require('http');
|
||||
var crypto = require('crypto');
|
||||
@@ -129,6 +128,41 @@ function signRequest(data) {
|
||||
}, JSON.stringify(data));
|
||||
}
|
||||
|
||||
// ===== 批量音质信息 =====
|
||||
function getBatchMusicQualityInfo(songList) {
|
||||
var songIds = songList.map(function(item) { return item.id; });
|
||||
var body = {
|
||||
comm: { ct: '19', cv: '1859', uin: '0' },
|
||||
req: {
|
||||
module: 'music.trackInfo.UniformRuleCtrl',
|
||||
method: 'CgiGetTrackInfo',
|
||||
param: { types: songIds.map(function(){ return 1; }), ids: songIds, ctx: 0 }
|
||||
}
|
||||
};
|
||||
return requestUrl('https://u.y.qq.com/cgi-bin/musicu.fcg', 'POST', {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
|
||||
'Content-Type': 'application/json'
|
||||
}, JSON.stringify(body)).then(function(res) {
|
||||
if (!res.body || res.body.code !== 0) return {};
|
||||
var qualityInfoMap = {};
|
||||
var tracks = (res.body.req && res.body.req.data && res.body.req.data.tracks) ? res.body.req.data.tracks : [];
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
var track = tracks[i];
|
||||
var file = track.file || {};
|
||||
var qualities = {};
|
||||
if (file.size_128mp3) qualities['128k'] = sizeFormate(file.size_128mp3);
|
||||
if (file.size_320mp3) qualities['320k'] = sizeFormate(file.size_320mp3);
|
||||
if (file.size_flac) qualities['flac'] = sizeFormate(file.size_flac);
|
||||
if (file.size_hires) qualities['hires'] = sizeFormate(file.size_hires);
|
||||
if (file.size_new && file.size_new[0]) qualities['master'] = sizeFormate(file.size_new[0]);
|
||||
if (file.size_new && file.size_new[1]) qualities['atmos'] = sizeFormate(file.size_new[1]);
|
||||
if (file.size_new && file.size_new[2]) qualities['atmos_plus'] = sizeFormate(file.size_new[2]);
|
||||
qualityInfoMap[track.id] = { qualities: qualities };
|
||||
}
|
||||
return qualityInfoMap;
|
||||
}).catch(function() { return {}; });
|
||||
}
|
||||
|
||||
// ===== 搜索实现 =====
|
||||
function musicSearch(str, page, limit) {
|
||||
var body = {
|
||||
@@ -142,38 +176,31 @@ function musicSearch(str, page, limit) {
|
||||
}
|
||||
|
||||
function handleSearchResult(rawList) {
|
||||
var list = [];
|
||||
if (!rawList) return list;
|
||||
for (var i = 0; i < rawList.length; i++) {
|
||||
var item = rawList[i];
|
||||
if (!item.file || !item.file.media_mid) continue;
|
||||
var qualities = {};
|
||||
var file = item.file;
|
||||
if (file.size_128mp3) qualities['128k'] = sizeFormate(file.size_128mp3);
|
||||
if (file.size_320mp3) qualities['320k'] = sizeFormate(file.size_320mp3);
|
||||
if (file.size_flac) qualities['flac'] = sizeFormate(file.size_flac);
|
||||
var albumId = '';
|
||||
var albumName = '';
|
||||
if (item.album) { albumName = item.album.name; albumId = item.album.mid; }
|
||||
var singerName = formatSingerName(item.singer, 'name');
|
||||
var picUrl = albumId && 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' : '');
|
||||
list.push({
|
||||
id: String(item.mid),
|
||||
name: item.name + (item.title_extra || ''),
|
||||
artists: singerName,
|
||||
source: 'tx',
|
||||
pic: picUrl,
|
||||
mPic: picUrl,
|
||||
sPic: picUrl,
|
||||
albumName: albumName,
|
||||
albumId: String(albumId || ''),
|
||||
interval: String(formatPlayTime(item.interval) || '--/--'),
|
||||
qualities: qualities
|
||||
});
|
||||
}
|
||||
return list;
|
||||
if (!rawList || rawList.length === 0) return Promise.resolve([]);
|
||||
return getBatchMusicQualityInfo(rawList).then(function(qualityInfoMap) {
|
||||
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 = (albumId === '' || albumId === '空')
|
||||
? (item.singer && item.singer.length ? 'https://y.gtimg.cn/music/photo_new/T001R500x500M000' + item.singer[0].mid + '.jpg' : '')
|
||||
: 'https://y.gtimg.cn/music/photo_new/T002R500x500M000' + albumId + '.jpg';
|
||||
var qInfo = qualityInfoMap[item.id] || {};
|
||||
list.push({
|
||||
id: String(item.mid),
|
||||
name: item.name + (item.title_extra || ''),
|
||||
artists: formatSingerName(item.singer, 'name'), pic: picUrl, mPic: picUrl, sPic: picUrl,
|
||||
albumName: albumName,
|
||||
albumId: String(albumId || ''),
|
||||
interval: String(formatPlayTime(item.interval) || '--/--'),
|
||||
qualities: qInfo.qualities || {}
|
||||
});
|
||||
}
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
function search(str, page, limit, retryNum) {
|
||||
@@ -182,18 +209,25 @@ function search(str, page, limit, retryNum) {
|
||||
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' };
|
||||
return handleSearchResult(data.body.item_song).then(function(list) {
|
||||
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(songId, quality) {
|
||||
// Map QZ v2 quality names to Ceru API format
|
||||
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 ceruKey = CERU_KEY || '';
|
||||
// Read env FRESH each call (dynamic refresh)
|
||||
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=tx&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(quality), 'GET', headers)
|
||||
@@ -243,9 +277,7 @@ function musicDetail(songmid) {
|
||||
return {
|
||||
id: String(t.mid),
|
||||
name: t.name,
|
||||
artists: singerName,
|
||||
source: 'tx',
|
||||
pic: picUrl,
|
||||
artists: singerName, pic: picUrl,
|
||||
mPic: picUrl,
|
||||
sPic: picUrl,
|
||||
albumName: t.album ? t.album.name : '',
|
||||
@@ -264,7 +296,7 @@ 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 } }
|
||||
hotkey: { method: 'GetHotkeyForQQMusicMobile', 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)',
|
||||
@@ -310,16 +342,19 @@ var pluginInfo = {
|
||||
info: {
|
||||
id: 'koneko_ceru_tx',
|
||||
name: 'QQ音乐 - Koneko 聆澜',
|
||||
version: '0.1.2',
|
||||
source: 'tx',
|
||||
description: 'QQ音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
|
||||
description: 'QQ音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口',
|
||||
version: '0.1.4'
|
||||
},
|
||||
ext: [],
|
||||
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜播放接口密钥' }],
|
||||
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜付费密钥。留空=公益模式(仅128k);填写=付费模式(高音质)。修改后下次播放自动生效,也可点扩展按钮激活' }],
|
||||
quality: [
|
||||
{ name: '标准音质', ui: '标', id: '128k' },
|
||||
{ name: '高品音质', ui: 'HQ', id: '320k' },
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' }
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' },
|
||||
{ name: '高解析度无损(Hi-Res)', ui: 'HR', id: 'hires' },
|
||||
{ name: '至臻母带', ui: 'MT', id: 'master' },
|
||||
{ name: '全景声', ui: 'AT', id: 'atmos' },
|
||||
{ name: '全景声+', ui: 'AT+', id: 'atmos_plus' }
|
||||
],
|
||||
supportFunc: ['search_song', 'search_playlist', 'playlist', 'album', 'lyric']
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
var env = global.env || {};
|
||||
var CERU_KEY = env.ceru_key || '';
|
||||
var https = require('https');
|
||||
var http = require('http');
|
||||
var crypto = require('crypto');
|
||||
@@ -74,14 +73,15 @@ function sizeFormate(bytes) {
|
||||
}
|
||||
|
||||
// ===== 搜索实现 =====
|
||||
function stripEm(s) {
|
||||
if (!s) return '';
|
||||
return String(s).replace(/<\/?em>/g, '').replace(/ /g, ' ').replace(/&/g, '&').trim();
|
||||
}
|
||||
|
||||
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;
|
||||
var url = 'https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/search_all.do?ua=Android_migu&version=5.0.1&text=' + encodeURIComponent(str) + '&pageNo=' + page + '&pageSize=' + limit + '&searchSwitch=%7B%22song%22%3A1%7D';
|
||||
return requestUrl(url, 'GET', { 'User-Agent': 'Mozilla/5.0 (Linux; Android 10)' }).then(function(res) {
|
||||
if (res.body && res.body.code === '000000' && res.body.songResultData && res.body.songResultData.result) return res.body.songResultData.result;
|
||||
throw new Error('mg search failed');
|
||||
});
|
||||
}
|
||||
@@ -95,26 +95,33 @@ function handleSearchResult(rawList) {
|
||||
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);
|
||||
if (fmt.formatType === 'LQ') qualities['128k'] = sizeFormate(fmt.size);
|
||||
else if (fmt.formatType === 'PQ') qualities['320k'] = sizeFormate(fmt.size);
|
||||
else if (fmt.formatType === 'SQ') qualities['flac'] = sizeFormate(fmt.size);
|
||||
else if (fmt.formatType === 'ZQ') qualities['hires'] = 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 = '--/--';
|
||||
var picUrl = '';
|
||||
if (item.imgItems && item.imgItems.length) picUrl = item.imgItems[0].img || '';
|
||||
var singerName = '';
|
||||
if (item.singers && item.singers.length) {
|
||||
var ns = [];
|
||||
for (var k = 0; k < item.singers.length; k++) ns.push(item.singers[k].name);
|
||||
singerName = ns.join('、');
|
||||
}
|
||||
var albumName = '';
|
||||
var albumId = '';
|
||||
if (item.albums && item.albums.length) {
|
||||
albumName = item.albums[0].name || '';
|
||||
albumId = String(item.albums[0].id || '');
|
||||
}
|
||||
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,
|
||||
name: stripEm(item.name || ''),
|
||||
artists: stripEm(singerName), pic: picUrl, mPic: picUrl, sPic: picUrl,
|
||||
albumName: stripEm(albumName),
|
||||
albumId: albumId,
|
||||
interval: '--/--',
|
||||
qualities: qualities
|
||||
});
|
||||
}
|
||||
@@ -128,14 +135,20 @@ function search(str, page, limit, retryNum) {
|
||||
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() { return { list: [], allPage: 1, limit: limit, total: 0, source: 'mg' }; });
|
||||
}
|
||||
|
||||
// ===== 播放链接 =====
|
||||
function getUrl(songId, quality) {
|
||||
// Map QZ v2 quality names to Ceru API format
|
||||
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 ceruKey = CERU_KEY || '';
|
||||
// Read env FRESH each call (dynamic refresh)
|
||||
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)
|
||||
@@ -195,16 +208,17 @@ var pluginInfo = {
|
||||
info: {
|
||||
id: 'koneko_ceru_mg',
|
||||
name: '咪咕音乐 - Koneko 聆澜',
|
||||
version: '0.1.2',
|
||||
source: 'mg',
|
||||
description: '咪咕音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
|
||||
description: '咪咕音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口',
|
||||
version: '0.1.4'
|
||||
},
|
||||
ext: [],
|
||||
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜播放接口密钥' }],
|
||||
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜付费密钥。留空=公益模式(仅128k);填写=付费模式(高音质)。修改后下次播放自动生效,也可点扩展按钮激活' }],
|
||||
quality: [
|
||||
{ name: '标准音质', ui: '标', id: '128k' },
|
||||
{ name: '高品音质', ui: 'HQ', id: '320k' },
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' }
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' },
|
||||
{ name: '高解析度无损(Hi-Res)', ui: 'HR', id: 'hires' },
|
||||
{ name: '至臻母带', ui: 'MT', id: 'master' }
|
||||
],
|
||||
supportFunc: ['search_song', 'search_playlist', 'playlist', 'album', 'lyric']
|
||||
}
|
||||
233
聆澜/Koneko_聆澜_咪咕音乐_v0.1.5_QZv2.js
Normal file
233
聆澜/Koneko_聆澜_咪咕音乐_v0.1.5_QZv2.js
Normal file
@@ -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
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
var env = global.env || {};
|
||||
var CERU_KEY = env.ceru_key || '';
|
||||
var WY_COOKIE = env.cookie || '';
|
||||
var https = require('https');
|
||||
var http = require('http');
|
||||
@@ -142,23 +141,18 @@ function formatPlayCount(count) {
|
||||
return String(count);
|
||||
}
|
||||
|
||||
// ===== 搜索实现 =====
|
||||
// ===== 搜索实现(老式API,无需加密) =====
|
||||
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',
|
||||
'Cookie': WY_COOKIE
|
||||
}, querystring.stringify(form)).then(function(res) {
|
||||
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',
|
||||
'Referer': 'https://music.163.com',
|
||||
'Cookie': WY_COOKIE || 'os=pc; appver=2.10.0;'
|
||||
}).then(function(res) {
|
||||
if (typeof res.body === 'string') {
|
||||
try { res.body = JSON.parse(res.body); } catch (e) { return null; }
|
||||
}
|
||||
return res.body;
|
||||
});
|
||||
}
|
||||
@@ -167,25 +161,32 @@ 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 s = rawList[i];
|
||||
var qualities = {};
|
||||
if (s.l && s.l.size) qualities['128k'] = sizeFormate(s.l.size);
|
||||
if (s.h && s.h.size) qualities['320k'] = sizeFormate(s.h.size);
|
||||
if (s.sq && s.sq.size) qualities['flac'] = sizeFormate(s.sq.size);
|
||||
var picUrl = s.al && s.al.picUrl ? s.al.picUrl : '';
|
||||
if (s.hr && s.hr.size) qualities['hires'] = sizeFormate(s.hr.size);
|
||||
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) : '';
|
||||
// 封面:picId 拼接 URL(老式API不返回picUrl)
|
||||
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: String(s.id),
|
||||
name: s.name,
|
||||
artists: formatSingerName(s.ar, 'name'),
|
||||
source: 'wy',
|
||||
pic: picUrl,
|
||||
mPic: picUrl,
|
||||
sPic: picUrl,
|
||||
albumName: s.al && s.al.name ? s.al.name : '',
|
||||
albumId: String(s.al && s.al.id ? s.al.id : ''),
|
||||
interval: String(formatPlayTime((s.dt || 0) / 1000) || '--/--'),
|
||||
id: s.id ? String(s.id) : '',
|
||||
name: s.name || '',
|
||||
artists: artists.join('、'), pic: pic, mPic: pic, sPic: pic,
|
||||
albumName: album,
|
||||
albumId: albumId,
|
||||
interval: formatPlayTime(s.duration),
|
||||
qualities: qualities
|
||||
});
|
||||
}
|
||||
@@ -196,18 +197,15 @@ 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'
|
||||
};
|
||||
return musicSearch(str, page, limit).then(function(res) {
|
||||
if (!res || !res.result || !res.result.songs) {
|
||||
return { list: [], allPage: 1, limit: limit, total: 0, source: 'wy' };
|
||||
}
|
||||
var list = handleSearchResult(res.result.songs);
|
||||
var total = res.result.songCount || 0;
|
||||
return { list: list, allPage: Math.ceil(total / limit), limit: limit, total: total, source: 'wy' };
|
||||
}).catch(function() {
|
||||
return { list: [], allPage: 1, limit: limit, total: 0, source: 'wy' };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,9 +213,13 @@ function search(str, page, limit, retryNum) {
|
||||
function getUrl(songId, quality) {
|
||||
songId = String(songId || '');
|
||||
quality = String(quality || '128k');
|
||||
var ceruKey = CERU_KEY || '';
|
||||
// Read env FRESH each call (dynamic refresh)
|
||||
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;
|
||||
@@ -275,9 +277,7 @@ function musicDetail(songmid) {
|
||||
return {
|
||||
id: String(s.id),
|
||||
name: s.name,
|
||||
artists: formatSingerName(s.ar, 'name'),
|
||||
source: 'wy',
|
||||
pic: picUrl,
|
||||
artists: formatSingerName(s.ar, 'name'), pic: picUrl,
|
||||
mPic: picUrl,
|
||||
sPic: picUrl,
|
||||
albumName: s.al && s.al.name ? s.al.name : '',
|
||||
@@ -375,9 +375,7 @@ function leaderboardGetList(id, page) {
|
||||
list.push({
|
||||
id: String(item.id),
|
||||
name: item.name,
|
||||
artists: formatSingerName(item.ar, 'name'),
|
||||
source: 'wy',
|
||||
pic: picUrl,
|
||||
artists: formatSingerName(item.ar, 'name'), pic: picUrl,
|
||||
mPic: picUrl,
|
||||
sPic: picUrl,
|
||||
albumName: item.al && item.al.name ? item.al.name : '',
|
||||
@@ -451,9 +449,7 @@ var songList = {
|
||||
img: p.coverImgUrl || '',
|
||||
play_count: formatPlayCount(p.playCount),
|
||||
total: p.trackCount || 0,
|
||||
desc: p.description || '',
|
||||
source: 'wy'
|
||||
});
|
||||
desc: p.description || '', });
|
||||
}
|
||||
return { list: list, total: res.body.total || 0, page: page, limit: limit, source: 'wy' };
|
||||
}).catch(function() { return { list: [], total: 0, page: page, limit: limit, source: 'wy' }; });
|
||||
@@ -477,9 +473,7 @@ var songList = {
|
||||
list.push({
|
||||
id: String(item.id),
|
||||
name: item.name,
|
||||
artists: formatSingerName(item.ar, 'name'),
|
||||
source: 'wy',
|
||||
pic: picUrl, mPic: picUrl, sPic: picUrl,
|
||||
artists: formatSingerName(item.ar, 'name'), pic: picUrl, mPic: picUrl, sPic: picUrl,
|
||||
albumName: item.al && item.al.name ? item.al.name : '',
|
||||
albumId: String(item.al && item.al.id ? item.al.id : ''),
|
||||
interval: String(formatPlayTime((item.dt || 0) / 1000) || '--/--'),
|
||||
@@ -488,9 +482,7 @@ var songList = {
|
||||
}
|
||||
return {
|
||||
list: list, page: page, limit: limit,
|
||||
total: playlist.trackIds ? playlist.trackIds.length : list.length,
|
||||
source: 'wy',
|
||||
info: { name: playlist.name, img: playlist.coverImgUrl, desc: playlist.description, author: playlist.creator && playlist.creator.nickname ? playlist.creator.nickname : '' }
|
||||
total: playlist.trackIds ? playlist.trackIds.length : list.length, info: { name: playlist.name, img: playlist.coverImgUrl, desc: playlist.description, author: playlist.creator && playlist.creator.nickname ? playlist.creator.nickname : '' }
|
||||
};
|
||||
}).catch(function() { return { list: [], total: 0, source: 'wy' }; });
|
||||
},
|
||||
@@ -624,22 +616,20 @@ var pluginInfo = {
|
||||
info: {
|
||||
id: 'koneko_ceru_wy',
|
||||
name: '网易云音乐 - Koneko 聆澜',
|
||||
version: '0.1.2',
|
||||
source: 'wy',
|
||||
description: '网易云音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
|
||||
description: '网易云音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口',
|
||||
version: '0.1.5'
|
||||
},
|
||||
ext: [
|
||||
{ type: 'homePage', url: 'https://music.163.com/', name: '网易云音乐主页', description: '网易云音乐主页', entry: '' },
|
||||
{ type: 'share', url: 'https://music.163.com/song/media/outer/url?id={id}.mp3', name: '外链分享', description: '外链分享', entry: '' }
|
||||
],
|
||||
ext: [],
|
||||
env: [
|
||||
{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜播放接口密钥' },
|
||||
{ key: 'cookie', name: '网易云Cookie', description: '网易云音乐登录Cookie(需包含MUSIC_U=),用于个人歌单、收藏等功能' }
|
||||
{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜付费密钥。留空=公益模式(仅128k);填写=付费模式(高音质)。修改后下次播放自动生效,也可点扩展按钮激活' },
|
||||
{ key: 'cookie', name: '网易云Cookie', description: '网易云音乐登录Cookie,格式: MUSIC_U=xxx; MUSIC_U 值从浏览器登录网易云后 F12→Application→Cookies 复制。用于个人歌单、收藏等功能' }
|
||||
],
|
||||
quality: [
|
||||
{ name: '标准音质', ui: '标', id: '128k' },
|
||||
{ name: '高品音质', ui: 'HQ', id: '320k' },
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' }
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' },
|
||||
{ name: '高解析度无损(Hi-Res)', ui: 'HR', id: 'hires' },
|
||||
{ name: '至臻母带', ui: 'MT', id: 'master' }
|
||||
],
|
||||
supportFunc: ['search_song', 'search_playlist', 'playlist', 'album', 'lyric']
|
||||
}
|
||||
253
聆澜/Koneko_聆澜_网易云音乐_v0.1.5_QZv2.js
Normal file
253
聆澜/Koneko_聆澜_网易云音乐_v0.1.5_QZv2.js
Normal file
@@ -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
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
var env = global.env || {};
|
||||
var CERU_KEY = env.ceru_key || '';
|
||||
var https = require('https');
|
||||
var http = require('http');
|
||||
var crypto = require('crypto');
|
||||
@@ -74,10 +73,22 @@ function sizeFormate(bytes) {
|
||||
}
|
||||
|
||||
// ===== 搜索实现 =====
|
||||
function stripEm(s) {
|
||||
if (!s) return '';
|
||||
return String(s).replace(/<\/?em>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/\\\\u0026/g, '&').trim();
|
||||
}
|
||||
|
||||
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;
|
||||
var url = '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';
|
||||
return requestUrl(url, 'GET', {
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)',
|
||||
'Referer': 'http://www.kuwo.cn/'
|
||||
}).then(function(res) {
|
||||
var body = res.body;
|
||||
if (typeof body === 'string') {
|
||||
try { body = JSON.parse(body.replace(/'/g, '"')); } catch (e) { body = null; }
|
||||
}
|
||||
if (body && body.abslist) return body;
|
||||
throw new Error('kw search failed');
|
||||
});
|
||||
}
|
||||
@@ -86,26 +97,46 @@ function handleSearchResult(rawList) {
|
||||
var list = [];
|
||||
if (!rawList) return list;
|
||||
for (var i = 0; i < rawList.length; i++) {
|
||||
var item = rawList[i];
|
||||
var info = rawList[i];
|
||||
var songId = (info.MUSICRID || '').replace('MUSIC_', '');
|
||||
if (!songId) songId = String(info.DC_TARGETID || info.rid || info.id || '');
|
||||
// 音质:从 N_MINFO 解析
|
||||
var qualities = {};
|
||||
var size128 = item.filesize || item.mp3size || item.mp3Size || 0;
|
||||
var size320 = item.sqfilesize || item.hqsize || item['320size'] || item['320filesize'] || 0;
|
||||
var sizeFlac = item.flacfilesize || item.flacsize || item.flacSize || 0;
|
||||
if (size128) qualities['128k'] = sizeFormate(size128);
|
||||
if (size320) qualities['320k'] = sizeFormate(size320);
|
||||
if (sizeFlac) qualities['flac'] = sizeFormate(sizeFlac);
|
||||
var picUrl = item.pic || item.albumpic || '';
|
||||
if (info.N_MINFO) {
|
||||
var parts = String(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['master'] = m[4];
|
||||
else if (m[2] === '4000') qualities['hires'] = m[4];
|
||||
else if (m[2] === '2000') qualities['flac'] = m[4];
|
||||
else if (m[2] === '320') qualities['320k'] = m[4];
|
||||
else if (m[2] === '128') qualities['128k'] = m[4];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 封面:web_albumpic_short 拼完整URL,fallback 用 ALBUMID,再 fallback 用 artistpic
|
||||
var picUrl = '';
|
||||
if (info.web_albumpic_short) {
|
||||
picUrl = 'https://img2.kuwo.cn/star/albumcover/' + info.web_albumpic_short;
|
||||
} else if (info.ALBUMID) {
|
||||
picUrl = 'https://img2.kuwo.cn/star/albumcover/300/' + info.ALBUMID + '.jpg';
|
||||
} else if (info.web_artistpic_short) {
|
||||
picUrl = 'https://img2.kuwo.cn/star/albumcover/' + info.web_artistpic_short;
|
||||
} else {
|
||||
picUrl = 'http://artistpicserver.kuwo.cn/pic.web?corp=kuwo&type=rid_pic&pictype=500&size=500&rid=MUSIC_' + songId;
|
||||
}
|
||||
var artistStr = stripEm(info.ARTIST || '').replace(/&/g, '、');
|
||||
var duration = parseInt(info.DURATION || '0', 10);
|
||||
list.push({
|
||||
id: String(item.rid || item.id || ''),
|
||||
name: item.name || '',
|
||||
artists: item.artist || '',
|
||||
source: 'kw',
|
||||
pic: picUrl,
|
||||
id: String(songId),
|
||||
name: stripEm(info.SONGNAME || ''),
|
||||
artists: artistStr, pic: picUrl,
|
||||
mPic: picUrl,
|
||||
sPic: picUrl,
|
||||
albumName: item.album || '',
|
||||
albumId: String(item.albumid || ''),
|
||||
interval: String(formatPlayTime(item.duration) || '--/--'),
|
||||
albumName: stripEm(info.ALBUM || ''),
|
||||
albumId: String(info.ALBUMID || ''),
|
||||
interval: isNaN(duration) ? '--/--' : String(formatPlayTime(duration) || '--/--'),
|
||||
qualities: qualities
|
||||
});
|
||||
}
|
||||
@@ -116,17 +147,25 @@ 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) {
|
||||
return musicSearch(str, page, limit).then(function(data) {
|
||||
var rawList = data.abslist || [];
|
||||
var total = parseInt(data.TOTAL || '0', 10) || rawList.length * 10;
|
||||
var list = handleSearchResult(rawList);
|
||||
return { list: list, allPage: page + 1, limit: limit, total: list.length * 10, source: 'kw' };
|
||||
});
|
||||
return { list: list, allPage: Math.ceil(total / limit), limit: limit, total: total, source: 'kw' };
|
||||
}).catch(function() { return { list: [], allPage: 1, limit: limit, total: 0, source: 'kw' }; });
|
||||
}
|
||||
|
||||
// ===== 播放链接 =====
|
||||
function getUrl(songId, quality) {
|
||||
// Map QZ v2 quality names to Ceru API format
|
||||
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 ceruKey = CERU_KEY || '';
|
||||
// Read env FRESH each call (dynamic refresh)
|
||||
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=kw&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(quality), 'GET', headers)
|
||||
@@ -185,16 +224,17 @@ var pluginInfo = {
|
||||
info: {
|
||||
id: 'koneko_ceru_kw',
|
||||
name: '酷我音乐 - Koneko 聆澜',
|
||||
version: '0.1.2',
|
||||
source: 'kw',
|
||||
description: '酷我音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
|
||||
description: '酷我音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口',
|
||||
version: '0.1.4'
|
||||
},
|
||||
ext: [],
|
||||
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜播放接口密钥' }],
|
||||
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜付费密钥。留空=公益模式(仅128k);填写=付费模式(高音质)。修改后下次播放自动生效,也可点扩展按钮激活' }],
|
||||
quality: [
|
||||
{ name: '标准音质', ui: '标', id: '128k' },
|
||||
{ name: '高品音质', ui: 'HQ', id: '320k' },
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' }
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' },
|
||||
{ name: '高解析度无损(Hi-Res)', ui: 'HR', id: 'hires' },
|
||||
{ name: '至臻母带', ui: 'MT', id: 'master' }
|
||||
],
|
||||
supportFunc: ['search_song', 'search_playlist', 'playlist', 'album', 'lyric']
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
var env = global.env || {};
|
||||
var CERU_KEY = env.ceru_key || '';
|
||||
var https = require('https');
|
||||
var http = require('http');
|
||||
var crypto = require('crypto');
|
||||
@@ -74,10 +73,15 @@ function sizeFormate(bytes) {
|
||||
}
|
||||
|
||||
// ===== 搜索实现 =====
|
||||
function stripEm(s) {
|
||||
if (!s) return '';
|
||||
return String(s).replace(/<\/?em>/g, '').replace(/ /g, ' ').replace(/&/g, '&').trim();
|
||||
}
|
||||
|
||||
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;
|
||||
if (res.body && res.body.data && res.body.data.info) return res.body.data.info;
|
||||
throw new Error('kg search failed');
|
||||
});
|
||||
}
|
||||
@@ -89,18 +93,22 @@ function handleSearchResult(rawList) {
|
||||
var item = rawList[i];
|
||||
var qualities = {};
|
||||
if (item.filesize) qualities['128k'] = sizeFormate(item.filesize);
|
||||
if (item.sqfilesize) qualities['320k'] = sizeFormate(item.sqfilesize);
|
||||
if (item.flacfilesize) qualities['flac'] = sizeFormate(item.flacfilesize);
|
||||
var picUrl = item.img || '';
|
||||
if (item['320filesize']) qualities['320k'] = sizeFormate(item['320filesize']);
|
||||
if (item.sqfilesize) qualities['flac'] = sizeFormate(item.sqfilesize);
|
||||
// 封面:imgurl 带 {size} 占位符
|
||||
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: item.songname || '',
|
||||
artists: item.singername || '',
|
||||
source: 'kg',
|
||||
pic: picUrl,
|
||||
name: stripEm(item.songname || item.song_name || ''),
|
||||
artists: stripEm(item.singername || item.singer_name || ''), pic: picUrl,
|
||||
mPic: picUrl,
|
||||
sPic: picUrl,
|
||||
albumName: item.album_name || '',
|
||||
albumName: stripEm(item.album_name || ''),
|
||||
albumId: String(item.album_id || ''),
|
||||
interval: String(formatPlayTime(item.duration) || '--/--'),
|
||||
qualities: qualities
|
||||
@@ -111,21 +119,27 @@ function handleSearchResult(rawList) {
|
||||
|
||||
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) {
|
||||
var rawList = data.info || [];
|
||||
var total = data.total || rawList.length * 10;
|
||||
return musicSearch(str, page, limit).then(function(rawList) {
|
||||
var list = handleSearchResult(rawList);
|
||||
return { list: list, allPage: Math.ceil(total / limit), limit: limit, total: total, source: 'kg' };
|
||||
return { list: list, allPage: page + 1, limit: limit, total: list.length * 10, source: 'kg' };
|
||||
}).catch(function() {
|
||||
if (retryNum < 1) return search(str, page, limit, 1);
|
||||
return { list: [], allPage: 1, limit: limit, total: 0, source: 'kg' };
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 播放链接 =====
|
||||
function getUrl(songId, quality) {
|
||||
// Map QZ v2 quality names to Ceru API format
|
||||
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 ceruKey = CERU_KEY || '';
|
||||
// Read env FRESH each call (dynamic refresh)
|
||||
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)
|
||||
@@ -195,16 +209,17 @@ var pluginInfo = {
|
||||
info: {
|
||||
id: 'koneko_ceru_kg',
|
||||
name: '酷狗音乐 - Koneko 聆澜',
|
||||
version: '0.1.2',
|
||||
source: 'kg',
|
||||
description: '酷狗音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
|
||||
description: '酷狗音乐音源,基于 CeruMusic musicSdk 搜索 + Ceru 播放接口',
|
||||
version: '0.1.4'
|
||||
},
|
||||
ext: [],
|
||||
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜播放接口密钥' }],
|
||||
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜付费密钥。留空=公益模式(仅128k);填写=付费模式(高音质)。修改后下次播放自动生效,也可点扩展按钮激活' }],
|
||||
quality: [
|
||||
{ name: '标准音质', ui: '标', id: '128k' },
|
||||
{ name: '高品音质', ui: 'HQ', id: '320k' },
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' }
|
||||
{ name: '无损音质', ui: 'SQ', id: 'flac' },
|
||||
{ name: '高解析度无损(Hi-Res)', ui: 'HR', id: 'hires' },
|
||||
{ name: '至臻母带', ui: 'MT', id: 'master' }
|
||||
],
|
||||
supportFunc: ['search_song', 'search_playlist', 'playlist', 'album', 'lyric']
|
||||
}
|
||||
222
聆澜/Koneko_聆澜_酷狗音乐_v0.1.5_QZv2.js
Normal file
222
聆澜/Koneko_聆澜_酷狗音乐_v0.1.5_QZv2.js
Normal file
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user