241 lines
7.4 KiB
TypeScript
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
|
|
}
|