支持本地同名 `krc` 格式歌词文件的读取(#2053)
parent
30fc818771
commit
a599e0716f
|
@ -1,6 +1,7 @@
|
||||||
### 新增
|
### 新增
|
||||||
|
|
||||||
- 新增托盘图标颜色 跟随系统亮暗模式 设置,可以在 设置-其他 启用 (#2016)
|
- 新增托盘图标颜色 跟随系统亮暗模式 设置,可以在 设置-其他 启用 (#2016)
|
||||||
|
- 支持本地同名 `krc` 格式歌词文件的读取(#2053)
|
||||||
|
|
||||||
### 优化
|
### 优化
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { inflate } from 'zlib'
|
||||||
|
import { decodeName } from './util'
|
||||||
|
|
||||||
|
// https://github.com/lyswhut/lx-music-desktop/issues/296#issuecomment-683285784
|
||||||
|
const enc_key = Buffer.from([0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69], 'binary')
|
||||||
|
const decodeLyric = str => new Promise((resolve, reject) => {
|
||||||
|
if (!str.length) return
|
||||||
|
const buf_str = Buffer.from(str, 'base64').subarray(4)
|
||||||
|
for (let i = 0, len = buf_str.length; i < len; i++) {
|
||||||
|
buf_str[i] = buf_str[i] ^ enc_key[i % 16]
|
||||||
|
}
|
||||||
|
inflate(buf_str, (err, result) => {
|
||||||
|
if (err) return reject(err)
|
||||||
|
resolve(result.toString())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const headExp = /^.*\[id:\$\w+\]\n/
|
||||||
|
|
||||||
|
const parseLyric = str => {
|
||||||
|
str = str.replace(/\r/g, '')
|
||||||
|
if (headExp.test(str)) str = str.replace(headExp, '')
|
||||||
|
let trans = str.match(/\[language:([\w=\\/+]+)\]/)
|
||||||
|
let lyric
|
||||||
|
let rlyric
|
||||||
|
let tlyric
|
||||||
|
if (trans) {
|
||||||
|
str = str.replace(/\[language:[\w=\\/+]+\]\n/, '')
|
||||||
|
let json = JSON.parse(Buffer.from(trans[1], 'base64').toString())
|
||||||
|
for (const item of json.content) {
|
||||||
|
switch (item.type) {
|
||||||
|
case 0:
|
||||||
|
rlyric = item.lyricContent
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
tlyric = item.lyricContent
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let i = 0
|
||||||
|
let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
|
||||||
|
let result = str.match(/\[((\d+),\d+)\].*/)
|
||||||
|
let time = parseInt(result[2])
|
||||||
|
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}`
|
||||||
|
if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}`
|
||||||
|
if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}`
|
||||||
|
i++
|
||||||
|
return str.replace(result[1], time)
|
||||||
|
})
|
||||||
|
rlyric = rlyric ? rlyric.join('\n') : ''
|
||||||
|
tlyric = tlyric ? tlyric.join('\n') : ''
|
||||||
|
lxlyric = lxlyric.replace(/<(\d+,\d+),\d+>/g, '<$1>')
|
||||||
|
lxlyric = decodeName(lxlyric)
|
||||||
|
lyric = lxlyric.replace(/<\d+,\d+>/g, '')
|
||||||
|
rlyric = decodeName(rlyric)
|
||||||
|
tlyric = decodeName(tlyric)
|
||||||
|
return {
|
||||||
|
lyric,
|
||||||
|
tlyric,
|
||||||
|
rlyric,
|
||||||
|
lxlyric,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const decodeKrc = async(data) => {
|
||||||
|
return decodeLyric(data).then(parseLyric)
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
const encodeNames = {
|
||||||
|
' ': ' ',
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
''': "'",
|
||||||
|
''': "'",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const decodeName = (str: string | null = '') => {
|
||||||
|
return str?.replace(/(?:&|<|>|"|'|'| )/gm, (s: string) => encodeNames[s as keyof typeof encodeNames]) ?? ''
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { checkPath, joinPath, extname, basename, readFile, getFileStats } from '@common/utils/nodejs'
|
import { checkPath, joinPath, extname, basename, readFile, getFileStats } from '@common/utils/nodejs'
|
||||||
import { formatPlayTime } from '@common/utils/common'
|
import { formatPlayTime } from '@common/utils/common'
|
||||||
import type { IComment } from 'music-metadata/lib/type'
|
import type { IComment } from 'music-metadata/lib/type'
|
||||||
|
import { decodeKrc } from '@common/utils/lyricUtils/kg'
|
||||||
|
|
||||||
export const checkDownloadFileAvailable = async(musicInfo: LX.Download.ListItem, savePath: string): Promise<boolean> => {
|
export const checkDownloadFileAvailable = async(musicInfo: LX.Download.ListItem, savePath: string): Promise<boolean> => {
|
||||||
return musicInfo.isComplate && !/\.ape$/.test(musicInfo.metadata.fileName) &&
|
return musicInfo.isComplate && !/\.ape$/.test(musicInfo.metadata.fileName) &&
|
||||||
|
@ -161,10 +162,11 @@ export const getLocalMusicFilePic = async(path: string) => {
|
||||||
* 获取歌曲文件歌词
|
* 获取歌曲文件歌词
|
||||||
* @param path 路径
|
* @param path 路径
|
||||||
*/
|
*/
|
||||||
export const getLocalMusicFileLyric = async(path: string): Promise<string | null> => {
|
export const getLocalMusicFileLyric = async(path: string): Promise<LX.Music.LyricInfo | null> => {
|
||||||
// 尝试读取同目录下的同名lrc文件
|
// 尝试读取同目录下的同名lrc文件
|
||||||
const lrcPath = path.replace(new RegExp('\\' + extname(path) + '$'), '.lrc')
|
const filePath = new RegExp('\\' + extname(path) + '$')
|
||||||
const stats = await getFileStats(lrcPath)
|
let lrcPath = path.replace(filePath, '.lrc')
|
||||||
|
let stats = await getFileStats(lrcPath)
|
||||||
// console.log(lrcPath, stats)
|
// console.log(lrcPath, stats)
|
||||||
if (stats && stats.size < 1024 * 1024 * 10) {
|
if (stats && stats.size < 1024 * 1024 * 10) {
|
||||||
const lrcBuf = await readFile(lrcPath)
|
const lrcBuf = await readFile(lrcPath)
|
||||||
|
@ -175,23 +177,46 @@ export const getLocalMusicFileLyric = async(path: string): Promise<string | null
|
||||||
const iconv = await import('iconv-lite')
|
const iconv = await import('iconv-lite')
|
||||||
if (iconv.encodingExists(encoding)) {
|
if (iconv.encodingExists(encoding)) {
|
||||||
const lrc = iconv.decode(lrcBuf, encoding)
|
const lrc = iconv.decode(lrcBuf, encoding)
|
||||||
if (lrc) return lrc
|
if (lrc) {
|
||||||
|
return {
|
||||||
|
lyric: lrc,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 尝试读取同目录下的同名krc文件
|
||||||
|
lrcPath = path.replace(filePath, '.krc')
|
||||||
|
stats = await getFileStats(lrcPath)
|
||||||
|
console.log(lrcPath, stats?.size)
|
||||||
|
if (stats && stats.size < 1024 * 1024 * 10) {
|
||||||
|
const lrcBuf = await readFile(lrcPath)
|
||||||
|
try {
|
||||||
|
return await decodeKrc(lrcBuf)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 尝试读取文件内歌词
|
// 尝试读取文件内歌词
|
||||||
const metadata = await getFileMetadata(path)
|
const metadata = await getFileMetadata(path)
|
||||||
if (!metadata) return null
|
if (!metadata) return null
|
||||||
if (metadata.common.lyrics?.[0]?.text && metadata.common.lyrics[0].text.length > 10) {
|
if (metadata.common.lyrics?.[0]?.text && metadata.common.lyrics[0].text.length > 10) {
|
||||||
return metadata.common.lyrics[0].text
|
return {
|
||||||
|
lyric: metadata.common.lyrics[0].text,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// console.log(metadata)
|
// console.log(metadata)
|
||||||
for (const info of Object.values(metadata.native)) {
|
for (const info of Object.values(metadata.native)) {
|
||||||
const ust = info.find(i => i.id == 'USLT')
|
const ust = info.find(i => i.id == 'USLT')
|
||||||
if (ust) {
|
if (ust) {
|
||||||
const value = ust.value as IComment
|
const value = ust.value as IComment
|
||||||
if (value.text && value.text.length > 10) return value.text
|
if (value.text && value.text.length > 10) {
|
||||||
|
return {
|
||||||
|
lyric: value.text,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -1,59 +1,5 @@
|
||||||
import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
import { decodeLyric } from './util'
|
import { decodeKrc } from '@common/utils/lyricUtils/kg'
|
||||||
import { decodeName } from '../../index'
|
|
||||||
|
|
||||||
const headExp = /^.*\[id:\$\w+\]\n/
|
|
||||||
|
|
||||||
const parseLyric = str => {
|
|
||||||
str = str.replace(/\r/g, '')
|
|
||||||
if (headExp.test(str)) str = str.replace(headExp, '')
|
|
||||||
let trans = str.match(/\[language:([\w=\\/+]+)\]/)
|
|
||||||
let lyric
|
|
||||||
let rlyric
|
|
||||||
let tlyric
|
|
||||||
if (trans) {
|
|
||||||
str = str.replace(/\[language:[\w=\\/+]+\]\n/, '')
|
|
||||||
let json = JSON.parse(Buffer.from(trans[1], 'base64').toString())
|
|
||||||
for (const item of json.content) {
|
|
||||||
switch (item.type) {
|
|
||||||
case 0:
|
|
||||||
rlyric = item.lyricContent
|
|
||||||
break
|
|
||||||
case 1:
|
|
||||||
tlyric = item.lyricContent
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let i = 0
|
|
||||||
let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
|
|
||||||
let result = str.match(/\[((\d+),\d+)\].*/)
|
|
||||||
let time = parseInt(result[2])
|
|
||||||
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}`
|
|
||||||
if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}`
|
|
||||||
if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}`
|
|
||||||
i++
|
|
||||||
return str.replace(result[1], time)
|
|
||||||
})
|
|
||||||
rlyric = rlyric ? rlyric.join('\n') : ''
|
|
||||||
tlyric = tlyric ? tlyric.join('\n') : ''
|
|
||||||
lxlyric = lxlyric.replace(/<(\d+,\d+),\d+>/g, '<$1>')
|
|
||||||
lxlyric = decodeName(lxlyric)
|
|
||||||
lyric = lxlyric.replace(/<\d+,\d+>/g, '')
|
|
||||||
rlyric = decodeName(rlyric)
|
|
||||||
tlyric = decodeName(tlyric)
|
|
||||||
return {
|
|
||||||
lyric,
|
|
||||||
tlyric,
|
|
||||||
rlyric,
|
|
||||||
lxlyric,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getIntv(interval) {
|
getIntv(interval) {
|
||||||
|
@ -130,7 +76,7 @@ export default {
|
||||||
|
|
||||||
switch (body.fmt) {
|
switch (body.fmt) {
|
||||||
case 'krc':
|
case 'krc':
|
||||||
return decodeLyric(body.content).then(result => parseLyric(result))
|
return decodeKrc(body.content)
|
||||||
case 'lrc':
|
case 'lrc':
|
||||||
return {
|
return {
|
||||||
lyric: Buffer.from(body.content, 'base64').toString('utf-8'),
|
lyric: Buffer.from(body.content, 'base64').toString('utf-8'),
|
||||||
|
|
|
@ -1,21 +1,6 @@
|
||||||
import { inflate } from 'zlib'
|
|
||||||
import { toMD5 } from '../utils'
|
import { toMD5 } from '../utils'
|
||||||
import { httpFetch } from '../../request'
|
import { httpFetch } from '../../request'
|
||||||
|
|
||||||
// https://github.com/lyswhut/lx-music-desktop/issues/296#issuecomment-683285784
|
|
||||||
const enc_key = Buffer.from([0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69], 'binary')
|
|
||||||
export const decodeLyric = str => new Promise((resolve, reject) => {
|
|
||||||
if (!str.length) return
|
|
||||||
const buf_str = Buffer.from(str, 'base64').slice(4)
|
|
||||||
for (let i = 0, len = buf_str.length; i < len; i++) {
|
|
||||||
buf_str[i] = buf_str[i] ^ enc_key[i % 16]
|
|
||||||
}
|
|
||||||
inflate(buf_str, (err, result) => {
|
|
||||||
if (err) return reject(err)
|
|
||||||
resolve(result.toString())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// s.content[0].lyricContent.forEach(([str]) => {
|
// s.content[0].lyricContent.forEach(([str]) => {
|
||||||
// console.log(str)
|
// console.log(str)
|
||||||
// })
|
// })
|
||||||
|
|
|
@ -31,7 +31,5 @@ export const getMusicFilePic = async(filePath: string) => {
|
||||||
export const getMusicFileLyric = async(filePath: string): Promise<LX.Music.LyricInfo | null> => {
|
export const getMusicFileLyric = async(filePath: string): Promise<LX.Music.LyricInfo | null> => {
|
||||||
const lyric = await getLocalMusicFileLyric(filePath)
|
const lyric = await getLocalMusicFileLyric(filePath)
|
||||||
if (!lyric) return null
|
if (!lyric) return null
|
||||||
return {
|
return lyric
|
||||||
lyric,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue