支持mg源逐字歌词的播放
parent
f2e3d4a53d
commit
bb8509a21c
|
@ -5,6 +5,7 @@
|
||||||
### 优化
|
### 优化
|
||||||
|
|
||||||
- 添加歌曲到“我的列表”时,若按住`ctrl`键(Mac对应`Command`),则不会自动关闭添加窗口,这对想要将同一首(一批)歌曲添加到多个列表时会很有用
|
- 添加歌曲到“我的列表”时,若按住`ctrl`键(Mac对应`Command`),则不会自动关闭添加窗口,这对想要将同一首(一批)歌曲添加到多个列表时会很有用
|
||||||
|
- 支持mg源逐字歌词的播放,感谢 @mozbugbox 提供的帮助
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
|
|
|
@ -238,6 +238,7 @@ const actions = {
|
||||||
switch (musicInfo.source) {
|
switch (musicInfo.source) {
|
||||||
case 'kg':
|
case 'kg':
|
||||||
case 'kw':
|
case 'kw':
|
||||||
|
case 'mg':
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return buildLyricInfo(lrcInfo, musicInfo)
|
return buildLyricInfo(lrcInfo, musicInfo)
|
||||||
|
|
|
@ -148,6 +148,8 @@ export default {
|
||||||
img: item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : null,
|
img: item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : null,
|
||||||
lrc: null,
|
lrc: null,
|
||||||
lrcUrl: item.lrcUrl,
|
lrcUrl: item.lrcUrl,
|
||||||
|
mrcUrl: item.mrcUrl,
|
||||||
|
trcUrl: item.trcUrl,
|
||||||
otherSource: null,
|
otherSource: null,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
|
|
|
@ -1,14 +1,112 @@
|
||||||
import { httpFetch } from '../../request'
|
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 {
|
export default {
|
||||||
getLyric(songInfo, tryNum = 0) {
|
getLyricWeb(songInfo, tryNum = 0) {
|
||||||
// console.log(songInfo.copyrightId)
|
// console.log(songInfo.copyrightId)
|
||||||
if (songInfo.lrcUrl) {
|
if (songInfo.lrcUrl) {
|
||||||
let requestObj = httpFetch(songInfo.lrcUrl)
|
let requestObj = httpFetch(songInfo.lrcUrl)
|
||||||
requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {
|
requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {
|
||||||
if (statusCode !== 200) {
|
if (statusCode !== 200) {
|
||||||
if (tryNum > 5) return Promise.reject('歌词获取失败')
|
if (tryNum > 5) return Promise.reject('歌词获取失败')
|
||||||
let tryRequestObj = this.getLyric(songInfo, ++tryNum)
|
let tryRequestObj = this.getLyricWeb(songInfo, ++tryNum)
|
||||||
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
|
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
|
||||||
return tryRequestObj.promise
|
return tryRequestObj.promise
|
||||||
}
|
}
|
||||||
|
@ -19,15 +117,15 @@ export default {
|
||||||
})
|
})
|
||||||
return requestObj
|
return requestObj
|
||||||
} else {
|
} 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: {
|
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 }) => {
|
requestObj.promise = requestObj.promise.then(({ body }) => {
|
||||||
if (body.returnCode !== '000000' || !body.lyric) {
|
if (body.returnCode !== '000000' || !body.lyric) {
|
||||||
if (tryNum > 5) return Promise.reject(new Error('Get lyric failed'))
|
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)
|
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
|
||||||
return tryRequestObj.promise
|
return tryRequestObj.promise
|
||||||
}
|
}
|
||||||
|
@ -39,4 +137,14 @@ export default {
|
||||||
return requestObj
|
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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -89,7 +89,7 @@ export default {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
albumName: albumNInfo.name,
|
albumName: albumNInfo.name,
|
||||||
albumId: albumNInfo.id,
|
albumId: albumNInfo.id,
|
||||||
songmid: item.id,
|
songmid: item.copyrightId,
|
||||||
songId: item.songId,
|
songId: item.songId,
|
||||||
copyrightId: item.copyrightId,
|
copyrightId: item.copyrightId,
|
||||||
source: 'mg',
|
source: 'mg',
|
||||||
|
@ -97,6 +97,8 @@ export default {
|
||||||
img: item.imgItems && item.imgItems.length ? item.imgItems[0].img : null,
|
img: item.imgItems && item.imgItems.length ? item.imgItems[0].img : null,
|
||||||
lrc: null,
|
lrc: null,
|
||||||
lrcUrl: item.lyricUrl,
|
lrcUrl: item.lyricUrl,
|
||||||
|
mrcUrl: item.mrcurl,
|
||||||
|
trcUrl: item.trcUrl,
|
||||||
otherSource: null,
|
otherSource: null,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
|
|
|
@ -212,6 +212,8 @@ export default {
|
||||||
img: item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : null,
|
img: item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : null,
|
||||||
lrc: null,
|
lrc: null,
|
||||||
lrcUrl: item.lrcUrl,
|
lrcUrl: item.lrcUrl,
|
||||||
|
mrcUrl: item.mrcUrl,
|
||||||
|
trcUrl: item.trcUrl,
|
||||||
otherSource: null,
|
otherSource: null,
|
||||||
types,
|
types,
|
||||||
_types,
|
_types,
|
||||||
|
|
Loading…
Reference in New Issue