From bb8509a21cf6fc39499c293bcd9577304f54e0f1 Mon Sep 17 00:00:00 2001 From: lyswhut Date: Thu, 9 Jun 2022 13:03:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81mg=E6=BA=90=E9=80=90=E5=AD=97?= =?UTF-8?q?=E6=AD=8C=E8=AF=8D=E7=9A=84=E6=92=AD=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- publish/changeLog.md | 1 + src/renderer/store/modules/player.js | 1 + src/renderer/utils/music/mg/leaderboard.js | 2 + src/renderer/utils/music/mg/lyric.js | 118 ++++++++++++++++++++- src/renderer/utils/music/mg/mrc.js | 104 ++++++++++++++++++ src/renderer/utils/music/mg/musicSearch.js | 4 +- src/renderer/utils/music/mg/songList.js | 2 + 7 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 src/renderer/utils/music/mg/mrc.js diff --git a/publish/changeLog.md b/publish/changeLog.md index c6fa80ec..26c37c37 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -5,6 +5,7 @@ ### 优化 - 添加歌曲到“我的列表”时,若按住`ctrl`键(Mac对应`Command`),则不会自动关闭添加窗口,这对想要将同一首(一批)歌曲添加到多个列表时会很有用 +- 支持mg源逐字歌词的播放,感谢 @mozbugbox 提供的帮助 ### 修复 diff --git a/src/renderer/store/modules/player.js b/src/renderer/store/modules/player.js index 9da84e1f..6eb62b01 100644 --- a/src/renderer/store/modules/player.js +++ b/src/renderer/store/modules/player.js @@ -238,6 +238,7 @@ const actions = { switch (musicInfo.source) { case 'kg': case 'kw': + case 'mg': break default: return buildLyricInfo(lrcInfo, musicInfo) diff --git a/src/renderer/utils/music/mg/leaderboard.js b/src/renderer/utils/music/mg/leaderboard.js index caa35c90..10d38081 100644 --- a/src/renderer/utils/music/mg/leaderboard.js +++ b/src/renderer/utils/music/mg/leaderboard.js @@ -148,6 +148,8 @@ export default { img: item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : null, lrc: null, lrcUrl: item.lrcUrl, + mrcUrl: item.mrcUrl, + trcUrl: item.trcUrl, otherSource: null, types, _types, diff --git a/src/renderer/utils/music/mg/lyric.js b/src/renderer/utils/music/mg/lyric.js index 308ceaee..fc75e8e5 100644 --- a/src/renderer/utils/music/mg/lyric.js +++ b/src/renderer/utils/music/mg/lyric.js @@ -1,14 +1,112 @@ import { httpFetch } from '../../request' +import musicSearch from './musicSearch' +import { decrypt } from './mrc' + +const mrcTools = { + rxps: { + lineTime: /^\s*\[(\d+),\d+\]/, + wordTime: /\(\d+,\d+\)/, + wordTimeAll: /(\(\d+,\d+\))/g, + }, + parseLyric(str) { + str = str.replace(/\r/g, '') + const lines = str.split('\n') + const lxlrcLines = [] + const lrcLines = [] + + for (const line of lines) { + if (line.length < 6) continue + let result = this.rxps.lineTime.exec(line) + if (!result) continue + + const startTime = parseInt(result[1]) + let time = startTime + let ms = time % 1000 + time /= 1000 + let m = parseInt(time / 60).toString().padStart(2, '0') + time %= 60 + let s = parseInt(time).toString().padStart(2, '0') + time = `${m}:${s}.${ms}` + + let words = line.replace(this.rxps.lineTime, '') + + lrcLines.push(`[${time}]${words.replace(this.rxps.wordTimeAll, '')}`) + + let times = words.match(this.rxps.wordTimeAll) + if (!times) continue + times = times.map(time => { + const result = /\((\d+),(\d+)\)/.exec(time) + return `<${parseInt(result[1]) - startTime},${result[2]}>` + }) + const wordArr = words.split(this.rxps.wordTime) + const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('') + lxlrcLines.push(`[${time}]${newWords}`) + } + return { + lyric: lrcLines.join('\n'), + lxlyric: lxlrcLines.join('\n'), + } + }, + getText(url, tryNum = 0) { + const requestObj = httpFetch(url, { + headers: { + Referer: 'https://app.c.nf.migu.cn/', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36', + channel: '0146921', + }, + }) + return requestObj.promise.then(({ statusCode, body }) => { + if (statusCode == 200) return body + if (tryNum > 5 || statusCode == 404) return Promise.reject('歌词获取失败') + return this.getText(url, ++tryNum) + }) + }, + getMrc(url) { + return this.getText(url).then(text => { + return this.parseLyric(decrypt(text)) + }) + }, + getLrc(url) { + return this.getText(url).then(text => ({ lxlyric: '', lyric: text })) + }, + getTrc(url) { + if (!url) return Promise.resolve('') + return this.getText(url) + }, + getMusicInfo(songInfo) { + return songInfo.mrcUrl == null + ? musicSearch.search(`${songInfo.name} ${songInfo.singer || ''}`.trim(), 1, { limit: 25 }).then(({ list }) => { + const targetSong = list.find(s => s.songmid == songInfo.songmid) + return targetSong ? { lrcUrl: targetSong.lrcUrl, mrcUrl: targetSong.mrcUrl, trcUrl: targetSong.trcUrl } : Promise.reject('获取歌词失败') + }) + : Promise.resolve({ lrcUrl: songInfo.lrcUrl, mrcUrl: songInfo.mrcUrl, trcUrl: songInfo.trcUrl }) + }, + getLyric(songInfo) { + return { + promise: this.getMusicInfo(songInfo).then(info => { + let p + if (info.mrcUrl) p = this.getMrc(info.mrcUrl) + else if (info.lrcUrl) p = this.getLrc(info.lrcUrl) + if (p == null) return Promise.reject('获取歌词失败') + return Promise.all([p, this.getTrc(info.trcUrl)]).then(([lrcInfo, tlyric]) => { + lrcInfo.tlyric = tlyric + return lrcInfo + }) + }), + cancelHttp() {}, + } + }, +} export default { - getLyric(songInfo, tryNum = 0) { + getLyricWeb(songInfo, tryNum = 0) { // console.log(songInfo.copyrightId) if (songInfo.lrcUrl) { let requestObj = httpFetch(songInfo.lrcUrl) requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { if (statusCode !== 200) { if (tryNum > 5) return Promise.reject('歌词获取失败') - let tryRequestObj = this.getLyric(songInfo, ++tryNum) + let tryRequestObj = this.getLyricWeb(songInfo, ++tryNum) requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) return tryRequestObj.promise } @@ -19,15 +117,15 @@ export default { }) return requestObj } else { - let requestObj = httpFetch(`http://music.migu.cn/v3/api/music/audioPlayer/getLyric?copyrightId=${songInfo.copyrightId}`, { + let requestObj = httpFetch(`https://music.migu.cn/v3/api/music/audioPlayer/getLyric?copyrightId=${songInfo.copyrightId}`, { headers: { - Referer: 'http://music.migu.cn/v3/music/player/audio?from=migu', + Referer: 'https://music.migu.cn/v3/music/player/audio?from=migu', }, }) requestObj.promise = requestObj.promise.then(({ body }) => { if (body.returnCode !== '000000' || !body.lyric) { if (tryNum > 5) return Promise.reject(new Error('Get lyric failed')) - let tryRequestObj = this.getLyric(songInfo, ++tryNum) + let tryRequestObj = this.getLyricWeb(songInfo, ++tryNum) requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) return tryRequestObj.promise } @@ -39,4 +137,14 @@ export default { return requestObj } }, + + getLyric(songInfo) { + let requestObj = mrcTools.getLyric(songInfo) + requestObj.promise = requestObj.promise.catch(() => { + let webRequestObj = this.getLyricWeb(songInfo) + requestObj.cancelHttp = webRequestObj.cancelHttp.bind(webRequestObj) + return webRequestObj.promise + }) + return requestObj + }, } diff --git a/src/renderer/utils/music/mg/mrc.js b/src/renderer/utils/music/mg/mrc.js new file mode 100644 index 00000000..98ebb68f --- /dev/null +++ b/src/renderer/utils/music/mg/mrc.js @@ -0,0 +1,104 @@ + +// const key = 'karakal@123Qcomyidongtiantianhaoting' +const DELTA = 2654435769n +const MIN_LENGTH = 32 +// const SPECIAL_CHAR = '0' +const keyArr = [ + 27303562373562475n, + 18014862372307051n, + 22799692160172081n, + 34058940340699235n, + 30962724186095721n, + 27303523720101991n, + 27303523720101998n, + 31244139033526382n, + 28992395054481524n, +] + + +const teaDecrypt = (data, key) => { + const length = data.length + const lengthBitint = BigInt(length) + if (length >= 1) { + // let j = data[data.length - 1]; + let j2 = data[0] + let j3 = toLong((6n + (52n / lengthBitint)) * DELTA) + while (true) { + let j4 = j3 + if (j4 == 0n) break + let j5 = toLong(3n & toLong(j4 >> 2n)) + let j6 = lengthBitint + while (true) { + j6-- + if (j6 > 0n) { + let j7 = data[(j6 - 1n)] + let i = j6 + j2 = toLong(data[i] - (toLong(toLong(j2 ^ j4) + toLong(j7 ^ key[toLong(toLong(3n & j6) ^ j5)])) ^ toLong(toLong(toLong(j7 >> 5n) ^ toLong(j2 << 2n)) + toLong(toLong(j2 >> 3n) ^ toLong(j7 << 4n))))) + data[i] = j2 + } else break + } + let j8 = data[lengthBitint - 1n] + j2 = toLong(data[0n] - toLong(toLong(toLong(key[toLong(toLong(j6 & 3n) ^ j5)] ^ j8) + toLong(j2 ^ j4)) ^ toLong(toLong(toLong(j8 >> 5n) ^ toLong(j2 << 2n)) + toLong(toLong(j2 >> 3n) ^ toLong(j8 << 4n))))) + data[0] = j2 + j3 = toLong(j4 - DELTA) + } + } + return data +} + +const longArrToString = (data) => { + const arrayList = [] + for (const j of data) arrayList.push(longToBytes(j).toString('utf16le')) + return arrayList.join('') +} + +// https://stackoverflow.com/a/29132118 +const longToBytes = (l) => { + const result = Buffer.alloc(8) + for (let i = 0; i < 8; i++) { + result[i] = parseInt(l & 0xFFn) + l >>= 8n + } + return result +} + + +const toBigintArray = (data) => { + const length = Math.floor(data.length / 16) + const jArr = Array(length) + for (let i = 0; i < length; i++) { + jArr[i] = toLong(data.substring(i * 16, (i * 16) + 16)) + } + return jArr +} + +// https://github.com/lyswhut/lx-music-desktop/issues/445#issuecomment-1139338682 +const MAX = 9223372036854775807n +const MIN = -9223372036854775808n +const toLong = str => { + const num = typeof str == 'string' ? BigInt('0x' + str) : str + if (num > MAX) return toLong(num - (1n << 64n)) + else if (num < MIN) return toLong(num + (1n << 64n)) + return num +} + +export const decrypt = (data) => { + // console.log(data.length) + // -3551594764563790630 + // console.log(toLongArrayFromArr(Buffer.from(key))) + // console.log(teaDecrypt(toBigintArray(data), keyArr)) + // console.log(longArrToString(teaDecrypt(toBigintArray(data), keyArr))) + // console.log(toByteArray(teaDecrypt(toBigintArray(data), keyArr))) + return (data == null || data.length < MIN_LENGTH) + ? data + : longArrToString(teaDecrypt(toBigintArray(data), keyArr)) +} + +// console.log(14895149309145760986n - ) +// console.log(toLong('14895149309145760986')) +// console.log(decrypt(str)) +// console.log(decrypt(str)) +// console.log(toByteArray([6048138644744000495n])) +// console.log(toByteArray([16325999628386395n])) +// console.log(toLong(90994076459972177136n)) + diff --git a/src/renderer/utils/music/mg/musicSearch.js b/src/renderer/utils/music/mg/musicSearch.js index 77595b60..62bd8bc4 100644 --- a/src/renderer/utils/music/mg/musicSearch.js +++ b/src/renderer/utils/music/mg/musicSearch.js @@ -89,7 +89,7 @@ export default { name: item.name, albumName: albumNInfo.name, albumId: albumNInfo.id, - songmid: item.id, + songmid: item.copyrightId, songId: item.songId, copyrightId: item.copyrightId, source: 'mg', @@ -97,6 +97,8 @@ export default { img: item.imgItems && item.imgItems.length ? item.imgItems[0].img : null, lrc: null, lrcUrl: item.lyricUrl, + mrcUrl: item.mrcurl, + trcUrl: item.trcUrl, otherSource: null, types, _types, diff --git a/src/renderer/utils/music/mg/songList.js b/src/renderer/utils/music/mg/songList.js index de789a7d..998415c5 100644 --- a/src/renderer/utils/music/mg/songList.js +++ b/src/renderer/utils/music/mg/songList.js @@ -212,6 +212,8 @@ export default { img: item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : null, lrc: null, lrcUrl: item.lrcUrl, + mrcUrl: item.mrcUrl, + trcUrl: item.trcUrl, otherSource: null, types, _types,