新增 Any Listen 歌词标签数据嵌入、下载、读取播放(#2485)

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