新增 Any Listen 歌词标签数据嵌入、下载、读取播放(#2485)
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
### 新增
|
||||
|
||||
- 新增「设置 → 其他设置 → 主窗口使用软件内置的圆角及阴影」选项(#2360)
|
||||
- 新增「设置 → 其他设置 → 主窗口使用软件内置的圆角及阴影」选项(#2360)
|
||||
*默认启用,关闭后将使用系统原生的窗口样式,该选项重启软件后生效*
|
||||
- 开放 API 新增播放器声音大小、静音、播放进度控制,详情看接入文档(#2386)
|
||||
- 新增「设置 → 播放设置 → 调换歌词翻译与歌词罗马音位置」选项,默认关闭(#2451)
|
||||
- 新增启动参数 `-hidden`,在启动时将软件最小化到系统托盘(#2459)
|
||||
- 新增 Any Listen 歌词标签数据读取与播放(#2485)
|
||||
- 新增 Any Listen 歌词(包含逐字歌词、翻译、罗马音歌词,如果有)嵌入与下载,默认启用
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -12,7 +14,7 @@
|
||||
|
||||
### 变更
|
||||
|
||||
- 调换「歌词翻译」与「歌词罗马音」的位置,现在歌词罗马音在歌词翻译的上方展示
|
||||
- 调换「歌词翻译」与「歌词罗马音」的位置,现在歌词罗马音在歌词翻译的上方展示
|
||||
*若你想要恢复以前的行为,可以开启「调换歌词翻译与歌词罗马音位置」选项*
|
||||
- 更新代理配置规则,现在不启用代理时,图片、音频加载将不再走系统代理(#2382 @Folltoshe)
|
||||
|
||||
|
||||
@@ -116,11 +116,13 @@ const defaultSetting: LX.AppSetting = {
|
||||
'download.maxDownloadNum': 3,
|
||||
'download.skipExistFile': true,
|
||||
'download.isDownloadLrc': false,
|
||||
'download.isDownloadLxLrc': true,
|
||||
'download.isDownloadTLrc': false,
|
||||
'download.isDownloadRLrc': false,
|
||||
'download.lrcFormat': 'utf8',
|
||||
'download.isEmbedPic': true,
|
||||
'download.isEmbedLyric': false,
|
||||
'download.isEmbedLyricLx': true,
|
||||
'download.isEmbedLyricT': false,
|
||||
'download.isEmbedLyricR': false,
|
||||
'download.isUseOtherSource': false,
|
||||
|
||||
Vendored
+10
@@ -530,6 +530,11 @@ declare global {
|
||||
*/
|
||||
'download.isDownloadLrc': boolean
|
||||
|
||||
/**
|
||||
* 是否在下载 lx 歌词
|
||||
*/
|
||||
'download.isDownloadLxLrc': boolean
|
||||
|
||||
/**
|
||||
* 是否下载翻译歌词文件
|
||||
*/
|
||||
@@ -550,6 +555,11 @@ declare global {
|
||||
*/
|
||||
'download.isEmbedPic': boolean
|
||||
|
||||
/**
|
||||
* 是否在音频文件中嵌入 lx 歌词
|
||||
*/
|
||||
'download.isEmbedLyricLx': boolean
|
||||
|
||||
/**
|
||||
* 是否在音频文件中嵌入歌词
|
||||
*/
|
||||
|
||||
@@ -407,11 +407,13 @@
|
||||
"setting__dislike_list_title": "Disliked Song Rule List",
|
||||
"setting__download": "Download",
|
||||
"setting__download_data_embed": "Embed the Following into Audio File",
|
||||
"setting__download_embed_lxlyric": "Embed LX Music lyrics at the same time (if any, verbatim lyrics can be supported when playing with LX Music)",
|
||||
"setting__download_embed_lyric": "Embed lyrics",
|
||||
"setting__download_embed_pic": "Embed cover",
|
||||
"setting__download_embed_rlyric": "Also embed romanized lyrics if available",
|
||||
"setting__download_embed_tlyric": "Also embed translated lyrics if available",
|
||||
"setting__download_enable": "Enable Download",
|
||||
"setting__download_lxlyric": "Also write LX Music lyrics into the lyrics file (if any, verbatim lyrics can be supported when playing with LX Music)",
|
||||
"setting__download_lyric": "Lyric Download",
|
||||
"setting__download_lyric_format": "Encoding Format of Downloaded Lyric Files",
|
||||
"setting__download_lyric_format_gbk": "GBK",
|
||||
|
||||
@@ -407,11 +407,13 @@
|
||||
"setting__dislike_list_title": "「不喜欢的歌曲」规则列表",
|
||||
"setting__download": "下载设置",
|
||||
"setting__download_data_embed": "嵌入到音频文件中的内容",
|
||||
"setting__download_embed_lxlyric": "同时嵌入 LX Music 歌词(如果有,使用 LX Music 播放时可支持逐字歌词)",
|
||||
"setting__download_embed_lyric": "嵌入歌词",
|
||||
"setting__download_embed_pic": "嵌入封面",
|
||||
"setting__download_embed_rlyric": "同时嵌入罗马音歌词(如果有)",
|
||||
"setting__download_embed_tlyric": "同时嵌入翻译歌词(如果有)",
|
||||
"setting__download_enable": "启用下载功能",
|
||||
"setting__download_lxlyric": "同时将 LX Music 歌词写入歌词文件中(如果有,使用 LX Music 播放时可支持逐字歌词)",
|
||||
"setting__download_lyric": "歌词下载",
|
||||
"setting__download_lyric_format": "下载的歌词文件编码格式",
|
||||
"setting__download_lyric_format_gbk": "GBK",
|
||||
|
||||
@@ -407,11 +407,13 @@
|
||||
"setting__dislike_list_title": "「不喜歡的歌曲」規則清單",
|
||||
"setting__download": "下載設定",
|
||||
"setting__download_data_embed": "嵌入到音訊檔案中的內容",
|
||||
"setting__download_embed_lxlyric": "同時嵌入 LX Music 歌詞(如果有,使用 LX Music 播放時可支持逐字歌詞)",
|
||||
"setting__download_embed_lyric": "嵌入歌詞",
|
||||
"setting__download_embed_pic": "嵌入封面",
|
||||
"setting__download_embed_rlyric": "同時嵌入羅馬音歌詞(如果有)",
|
||||
"setting__download_embed_tlyric": "同時嵌入翻譯歌詞(如果有)",
|
||||
"setting__download_enable": "啟用下載功能",
|
||||
"setting__download_lxlyric": "同時將 LX Music 歌詞寫入歌詞文件中(如果有,使用 LX Music 播放時可支持逐字歌詞)",
|
||||
"setting__download_lyric": "歌詞下載",
|
||||
"setting__download_lyric_format": "下載的歌詞檔案編碼格式",
|
||||
"setting__download_lyric_format_gbk": "GBK",
|
||||
|
||||
@@ -166,22 +166,17 @@ const saveMeta = (downloadInfo: LX.Download.ListItem) => {
|
||||
: Promise.resolve(null),
|
||||
]
|
||||
void Promise.all(tasks).then(([imgUrl, lyrics]) => {
|
||||
const lrcData = {
|
||||
lrc: '',
|
||||
tlrc: null as string | null,
|
||||
rlrc: null as string | null,
|
||||
}
|
||||
if (lyrics) {
|
||||
lrcData.lrc = lyrics.lyric
|
||||
if (appSetting['download.isEmbedLyricT'] && lyrics.tlyric) lrcData.tlrc = lyrics.tlyric
|
||||
if (appSetting['download.isEmbedLyricR'] && lyrics.rlyric) lrcData.rlrc = lyrics.rlyric
|
||||
}
|
||||
void window.lx.worker.download.writeMeta(downloadInfo.metadata.filePath, {
|
||||
const info = {
|
||||
filePath: downloadInfo.metadata.filePath,
|
||||
isEmbedLyricLx: appSetting['download.isEmbedLyricLx'],
|
||||
isEmbedLyricT: appSetting['download.isEmbedLyricT'],
|
||||
isEmbedLyricR: appSetting['download.isEmbedLyricR'],
|
||||
title: downloadInfo.metadata.musicInfo.name,
|
||||
artist: downloadInfo.metadata.musicInfo.singer?.replaceAll('、', ';'),
|
||||
album: downloadInfo.metadata.musicInfo.meta.albumName,
|
||||
APIC: imgUrl,
|
||||
}, lrcData, getProxy())
|
||||
}
|
||||
void window.lx.worker.download.writeMeta(info, lyrics ?? { lyric: '' }, getProxy())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -198,14 +193,14 @@ const downloadLyric = (downloadInfo: LX.Download.ListItem) => {
|
||||
}).then(lrcs => {
|
||||
if (lrcs.lyric) {
|
||||
lrcs.lyric = fixKgLyric(lrcs.lyric)
|
||||
const lrcData = {
|
||||
lrc: lrcs.lyric,
|
||||
tlrc: appSetting['download.isDownloadTLrc'] && lrcs.tlyric ? lrcs.tlyric : null,
|
||||
rlrc: appSetting['download.isDownloadRLrc'] && lrcs.rlyric ? lrcs.rlyric : null,
|
||||
const info = {
|
||||
filePath: downloadInfo.metadata.filePath.substring(0, downloadInfo.metadata.filePath.lastIndexOf('.')) + '.lrc',
|
||||
format: appSetting['download.lrcFormat'],
|
||||
downloadLxlrc: appSetting['download.isDownloadLxLrc'],
|
||||
downloadTlrc: appSetting['download.isDownloadTLrc'],
|
||||
downloadRlrc: appSetting['download.isDownloadRLrc'],
|
||||
}
|
||||
void window.lx.worker.download.saveLrc(lrcData,
|
||||
downloadInfo.metadata.filePath.substring(0, downloadInfo.metadata.filePath.lastIndexOf('.')) + '.lrc',
|
||||
appSetting['download.lrcFormat'])
|
||||
void window.lx.worker.download.saveLrc(lrcs, info)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { setMeta } from '@common/utils/musicMeta'
|
||||
import { mergeLyrics } from './lrcTool'
|
||||
import { buildLyrics } from './lrcTool'
|
||||
|
||||
export const writeMeta = (filePath: string, meta: Omit<LX.Music.MusicFileMeta, 'lyrics'>, lyricData: { lrc: string, tlrc: string | null, rlrc: string | null }, proxy?: { host: string, port: number }) => {
|
||||
setMeta(filePath, { ...meta, lyrics: mergeLyrics(lyricData.lrc, lyricData.tlrc, lyricData.rlrc) || null }, proxy)
|
||||
export const writeMeta = ({ filePath, isEmbedLyricLx, isEmbedLyricT, isEmbedLyricR, ...meta }: {
|
||||
filePath: string
|
||||
isEmbedLyricLx: boolean
|
||||
isEmbedLyricT: boolean
|
||||
isEmbedLyricR: boolean
|
||||
title: string
|
||||
artist: string
|
||||
album: string
|
||||
APIC: string | null
|
||||
}, lyric: LX.Music.LyricInfo, proxy?: { host: string, port: number }) => {
|
||||
setMeta(filePath, { ...meta, lyrics: buildLyrics(lyric, isEmbedLyricLx, isEmbedLyricT, isEmbedLyricR) }, proxy)
|
||||
}
|
||||
|
||||
export { saveLrc } from './utils'
|
||||
|
||||
@@ -61,14 +61,38 @@ const parseLrcTimeLabel = (lrc: string) => {
|
||||
return linesSet
|
||||
}
|
||||
|
||||
const buildAwlyric = (lrcData: LX.Music.LyricInfo) => {
|
||||
let lrc: string[] = []
|
||||
if (lrcData.lyric) {
|
||||
lrc.push(`lrc:${Buffer.from(lrcData.lyric.trim(), 'utf-8').toString('base64')}`)
|
||||
}
|
||||
if (lrcData.tlyric) {
|
||||
lrc.push(`tlrc:${Buffer.from(lrcData.tlyric.trim(), 'utf-8').toString('base64')}`)
|
||||
}
|
||||
if (lrcData.rlyric) {
|
||||
lrc.push(`rlrc:${Buffer.from(lrcData.rlyric.trim(), 'utf-8').toString('base64')}`)
|
||||
}
|
||||
if (lrcData.lxlyric) {
|
||||
lrc.push(`awlrc:${Buffer.from(lrcData.lxlyric.trim(), 'utf-8').toString('base64')}`)
|
||||
}
|
||||
return lrc.length ? `[awlrc:${lrc.join(',')}]` : ''
|
||||
}
|
||||
|
||||
export const mergeLyrics = (lrc: string, tlrc: string | null, rlrc: string | null) => {
|
||||
if (!tlrc && !rlrc) return lrc
|
||||
export const buildLyrics = (lrcData: LX.Music.LyricInfo, downloadAwlrc: boolean, downloadTlrc: boolean, downloadRlrc: boolean) => {
|
||||
if (!lrcData.tlyric && !lrcData.rlyric && !lrcData.lxlyric) return lrcData.lyric
|
||||
|
||||
const lrcTimeLabels = parseLrcTimeLabel(lrc)
|
||||
// console.log(lrcTimeLabels)
|
||||
if (tlrc) lrc += `\n\n${filterExtendedLyricLabel(lrcTimeLabels, tlrc)}\n`
|
||||
if (rlrc) lrc += `\n\n${filterExtendedLyricLabel(lrcTimeLabels, rlrc)}\n`
|
||||
// console.log(lrc)
|
||||
const lrcTimeLabels = parseLrcTimeLabel(lrcData.lyric)
|
||||
|
||||
let lrc = lrcData.lyric
|
||||
if (downloadTlrc && lrcData.tlyric) {
|
||||
lrc = lrc.trim() + `\n\n${filterExtendedLyricLabel(lrcTimeLabels, lrcData.tlyric)}\n`
|
||||
}
|
||||
if (downloadRlrc && lrcData.rlyric) {
|
||||
lrc = lrc.trim() + `\n\n${filterExtendedLyricLabel(lrcTimeLabels, lrcData.rlyric)}\n`
|
||||
}
|
||||
if (downloadAwlrc) {
|
||||
const awlrc = buildAwlyric(lrcData)
|
||||
if (awlrc) lrc = lrc.trim() + `\n\n${awlrc}\n`
|
||||
}
|
||||
return lrc
|
||||
}
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import { DOWNLOAD_STATUS, QUALITYS } from '@common/constants'
|
||||
import { filterFileName } from '@common/utils/common'
|
||||
import { mergeLyrics } from './lrcTool'
|
||||
import { buildLyrics } from './lrcTool'
|
||||
import fs from 'fs'
|
||||
import { clipFileNameLength, clipNameLength } from '@common/utils/tools'
|
||||
|
||||
/**
|
||||
* 保存歌词文件
|
||||
* @param {*} filePath
|
||||
* @param {*} lrc
|
||||
* @param {*} format
|
||||
*/
|
||||
export const saveLrc = async(lrcData: { lrc: string, tlrc: string | null, rlrc: string | null }, filePath: string, format: LX.LyricFormat) => {
|
||||
export const saveLrc = async(lrcData: LX.Music.LyricInfo, info: {
|
||||
filePath: string
|
||||
format: LX.LyricFormat
|
||||
downloadLxlrc: boolean
|
||||
downloadTlrc: boolean
|
||||
downloadRlrc: boolean
|
||||
}) => {
|
||||
const iconv = await import('iconv-lite')
|
||||
const lrc = mergeLyrics(lrcData.lrc, lrcData.tlrc, lrcData.rlrc)
|
||||
switch (format) {
|
||||
const lrc = buildLyrics(lrcData, info.downloadLxlrc, info.downloadTlrc, info.downloadRlrc)
|
||||
switch (info.format) {
|
||||
case 'gbk':
|
||||
fs.writeFile(filePath, iconv.encode(lrc, 'gbk', { addBOM: true }), err => {
|
||||
fs.writeFile(info.filePath, iconv.encode(lrc, 'gbk', { addBOM: true }), err => {
|
||||
if (err) console.log(err)
|
||||
})
|
||||
break
|
||||
case 'utf8':
|
||||
default:
|
||||
fs.writeFile(filePath, iconv.encode(lrc, 'utf8', { addBOM: true }), err => {
|
||||
fs.writeFile(info.filePath, iconv.encode(lrc, 'utf8', { addBOM: true }), err => {
|
||||
if (err) console.log(err)
|
||||
})
|
||||
break
|
||||
|
||||
@@ -29,8 +29,57 @@ export const getMusicFilePic = async(filePath: string) => {
|
||||
return `data:${picture.format};base64,${Buffer.from(picture.data).toString('base64')}`
|
||||
}
|
||||
|
||||
export const parseLyric = (lrc: string): LX.Music.LyricInfo => {
|
||||
const verifyAwlrc = (lrc: string) => {
|
||||
return /(?:^|\s*)\[\d+:\d+(?:\.\d+)]<\d+,\d+>.+$/m.test(lrc)
|
||||
}
|
||||
const verifylrc = (lrc: string) => {
|
||||
return /(?:^|\s*)\[\d+:\d+(?:\.\d+)].+$/m.test(lrc)
|
||||
}
|
||||
const lrcTags = {
|
||||
awlrc: {
|
||||
name: 'lxlyric',
|
||||
verify: verifyAwlrc,
|
||||
},
|
||||
lrc: {
|
||||
name: 'lyric',
|
||||
verify: verifylrc,
|
||||
},
|
||||
tlrc: {
|
||||
name: 'tlyric',
|
||||
verify: verifylrc,
|
||||
},
|
||||
rlrc: {
|
||||
name: 'rlyric',
|
||||
verify: verifylrc,
|
||||
},
|
||||
rxp: /(?:^|\n\s*)\[(?:awlrc):([^\]]+)]/i,
|
||||
rxp2: /^(lrc|awlrc|tlrc|rlrc):([^,]+)$/i,
|
||||
} as const
|
||||
const parse = (content: string) => {
|
||||
const lyricInfo: Partial<LX.Music.LyricInfo> = {}
|
||||
const lrcs = content.trim().split(',')
|
||||
for (const lrc of lrcs) {
|
||||
const result = lrcTags.rxp2.exec(lrc.trim())
|
||||
if (!result) continue
|
||||
const target = lrcTags[result[1].toLowerCase() as 'tlrc' | 'rlrc' | 'lrc' | 'awlrc']
|
||||
if (!target) continue
|
||||
const data = Buffer.from(result[2], 'base64').toString('utf-8').trim()
|
||||
if (target.verify(data)) lyricInfo[target.name] = data
|
||||
}
|
||||
return lyricInfo
|
||||
}
|
||||
let parsedInfo: Partial<LX.Music.LyricInfo> = {}
|
||||
let lyric = lrc.replace(lrcTags.rxp, (_: string, p1: string) => {
|
||||
parsedInfo = parse(p1)
|
||||
return ''
|
||||
}).trim()
|
||||
return { lyric, ...parsedInfo }
|
||||
}
|
||||
|
||||
|
||||
export const getMusicFileLyric = async(filePath: string): Promise<LX.Music.LyricInfo | null> => {
|
||||
const lyric = await getLocalMusicFileLyric(filePath)
|
||||
if (!lyric) return null
|
||||
return lyric
|
||||
return parseLyric(lyric.lyric)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user