lx-music-desktop/src/renderer/utils/music.ts

241 lines
7.4 KiB
TypeScript

import { checkPath, joinPath, extname, basename, readFile, getFileStats } from '@common/utils/nodejs'
import { formatPlayTime } from '@common/utils/common'
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> => {
return musicInfo.isComplate && !/\.ape$/.test(musicInfo.metadata.fileName) &&
(await checkPath(musicInfo.metadata.filePath) || await checkPath(joinPath(savePath, musicInfo.metadata.fileName)))
}
export const checkLocalFileAvailable = async(musicInfo: LX.Music.MusicInfoLocal): Promise<boolean> => {
return checkPath(musicInfo.meta.filePath)
}
/**
* 检查音乐文件是否存在
* @param musicInfo
* @param savePath
*/
export const checkMusicFileAvailable = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, savePath: string): Promise<boolean> => {
if ('progress' in musicInfo) {
return checkDownloadFileAvailable(musicInfo, savePath)
} else if (musicInfo.source == 'local') {
return checkLocalFileAvailable(musicInfo)
} else return true
}
export const getDownloadFilePath = async(musicInfo: LX.Download.ListItem, savePath: string): Promise<string> => {
if (musicInfo.isComplate && !/\.ape$/.test(musicInfo.metadata.fileName)) {
if (await checkPath(musicInfo.metadata.filePath)) return musicInfo.metadata.filePath
const path = joinPath(savePath, musicInfo.metadata.fileName)
if (await checkPath(path)) return path
}
return ''
}
export const getLocalFilePath = async(musicInfo: LX.Music.MusicInfoLocal): Promise<string> => {
return (await checkPath(musicInfo.meta.filePath)) ? musicInfo.meta.filePath : ''
}
/**
* 获取音乐文件路径
* @param musicInfo
* @param savePath
* @returns
*/
export const getMusicFilePath = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, savePath: string): Promise<string> => {
if ('progress' in musicInfo) {
return getDownloadFilePath(musicInfo, savePath)
} else if (musicInfo.source == 'local') {
return getLocalFilePath(musicInfo)
}
return ''
}
/**
* 创建本地音乐信息对象
* @param path 文件路径
* @returns
*/
export const createLocalMusicInfo = async(path: string): Promise<LX.Music.MusicInfoLocal | null> => {
if (!await checkPath(path)) return null
const { parseFile } = await import('music-metadata')
let metadata
try {
metadata = await parseFile(path)
} catch (err) {
console.log(err)
return null
}
// console.log(metadata)
let ext = extname(path)
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let name = (metadata.common.title || basename(path, ext)).trim()
let singer = metadata.common.artists?.length ? metadata.common.artists.map(a => a.trim()).join('、') : ''
let interval = metadata.format.duration ? formatPlayTime(metadata.format.duration) : ''
let albumName = metadata.common.album?.trim() ?? ''
return {
id: path,
name,
singer,
source: 'local',
interval,
meta: {
albumName,
filePath: path,
songId: path,
picUrl: '',
ext: ext.replace(/^\./, ''),
},
}
}
let prevFileInfo: {
path: string
promise: Promise<LX.MusicMetadataModule.IAudioMetadata | null>
} = {
path: '',
promise: Promise.resolve(null),
}
const getFileMetadata = async(path: string) => {
if (prevFileInfo.path == path) return prevFileInfo.promise
prevFileInfo.path = path
return prevFileInfo.promise = checkPath(path).then(async(isExist) => {
return isExist ? import('music-metadata').then(async({ parseFile }) => parseFile(path)).catch(err => {
console.log(err)
return null
}) : null
})
}
/**
* 获取歌曲文件封面图片
* @param path 路径
*/
export const getLocalMusicFilePic = async(path: string) => {
const filePath = new RegExp('\\' + extname(path) + '$')
let picPath = path.replace(filePath, '.jpg')
let stats = await getFileStats(picPath)
if (stats) return picPath
picPath = path.replace(filePath, '.png')
stats = await getFileStats(picPath)
if (stats) return picPath
const metadata = await getFileMetadata(path)
if (!metadata) return null
const { selectCover } = await import('music-metadata')
return selectCover(metadata.common.picture)
}
// const timeExp = /^\[([\d:.]*)\]{1}/
/**
* 解析歌词文件,分离可能存在的翻译、罗马音歌词
* @param lrc 歌词内容
* @returns
*/
// export const parseLyric = (lrc: string): LX.Music.LyricInfo => {
// const lines = lrc.split(/\r\n|\r|\n/)
// const lyrics: string[][] = []
// const map = new Map<string, number>()
// for (let i = 0; i < lines.length; i++) {
// const line = lines[i].trim()
// let result = timeExp.exec(line)
// if (result) {
// const index = map.get(result[1]) ?? 0
// if (!lyrics[index]) lyrics[index] = []
// lyrics[index].push(line)
// map.set(result[1], index + 1)
// } else {
// if (!lyrics[0]) lyrics[0] = []
// lyrics[0].push(line)
// }
// }
// const lyricInfo: LX.Music.LyricInfo = {
// lyric: lyrics[0].join('\n'),
// tlyric: '',
// }
// if (lyrics[1]) lyricInfo.tlyric = lyrics[1].join('\n')
// if (lyrics[2]) lyricInfo.rlyric = lyrics[2].join('\n')
// return lyricInfo
// }
/**
* 获取歌曲文件歌词
* @param path 路径
*/
export const getLocalMusicFileLyric = async(path: string): Promise<LX.Music.LyricInfo | null> => {
// 尝试读取同目录下的同名lrc文件
const filePath = new RegExp('\\' + extname(path) + '$')
let lrcPath = path.replace(filePath, '.lrc')
let stats = await getFileStats(lrcPath)
// console.log(lrcPath, stats)
if (stats && stats.size < 1024 * 1024 * 10) {
const lrcBuf = await readFile(lrcPath)
const { detect } = await import('jschardet')
const { confidence, encoding } = detect(lrcBuf)
console.log('lrc file encoding', confidence, encoding)
if (confidence > 0.8) {
const iconv = await import('iconv-lite')
if (iconv.encodingExists(encoding)) {
const lrc = iconv.decode(lrcBuf, encoding)
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)
// console.log(metadata?.common)
if (!metadata) return null
let lyricInfo = metadata.common.lyrics?.[0]
if (lyricInfo) {
let lyric: string | undefined
if (typeof lyricInfo == 'object') lyric = lyricInfo.text
else if (typeof lyricInfo == 'string') lyric = lyricInfo
if (lyric && lyric.length > 10) {
return { lyric }
}
}
// console.log(metadata)
for (const info of Object.values(metadata.native)) {
for (const ust of info) {
switch (ust.id) {
case 'LYRICS': {
const value = typeof ust.value == 'string' ? ust.value : (ust as IComment).text
if (value && value.length > 10) return { lyric: value }
break
}
case 'USLT': {
const value = ust.value as IComment
if (value.text && value.text.length > 10) return { lyric: value.text }
break
}
}
}
}
return null
}