Files
Koneko_api_for_QZ-Music/Koneko_聆澜_QQ音乐_v0.0.3_QZv2.js

325 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
var https = require('https');
var http = require('http');
var crypto = require('crypto');
var querystring = require('querystring');
// ===== 通用HTTP请求 =====
function doRequest(protocol, options, postData) {
return new Promise(function(resolve, reject) {
var client = protocol === 'https' ? https : http;
var req = client.request(options, function(res) {
var chunks = [];
res.on('data', function(chunk) { chunks.push(chunk); });
res.on('end', function() {
var buf = Buffer.concat(chunks);
var body;
try {
body = JSON.parse(buf.toString('utf8'));
} catch (e) {
body = buf.toString('utf8');
}
resolve({ body: body, statusCode: res.statusCode, headers: res.headers });
});
});
req.on('error', function(err) { reject(err); });
req.setTimeout(15000, function() { req.destroy(); reject(new Error('timeout')); });
if (postData) req.write(postData);
req.end();
});
}
function requestUrl(urlStr, method, headers, body) {
var parsed = new URL(urlStr);
var protocol = parsed.protocol.replace(':', '');
var options = {
hostname: parsed.hostname,
port: parsed.port || (protocol === 'https' ? 443 : 80),
path: parsed.pathname + parsed.search,
method: method || 'GET',
headers: headers || {}
};
var postData = null;
if (body) {
if (typeof body === 'string') {
postData = Buffer.from(body, 'utf8');
} else if (Buffer.isBuffer(body)) {
postData = body;
} else {
postData = Buffer.from(JSON.stringify(body), 'utf8');
if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json';
}
if (postData) options.headers['Content-Length'] = postData.length;
}
return doRequest(protocol, options, postData);
}
// ===== QQ音乐zzcSign =====
var PART_1_INDEXES = [23, 14, 6, 36, 16, 40, 7, 19];
var PART_2_INDEXES = [16, 1, 32, 12, 19, 27, 8, 5];
var SCRAMBLE_VALUES = [89, 39, 179, 150, 218, 82, 58, 252, 177, 52, 186, 123, 120, 64, 242, 133, 143, 161, 121, 179];
function hashSHA1(text) {
return crypto.createHash('sha1').update(Buffer.from(text, 'utf-8')).digest().toString('hex').toUpperCase();
}
function pickHashByIdx(hash, indexes) {
var out = '';
for (var i = 0; i < indexes.length; i++) out += hash[indexes[i]];
return out;
}
function base64Encode(data) {
var buf;
if (typeof data === 'string') buf = Buffer.from(data);
else if (Array.isArray(data)) buf = Buffer.from(data);
else buf = data;
return buf.toString('base64').replace(/[\\/+=]/g, '');
}
function zzcSign(text) {
var hash = hashSHA1(text);
var part1 = pickHashByIdx(hash, PART_1_INDEXES);
var part2 = pickHashByIdx(hash, PART_2_INDEXES);
var part3 = [];
for (var i = 0; i < SCRAMBLE_VALUES.length; i++) {
part3.push(SCRAMBLE_VALUES[i] ^ parseInt(hash.slice(i * 2, i * 2 + 2), 16));
}
var b64Part = base64Encode(part3).replace(/[\\/+=]/g, '');
return ('zzc' + part1 + b64Part + part2).toLowerCase();
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getSearchId() {
var e = randomInt(1, 20);
var t = Number(e * Number('18014398509481984').toFixed());
var n = randomInt(0, 4194304) * 4294967296;
var a = Date.now();
var r = Math.round(a * 1000) % (24 * 60 * 60 * 1000);
return String(t + n + r);
}
function signRequest(data) {
var sign = zzcSign(JSON.stringify(data));
return requestUrl('https://u.y.qq.com/cgi-bin/musics.fcg?sign=' + encodeURIComponent(sign), 'POST', {
'User-Agent': 'QQMusic 14090508(android 12)',
'Content-Type': 'application/json'
}, JSON.stringify(data));
}
function formatPlayTime(seconds) {
if (!seconds) return '00:00';
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
// ===== 搜索实现 =====
function musicSearch(str, page, limit) {
var body = {
comm: { ct: '11', cv: '14090508', v: '14090508', tmeAppID: 'qqmusic', phonetype: 'EBG-AN10', deviceScore: '553.47', devicelevel: '50', newdevicelevel: '20', rom: 'HuaWei/EMOTION/EmotionUI_14.2.0', os_ver: '12', OpenUDID: '0', OpenUDID2: '0', QIMEI36: '0', udid: '0', chid: '0', aid: '0', oaid: '0', taid: '0', tid: '0', wid: '0', uid: '0', sid: '0', modeSwitch: '6', teenMode: '0', ui_mode: '2', nettype: '1020', v4ip: '' },
req: { module: 'music.search.SearchCgiService', method: 'DoSearchForQQMusicMobile', param: { search_type: 0, searchid: getSearchId(), query: str, page_num: page, num_per_page: limit, highlight: 0, nqc_flag: 0, multi_zhida: 0, cat: 2, grp: 1, sin: 0, sem: 0 } }
};
return signRequest(body).then(function(res) {
if (res.body && res.body.code === 0 && res.body.req && res.body.req.code === 0) return res.body.req.data;
throw new Error('tx search failed');
});
}
function handleSearchResult(rawList) {
var list = [];
for (var i = 0; i < rawList.length; i++) {
var item = rawList[i];
if (!item.file || !item.file.media_mid) continue;
var qualities = {};
if (item.file.size_128mp3) qualities.standard = String(item.file.size_128mp3);
if (item.file.size_320mp3) qualities.exhigh = String(item.file.size_320mp3);
if (item.file.size_flac) qualities.lossless = String(item.file.size_flac);
if (item.file.size_hires) qualities.hires = String(item.file.size_hires);
var albumId = '';
var albumName = '';
if (item.album) { albumName = item.album.name; albumId = item.album.mid; }
var singerName = '';
if (item.singer && item.singer.length) {
var ns = [];
for (var j = 0; j < item.singer.length; j++) ns.push(item.singer[j].name);
singerName = ns.join('、');
}
var picUrl = 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: item.mid,
name: item.name + (item.title_extra || ''),
artists: singerName,
source: 'tx',
pic: picUrl,
mPic: picUrl,
sPic: picUrl,
albumName: albumName,
albumId: albumId,
interval: formatPlayTime(item.interval),
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(data) {
if (!data || !data.body || !data.body.item_song) return search(str, page, limit, retryNum);
var list = handleSearchResult(data.body.item_song);
var meta = data.meta || {};
var total = meta.estimate_sum || 0;
return { list: list, allPage: Math.ceil(total / limit), limit: limit, total: total, source: 'tx' };
});
}
// ===== 播放链接 =====
function getUrl(songInfo, type) {
var q = type || '128k';
var songId = songInfo.id || '';
var ceruKey = process.env.ceru_key || '';
var headers = { 'User-Agent': 'QZMusic/2.0' };
if (ceruKey) headers['X-API-Key'] = ceruKey;
return requestUrl('https://source.shiqianjiang.cn/api/music/url?source=tx&songId=' + encodeURIComponent(songId) + '&quality=' + encodeURIComponent(q), 'GET', headers)
.then(function(res) {
if (res.body && typeof res.body === 'object' && res.body.url) return res.body.url;
if (res.body && typeof res.body === 'object' && res.body.data && res.body.data.url) return res.body.data.url;
if (typeof res.body === 'string' && res.body.indexOf('http') === 0) return res.body;
return '';
}).catch(function() { return ''; });
}
// ===== 封面图 =====
function getPic(songInfo) {
if (songInfo.pic) return Promise.resolve(songInfo.pic);
var albumId = songInfo.albumId || '';
if (albumId) return Promise.resolve('https://y.gtimg.cn/music/photo_new/T002R500x500M000' + albumId + '.jpg');
return Promise.resolve('');
}
// ===== 歌词 =====
function getLyric(songInfo) {
var songmid = songInfo.id || '';
return requestUrl('https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?format=json&nobase64=1&songmid=' + encodeURIComponent(songmid), 'GET', {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)',
'Referer': 'https://y.qq.com/portal/player.html'
}).then(function(res) {
if (typeof res.body === 'string') {
try { var obj = JSON.parse(res.body.replace(/^MusicJsonCallback\(/, '').replace(/\)$/, '')); return obj.lyric || ''; } catch(e) {}
}
if (res.body && res.body.lyric) return res.body.lyric;
return '';
}).catch(function() { return ''; });
}
// ===== 音乐详情 =====
function musicDetail(songmid) {
var data = {
comm: { ct: '11', cv: '14090508', v: '14090508', tmeAppID: 'qqmusic' },
req: { module: 'music.pf_song_detail_svr', method: 'get_song_detail_yqq', param: { song_mid: songmid } }
};
return signRequest(data).then(function(res) {
if (res.body && res.body.req && res.body.req.data && res.body.req.data.info && res.body.req.data.info.track_info) {
var t = res.body.req.data.info.track_info;
var albumId = t.album ? t.album.mid : '';
var singerName = '';
if (t.singer && t.singer.length) { var ns = []; for (var j = 0; j < t.singer.length; j++) ns.push(t.singer[j].name); singerName = ns.join('、'); }
var picUrl = albumId ? 'https://y.gtimg.cn/music/photo_new/T002R500x500M000' + albumId + '.jpg' : '';
return {
id: t.mid,
name: t.name,
artists: singerName,
source: 'tx',
pic: picUrl,
mPic: picUrl,
sPic: picUrl,
albumName: t.album ? t.album.name : '',
albumId: albumId,
interval: formatPlayTime(t.interval),
qualities: {}
};
}
return null;
}).catch(function() { return null; });
}
function musicInfo(songmid) { return musicDetail(songmid); }
// ===== 热搜 =====
function hotSearch() {
var data = {
comm: { ct: '19', cv: '1803', guid: '0', patch: '118', psrf_access_token_expiresAt: 0, psrf_qqaccess_token: '', psrf_qqopenid: '', psrf_qqunionid: '', tmeAppID: 'qqmusic', tmeLoginType: 0, uin: '0', wid: '0' },
hotkey: { method: 'GetHotkeyForQQMusicPC', module: 'tencent_musicsoso_hotkey.HotkeyService', param: { search_id: '', uin: 0 } }
};
return requestUrl('https://u.y.qq.com/cgi-bin/musicu.fcg', 'POST', {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)',
'Referer': 'https://y.qq.com/portal/player.html',
'Content-Type': 'application/json'
}, JSON.stringify(data)).then(function(res) {
var list = [];
if (res.body && res.body.hotkey && res.body.hotkey.data && res.body.hotkey.data.vec_hotkey) {
var arr = res.body.hotkey.data.vec_hotkey;
for (var i = 0; i < arr.length; i++) list.push(arr[i].query);
}
return { source: 'tx', list: list };
}).catch(function() { return { source: 'tx', list: [] }; });
}
// ===== 搜索建议 =====
function tipSearch(str) {
return Promise.resolve({ order: [], songs: [], artists: [], albums: [], playlists: [] });
}
// ===== 榜单 =====
function leaderboard() {
return Promise.resolve([]);
}
// ===== 歌单 =====
function songList() {
return Promise.resolve({ list: [], total: 0, page: 1, source: 'tx' });
}
// ===== 歌手 =====
function singer() {
return Promise.resolve({ source: 'tx', list: [], total: 0 });
}
// ===== 专辑 =====
function album() {
return Promise.resolve({ source: 'tx', list: [], total: 0 });
}
// ===== 插件信息 =====
var pluginInfo = {
info: {
id: 'koneko_ceru_tx',
name: 'QQ音乐 - Koneko 聆澜',
version: '0.0.3',
source: 'tx',
description: 'QQ音乐音源基于 CeruMusic musicSdk 搜索 + Ceru 播放接口'
},
ext: [],
env: [{ key: 'ceru_key', name: 'Ceru API Key', description: '聆澜播放接口密钥' }],
quality: [
{ name: '标准', ui: '标', id: '128k' },
{ name: '高品', ui: 'HQ', id: '320k' },
{ name: '无损', ui: 'SQ', id: 'flac' }
],
supportFunc: []
}
module.exports = {
musicSearch: { search: search },
tipSearch: { search: tipSearch },
hotSearch: { getList: hotSearch },
leaderboard: { getBoards: leaderboard },
songList: songList,
artists: singer,
album: album,
getLyric: getLyric,
getPic: getPic,
getUrl: getUrl,
musicDetail: musicDetail,
musicInfo: musicInfo,
pluginInfo: pluginInfo
};