diff --git a/publish/changeLog.md b/publish/changeLog.md index d274a597..8c333b90 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -1,10 +1,12 @@ ### 新增 -- 新增「设置 → 其他设置 → 主窗口使用软件内置的圆角及阴影」选项(#2360) +- 新增「设置 → 其他设置 → 主窗口使用软件内置的圆角及阴影」选项(#2360) *默认启用,关闭后将使用系统原生的窗口样式,该选项重启软件后生效* - 开放 API 新增播放器声音大小、静音、播放进度控制,详情看接入文档(#2386) - 新增「设置 → 播放设置 → 调换歌词翻译与歌词罗马音位置」选项,默认关闭(#2451) - 新增启动参数 `-hidden`,在启动时将软件最小化到系统托盘(#2459) +- 新增 Any Listen 歌词标签数据读取与播放(#2485) +- 新增 Any Listen 歌词(包含逐字歌词、翻译、罗马音歌词,如果有)嵌入与下载,默认启用 ### 修复 @@ -12,7 +14,7 @@ ### 变更 -- 调换「歌词翻译」与「歌词罗马音」的位置,现在歌词罗马音在歌词翻译的上方展示 +- 调换「歌词翻译」与「歌词罗马音」的位置,现在歌词罗马音在歌词翻译的上方展示 *若你想要恢复以前的行为,可以开启「调换歌词翻译与歌词罗马音位置」选项* - 更新代理配置规则,现在不启用代理时,图片、音频加载将不再走系统代理(#2382 @Folltoshe) diff --git a/src/common/defaultSetting.ts b/src/common/defaultSetting.ts index 564e9948..e417b325 100644 --- a/src/common/defaultSetting.ts +++ b/src/common/defaultSetting.ts @@ -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, diff --git a/src/common/types/app_setting.d.ts b/src/common/types/app_setting.d.ts index ac343786..14ca61af 100644 --- a/src/common/types/app_setting.d.ts +++ b/src/common/types/app_setting.d.ts @@ -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 + /** * 是否在音频文件中嵌入歌词 */ diff --git a/src/lang/en-us.json b/src/lang/en-us.json index ea57e204..f04971a5 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -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", diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index 6490e53a..c5a52423 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -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", diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index 43c6a9b4..491563fa 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -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", diff --git a/src/renderer/store/download/action.ts b/src/renderer/store/download/action.ts index 15618e9e..313d38e7 100644 --- a/src/renderer/store/download/action.ts +++ b/src/renderer/store/download/action.ts @@ -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) } }) } diff --git a/src/renderer/worker/download/common.ts b/src/renderer/worker/download/common.ts index 00411f82..6c31f6ec 100644 --- a/src/renderer/worker/download/common.ts +++ b/src/renderer/worker/download/common.ts @@ -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, 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' diff --git a/src/renderer/worker/download/lrcTool.ts b/src/renderer/worker/download/lrcTool.ts index bbb31f69..d6581389 100644 --- a/src/renderer/worker/download/lrcTool.ts +++ b/src/renderer/worker/download/lrcTool.ts @@ -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 } diff --git a/src/renderer/worker/download/utils.ts b/src/renderer/worker/download/utils.ts index c487bf4d..4e1a4e90 100644 --- a/src/renderer/worker/download/utils.ts +++ b/src/renderer/worker/download/utils.ts @@ -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 diff --git a/src/renderer/worker/main/music.ts b/src/renderer/worker/main/music.ts index daeb04c2..e705cde2 100644 --- a/src/renderer/worker/main/music.ts +++ b/src/renderer/worker/main/music.ts @@ -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 = {} + 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 = {} + let lyric = lrc.replace(lrcTags.rxp, (_: string, p1: string) => { + parsedInfo = parse(p1) + return '' + }).trim() + return { lyric, ...parsedInfo } +} + + export const getMusicFileLyric = async(filePath: string): Promise => { const lyric = await getLocalMusicFileLyric(filePath) if (!lyric) return null - return lyric + return parseLyric(lyric.lyric) }