diff --git a/src/renderer/utils/musicSdk/kw/album.js b/src/renderer/utils/musicSdk/kw/album.js index 79e641fc..aef82a3b 100644 --- a/src/renderer/utils/musicSdk/kw/album.js +++ b/src/renderer/utils/musicSdk/kw/album.js @@ -1,6 +1,6 @@ import { httpFetch } from '../../request' import { decodeName } from '../../index' -import { formatSinger, objStr2JSON } from './util' +import { formatSinger, objStr2JSON } from './utils' // let requestObj_list export default { diff --git a/src/renderer/utils/musicSdk/kw/index.js b/src/renderer/utils/musicSdk/kw/index.js index bdc5e796..567c85cb 100644 --- a/src/renderer/utils/musicSdk/kw/index.js +++ b/src/renderer/utils/musicSdk/kw/index.js @@ -1,7 +1,7 @@ import { httpFetch } from '../../request' import tipSearch from './tipSearch' import musicSearch from './musicSearch' -import { formatSinger } from './util' +import { formatSinger } from './utils' import leaderboard from './leaderboard' import lyric from './lyric' import pic from './pic' diff --git a/src/renderer/utils/musicSdk/kw/leaderboard.js b/src/renderer/utils/musicSdk/kw/leaderboard.js index 5756ea72..b4c932f8 100644 --- a/src/renderer/utils/musicSdk/kw/leaderboard.js +++ b/src/renderer/utils/musicSdk/kw/leaderboard.js @@ -1,6 +1,8 @@ import { httpFetch } from '../../request' import { formatPlayTime, decodeName } from '../../index' -import { formatSinger } from './util' +import { sortQualityArray } from '../utils' +import { formatSinger } from './utils' +import { wbdCrypto } from './utils/crypto' const boardList = [{ id: 'kw__93', name: '飙升榜', bangid: '93' }, { id: 'kw__17', name: '新歌榜', bangid: '17' }, { id: 'kw__16', name: '热歌榜', bangid: '16' }, { id: 'kw__158', name: '抖音热歌榜', bangid: '158' }, { id: 'kw__292', name: '铃声榜', bangid: '292' }, { id: 'kw__284', name: '热评榜', bangid: '284' }, { id: 'kw__290', name: 'ACG新歌榜', bangid: '290' }, { id: 'kw__286', name: '台湾KKBOX榜', bangid: '286' }, { id: 'kw__279', name: '冬日暖心榜', bangid: '279' }, { id: 'kw__281', name: '巴士随身听榜', bangid: '281' }, { id: 'kw__255', name: 'KTV点唱榜', bangid: '255' }, { id: 'kw__280', name: '家务进行曲榜', bangid: '280' }, { id: 'kw__282', name: '熬夜修仙榜', bangid: '282' }, { id: 'kw__283', name: '枕边轻音乐榜', bangid: '283' }, { id: 'kw__278', name: '古风音乐榜', bangid: '278' }, { id: 'kw__264', name: 'Vlog音乐榜', bangid: '264' }, { id: 'kw__242', name: '电音榜', bangid: '242' }, { id: 'kw__187', name: '流行趋势榜', bangid: '187' }, { id: 'kw__204', name: '现场音乐榜', bangid: '204' }, { id: 'kw__186', name: 'ACG神曲榜', bangid: '186' }, { id: 'kw__185', name: '最强翻唱榜', bangid: '185' }, { id: 'kw__26', name: '经典怀旧榜', bangid: '26' }, { id: 'kw__104', name: '华语榜', bangid: '104' }, { id: 'kw__182', name: '粤语榜', bangid: '182' }, { id: 'kw__22', name: '欧美榜', bangid: '22' }, { id: 'kw__184', name: '韩语榜', bangid: '184' }, { id: 'kw__183', name: '日语榜', bangid: '183' }, { id: 'kw__145', name: '会员畅听榜', bangid: '145' }, { id: 'kw__153', name: '网红新歌榜', bangid: '153' }, { id: 'kw__64', name: '影视金曲榜', bangid: '64' }, { id: 'kw__176', name: 'DJ嗨歌榜', bangid: '176' }, { id: 'kw__106', name: '真声音', bangid: '106' }, { id: 'kw__12', name: 'Billboard榜', bangid: '12' }, { id: 'kw__49', name: 'iTunes音乐榜', bangid: '49' }, { id: 'kw__180', name: 'beatport电音榜', bangid: '180' }, { id: 'kw__13', name: '英国UK榜', bangid: '13' }, { id: 'kw__164', name: '百大DJ榜', bangid: '164' }, { id: 'kw__246', name: 'YouTube音乐排行榜', bangid: '246' }, { id: 'kw__265', name: '韩国Genie榜', bangid: '265' }, { id: 'kw__14', name: '韩国M-net榜', bangid: '14' }, { id: 'kw__8', name: '香港电台榜', bangid: '8' }, { id: 'kw__15', name: '日本公信榜', bangid: '15' }, { id: 'kw__151', name: '腾讯音乐人原创榜', bangid: '151' }] @@ -62,9 +64,9 @@ export default { bangid: 183, }, ], - getUrl: (p, l, id) => `http://kbangserver.kuwo.cn/ksong.s?from=pc&fmt=json&pn=${p - 1}&rn=${l}&type=bang&data=content&id=${id}&show_copyright_off=0&pcmp4=1&isbang=1`, + // getUrl: (p, l, id) => `http://kbangserver.kuwo.cn/ksong.s?from=pc&fmt=json&pn=${p - 1}&rn=${l}&type=bang&data=content&id=${id}&show_copyright_off=0&pcmp4=1&isbang=1`, regExps: { - + mInfo: /level:(\w+),bitrate:(\d+),format:(\w+),size:([\w.]+)/, }, limit: 100, _requestBoardsObj: null, @@ -74,62 +76,51 @@ export default { this._requestBoardsObj = httpFetch('http://qukudata.kuwo.cn/q.k?op=query&cont=tree&node=2&pn=0&rn=1000&fmt=json&level=2') return this._requestBoardsObj.promise }, - getData(url) { - const requestDataObj = httpFetch(url) - return requestDataObj.promise - }, filterData(rawList) { - // console.log(rawList) - // console.log(rawList.length, rawList2.length) - return rawList.map((item, inedx) => { - let formats = item.formats.split('|') + return rawList.map(item => { let types = [] - let _types = {} - if (formats.includes('MP3128')) { - types.push({ type: '128k', size: null }) - _types['128k'] = { - size: null, + const _types = {} + const qualitys = new Set() + + item.n_minfo.split(';').forEach(i => { + const info = i.match(this.regExps.mInfo) + if (!info) return + + const quality = info[2] + const size = info[4].toLocaleUpperCase() + + if (qualitys.has(quality)) return + qualitys.add(quality) + + switch (quality) { + case '4000': + types.push({ type: 'flac24bit', size }) + _types.flac24bit = { size } + break + case '2000': + types.push({ type: 'flac', size }) + _types.flac = { size } + break + case '320': + types.push({ type: '320k', size }) + _types['320k'] = { size } + break + case '128': + types.push({ type: '128k', size }) + _types['128k'] = { size } + break } - } - // if (formats.includes('MP3192')) { - // types.push({ type: '192k', size: null }) - // _types['192k'] = { - // size: null, - // } - // } - if (formats.includes('MP3H')) { - types.push({ type: '320k', size: null }) - _types['320k'] = { - size: null, - } - } - // if (formats.includes('AL')) { - // types.push({ type: 'ape', size: null }) - // _types.ape = { - // size: null, - // } - // } - if (formats.includes('ALFLAC')) { - types.push({ type: 'flac', size: null }) - _types.flac = { - size: null, - } - } - if (formats.includes('HIRFLAC')) { - types.push({ type: 'flac24bit', size: null }) - _types.flac24bit = { - size: null, - } - } - // types.reverse() + }) + types = sortQualityArray(types) + return { singer: formatSinger(decodeName(item.artist)), name: decodeName(item.name), albumName: decodeName(item.album), - albumId: item.albumid, + albumId: item.albumId, songmid: item.id, source: 'kw', - interval: formatPlayTime(parseInt(item.song_duration)), + interval: formatPlayTime(parseInt(item.duration)), img: item.pic, lrc: null, otherSource: null, @@ -180,12 +171,19 @@ export default { getList(id, page, retryNum = 0) { if (++retryNum > 3) return Promise.reject(new Error('try max num')) - return this.getData(this.getUrl(page, this.limit, id)).then(({ statusCode, body }) => { - // console.log(body) - if (statusCode !== 200 || !body.musiclist) return this.getList(id, page, retryNum) - // console.log(data1.musiclist, data2.data) - let total = parseInt(body.num) - let list = this.filterData(body.musiclist) + + const requestBody = { uid: '', devId: '', sFrom: 'kuwo_sdk', user_type: 'AP', carSource: 'kwplayercar_ar_6.0.1.0_apk_keluze.apk', id, pn: page - 1, rn: this.limit } + const requestUrl = `https://wbd.kuwo.cn/api/bd/bang/bang_info?${wbdCrypto.buildParam(requestBody)}` + const request = httpFetch(requestUrl).promise + + return request.then(({ statusCode, body }) => { + const rawData = wbdCrypto.decodeData(body) + const data = rawData.data + if (statusCode !== 200 || rawData.code != 200 || !data.musiclist) return this.getList(id, page, retryNum) + + const total = parseInt(data.total) + const list = this.filterData(data.musiclist) + return { total, list, diff --git a/src/renderer/utils/musicSdk/kw/lyric.js b/src/renderer/utils/musicSdk/kw/lyric.js index 6e94db26..0d20c644 100644 --- a/src/renderer/utils/musicSdk/kw/lyric.js +++ b/src/renderer/utils/musicSdk/kw/lyric.js @@ -1,5 +1,5 @@ import { httpFetch } from '../../request' -import { decodeLyric, lrcTools } from './util' +import lyricTools from './utils/lrc' import { decodeName } from '../../index' /* @@ -198,7 +198,7 @@ export default { time, text, }) - } else if (lrcTools.rxps.tagLine.test(line)) { + } else if (lyricTools.rxps.tagLine.test(line)) { tags.push(line) } } @@ -212,7 +212,7 @@ export default { // const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`) // requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => { // if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body))) - // return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => { + // return lyricTools.decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => { // let lrcInfo // console.log(Buffer.from(base64Data, 'base64').toString()) // try { @@ -220,8 +220,8 @@ export default { // } catch { // return Promise.reject(new Error('Get lyric failed')) // } - // if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '') - // lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric) + // if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lyricTools.rxps.wordTimeAll, '') + // lrcInfo.lxlyric = lyricTools.parse(lrcInfo.lyric) // // console.log(lrcInfo.lyric) // // console.log(lrcInfo.tlyric) // // console.log(lrcInfo.lxlyric) @@ -235,7 +235,7 @@ export default { const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`) requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => { if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body))) - return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => { + return lyricTools.decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => { // let lrcInfo // try { // lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString()) @@ -250,13 +250,13 @@ export default { return Promise.reject(new Error('Get lyric failed')) } // console.log(lrcInfo) - if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '') + if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lyricTools.rxps.wordTimeAll, '') try { - lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric) + lrcInfo.lxlyric = lyricTools.parse(lrcInfo.lyric) } catch { lrcInfo.lxlyric = '' } - lrcInfo.lyric = lrcInfo.lyric.replace(lrcTools.rxps.wordTimeAll, '') + lrcInfo.lyric = lrcInfo.lyric.replace(lyricTools.rxps.wordTimeAll, '') if (!existTimeExp.test(lrcInfo.lyric)) return Promise.reject(new Error('Get lyric failed')) // console.log(lrcInfo) return lrcInfo diff --git a/src/renderer/utils/musicSdk/kw/musicSearch.js b/src/renderer/utils/musicSdk/kw/musicSearch.js index fdcb3789..e2c3f80f 100644 --- a/src/renderer/utils/musicSdk/kw/musicSearch.js +++ b/src/renderer/utils/musicSdk/kw/musicSearch.js @@ -3,7 +3,7 @@ import { httpFetch } from '../../request' import { formatPlayTime, decodeName } from '../../index' // import { debug } from '../../utils/env' -import { formatSinger } from './util' +import { formatSinger } from './utils' export default { regExps: { diff --git a/src/renderer/utils/musicSdk/kw/songList.js b/src/renderer/utils/musicSdk/kw/songList.js index ac24630a..5f8a462b 100644 --- a/src/renderer/utils/musicSdk/kw/songList.js +++ b/src/renderer/utils/musicSdk/kw/songList.js @@ -1,6 +1,6 @@ import { httpFetch } from '../../request' import { formatPlayTime, decodeName } from '../../index' -import { formatSinger, objStr2JSON } from './util' +import { formatSinger, objStr2JSON } from './utils' import album from './album' export default { diff --git a/src/renderer/utils/musicSdk/kw/utils/crypto.js b/src/renderer/utils/musicSdk/kw/utils/crypto.js new file mode 100644 index 00000000..17e5fd4e --- /dev/null +++ b/src/renderer/utils/musicSdk/kw/utils/crypto.js @@ -0,0 +1,48 @@ +import { createCipheriv, createDecipheriv, createHash } from 'crypto' + +const createAesEncrypt = (buffer, mode, key, iv) => { + const cipher = createCipheriv(mode, key, iv) + return Buffer.concat([cipher.update(buffer), cipher.final()]) +} + +const createAesDecrypt = (buffer, mode, key, iv) => { + const cipher = createDecipheriv(mode, key, iv) + return Buffer.concat([cipher.update(buffer), cipher.final()]) +} + +const createMD5 = str => createHash('md5').update(str).digest('hex') + +const strToUint8Array = str => { + const length = Math.floor(str.length / 2) + const bArr = new Uint8Array(length) + for (let i = 0; i < length; i++) { + const i2 = i * 2 + bArr[i] = parseInt(str.substring(i2, i2 + 2), 16) + } + return bArr +} + +export const wbdCrypto = { + aesMode: 'aes-128-ecb', + aesKey: strToUint8Array('7057273DC7FA29BF39442D72DD5E8CE4'), + aesIv: '', + appId: 'y67sprxhhpws', + decodeData(base64Result) { + const data = Buffer.from(decodeURIComponent(base64Result), 'base64') + return JSON.parse(createAesDecrypt(data, this.aesMode, this.aesKey, this.aesIv).toString()) + }, + createSign(data, time) { + const str = `${this.appId}${data}${time}` + return createMD5(str).toUpperCase() + }, + buildParam(jsonData) { + const data = Buffer.from(JSON.stringify(jsonData)) + const time = Date.now() + + const encodeData = createAesEncrypt(data, this.aesMode, this.aesKey, this.aesIv).toString('base64') + const sign = this.createSign(encodeData, time) + + return `data=${encodeURIComponent(encodeData)}&time=${time}&appId=${this.appId}&sign=${sign}` + }, +} + diff --git a/src/renderer/utils/musicSdk/kw/utils/index.js b/src/renderer/utils/musicSdk/kw/utils/index.js new file mode 100644 index 00000000..122f7a7b --- /dev/null +++ b/src/renderer/utils/musicSdk/kw/utils/index.js @@ -0,0 +1,73 @@ +export const objStr2JSON = str => JSON.parse(str.replace(/('(?=(,\s*')))|('(?=:))|((?<=([:,]\s*))')|((?<={)')|('(?=}))/g, '"')) + +export const formatSinger = rawData => rawData.replace(/&/g, '、') + +export const matchToken = headers => { + try { + return headers['set-cookie'][0].match(/kw_token=(\w+)/)[1] + } catch (err) { + return null + } +} + +// const kw_token = { +// token: null, +// isGetingToken: false, +// } + +// export const getToken = (retryNum = 0) => new Promise((resolve, reject) => { +// if (retryNum > 2) return Promise.reject(new Error('try max num')) + +// if (kw_token.isGetingToken) return wait(1000).then(() => getToken(retryNum).then(token => resolve(token))) +// if (kw_token.token) return resolve(kw_token.token) +// kw_token.isGetingToken = true +// httpGet('http://www.kuwo.cn/', (err, resp) => { +// kw_token.isGetingToken = false +// if (err) return getToken(++retryNum) +// if (resp.statusCode != 200) return reject(new Error('获取失败')) +// const token = kw_token.token = matchToken(resp.headers) +// resolve(token) +// }) +// }) + +// export const tokenRequest = async(url, options = {}) => { +// let token = kw_token.token +// if (!token) token = await getToken() +// if (!options.headers) { +// options.headers = { +// Referer: 'http://www.kuwo.cn/', +// csrf: token, +// cookie: 'kw_token=' + token, +// } +// } +// const requestObj = httpFetch(url, options) +// requestObj.promise = requestObj.promise.then(resp => { +// // console.log(resp) +// if (resp.statusCode == 200) { +// kw_token.token = matchToken(resp.headers) +// } +// return resp +// }) +// return requestObj +// } + +// const translationMap = { +// "{'": '{"', +// "'}\n": '"}', +// "'}": '"}', +// "':'": '":"', +// "','": '","', +// "':{'": '":{"', +// "':['": '":["', +// "'}],'": '"}],"', +// "':[{'": '":[{"', +// "'},'": '"},"', +// "'},{'": '"},{"', +// "':[],'": '":[],"', +// "':{},'": '":{},"', +// "'}]}": '"}]}', +// } + +// export const objStr2JSON = str => { +// return JSON.parse(str.replace(/(^{'|'}\n$|'}$|':'|','|':\[{'|'}\],'|':{'|'},'|'},{'|':\['|':\[\],'|':{},'|'}]})/g, s => translationMap[s])) +// } diff --git a/src/renderer/utils/musicSdk/kw/util.js b/src/renderer/utils/musicSdk/kw/utils/lrc.js similarity index 55% rename from src/renderer/utils/musicSdk/kw/util.js rename to src/renderer/utils/musicSdk/kw/utils/lrc.js index 2ad4c619..3d2d161d 100644 --- a/src/renderer/utils/musicSdk/kw/util.js +++ b/src/renderer/utils/musicSdk/kw/utils/lrc.js @@ -1,90 +1,7 @@ -// import { httpGet, httpFetch } from '../../request' import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames' import { rendererInvoke } from '@common/rendererIpc' -// const kw_token = { -// token: null, -// isGetingToken: false, -// } - -// const translationMap = { -// "{'": '{"', -// "'}\n": '"}', -// "'}": '"}', -// "':'": '":"', -// "','": '","', -// "':{'": '":{"', -// "':['": '":["', -// "'}],'": '"}],"', -// "':[{'": '":[{"', -// "'},'": '"},"', -// "'},{'": '"},{"', -// "':[],'": '":[],"', -// "':{},'": '":{},"', -// "'}]}": '"}]}', -// } - -// export const objStr2JSON = str => { -// return JSON.parse(str.replace(/(^{'|'}\n$|'}$|':'|','|':\[{'|'}\],'|':{'|'},'|'},{'|':\['|':\[\],'|':{},'|'}]})/g, s => translationMap[s])) -// } - -export const objStr2JSON = str => { - return JSON.parse(str.replace(/('(?=(,\s*')))|('(?=:))|((?<=([:,]\s*))')|((?<={)')|('(?=}))/g, '"')) -} - - -export const formatSinger = rawData => rawData.replace(/&/g, '、') - -export const matchToken = headers => { - try { - return headers['set-cookie'][0].match(/kw_token=(\w+)/)[1] - } catch (err) { - return null - } -} - -// const wait = time => new Promise(resolve => setTimeout(() => resolve(), time)) - - -// export const getToken = (retryNum = 0) => new Promise((resolve, reject) => { -// if (retryNum > 2) return Promise.reject(new Error('try max num')) - -// if (kw_token.isGetingToken) return wait(1000).then(() => getToken(retryNum).then(token => resolve(token))) -// if (kw_token.token) return resolve(kw_token.token) -// kw_token.isGetingToken = true -// httpGet('http://www.kuwo.cn/', (err, resp) => { -// kw_token.isGetingToken = false -// if (err) return getToken(++retryNum) -// if (resp.statusCode != 200) return reject(new Error('获取失败')) -// const token = kw_token.token = matchToken(resp.headers) -// resolve(token) -// }) -// }) - -export const decodeLyric = base64Data => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, base64Data) - -// export const tokenRequest = async(url, options = {}) => { -// let token = kw_token.token -// if (!token) token = await getToken() -// if (!options.headers) { -// options.headers = { -// Referer: 'http://www.kuwo.cn/', -// csrf: token, -// cookie: 'kw_token=' + token, -// } -// } -// const requestObj = httpFetch(url, options) -// requestObj.promise = requestObj.promise.then(resp => { -// // console.log(resp) -// if (resp.statusCode == 200) { -// kw_token.token = matchToken(resp.headers) -// } -// return resp -// }) -// return requestObj -// } - -export const lrcTools = { +export default { rxps: { wordLine: /^(\[\d{1,2}:.*\d{1,4}\])\s*(\S+(?:\s+\S+)*)?\s*/, tagLine: /\[(ver|ti|ar|al|offset|by|kuwo):\s*(\S+(?:\s+\S+)*)\s*\]/, @@ -96,6 +13,7 @@ export const lrcTools = { isOK: false, lines: [], tags: [], + decodeLyric: base64Data => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, base64Data), getWordInfo(str, str2, prevWord) { const offset = parseInt(str) const offset2 = parseInt(str2) diff --git a/src/renderer/utils/musicSdk/utils.js b/src/renderer/utils/musicSdk/utils.js index 0bbf6325..a489f7ec 100644 --- a/src/renderer/utils/musicSdk/utils.js +++ b/src/renderer/utils/musicSdk/utils.js @@ -48,3 +48,32 @@ export const formatSingerName = (singers, nameKey = 'name', join = '、') => { } return decodeName(String(singers ?? '')) } + +/** + * 音质排序 + * @param {*} array + */ +export const sortQualityArray = array => { + const qualityMap = { + flac24bit: 4, + flac: 3, + '320k': 2, + '128k': 1, + } + const rawQualityArray = [] + const newQualityArray = [] + + array.forEach((item, index) => { + const type = qualityMap[item.type] + if (!type) return + rawQualityArray.push({ type, index }) + }) + + rawQualityArray.sort((a, b) => a.type - b.type) + + rawQualityArray.forEach(item => { + newQualityArray.push(array[item.index]) + }) + + return newQualityArray +}