Merge pull request #15 from lxmusics/dev

Dev
This commit is contained in:
Folltoshe 2023-06-28 00:01:01 +08:00 committed by GitHub
commit fe35454e87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 811 additions and 721 deletions

1143
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "lx-music-desktop", "name": "lx-music-desktop",
"version": "2.3.0-beta.9", "version": "2.3.0-beta.10",
"description": "一个免费的音乐查找助手", "description": "一个免费的音乐查找助手",
"main": "./dist/main.js", "main": "./dist/main.js",
"productName": "lx-music-desktop", "productName": "lx-music-desktop",
@ -221,8 +221,8 @@
"@types/needle": "^3.2.0", "@types/needle": "^3.2.0",
"@types/tunnel": "^0.0.3", "@types/tunnel": "^0.0.3",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "^5.59.11", "@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.59.11", "@typescript-eslint/parser": "^5.60.0",
"@volar/vue-language-plugin-pug": "^1.6.5", "@volar/vue-language-plugin-pug": "^1.6.5",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"browserslist": "^4.21.9", "browserslist": "^4.21.9",
@ -235,12 +235,12 @@
"css-minimizer-webpack-plugin": "^5.0.1", "css-minimizer-webpack-plugin": "^5.0.1",
"del": "^6.1.1", "del": "^6.1.1",
"electron": "^22.3.13", "electron": "^22.3.13",
"electron-builder": "^24.4.0", "electron-builder": "^24.5.0",
"electron-debug": "^3.2.0", "electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-to-chromium": "^1.4.432", "electron-to-chromium": "^1.4.435",
"electron-updater": "^6.1.0", "electron-updater": "^6.1.1",
"eslint": "^8.42.0", "eslint": "^8.43.0",
"eslint-config-standard": "^17.1.0", "eslint-config-standard": "^17.1.0",
"eslint-config-standard-with-typescript": "^35.0.0", "eslint-config-standard-with-typescript": "^35.0.0",
"eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53", "eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53",
@ -248,7 +248,7 @@
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.7.0", "eslint-plugin-n": "^15.7.0",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.14.1", "eslint-plugin-vue": "^9.15.0",
"eslint-plugin-vue-pug": "^0.6.0", "eslint-plugin-vue-pug": "^0.6.0",
"eslint-webpack-plugin": "^4.0.1", "eslint-webpack-plugin": "^4.0.1",
"html-webpack-plugin": "^5.5.3", "html-webpack-plugin": "^5.5.3",
@ -266,7 +266,7 @@
"svg-sprite-loader": "^6.0.11", "svg-sprite-loader": "^6.0.11",
"svg-transform-loader": "^2.0.13", "svg-transform-loader": "^2.0.13",
"svgo-loader": "^4.0.0", "svgo-loader": "^4.0.0",
"terser": "^5.18.0", "terser": "^5.18.1",
"terser-webpack-plugin": "^5.3.9", "terser-webpack-plugin": "^5.3.9",
"ts-loader": "^9.4.3", "ts-loader": "^9.4.3",
"typescript": "^5.1.3", "typescript": "^5.1.3",
@ -285,7 +285,7 @@
"bufferutil": "^4.0.7", "bufferutil": "^4.0.7",
"comlink": "~4.3.1", "comlink": "~4.3.1",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"electron-font-manager": "github:lyswhut/electron-font-manager#60348260de557753c870cc671c56da6f58fd0ef7", "electron-font-manager": "github:lyswhut/electron-font-manager#6d2f5ecf850c4fe34812b9394913680462ee0dae",
"electron-log": "^4.4.8", "electron-log": "^4.4.8",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"font-list": "^1.5.0", "font-list": "^1.5.0",

View File

@ -7,6 +7,7 @@
- Windows、MacOS平台下的字体列表改用原生方式获取现在Windows平台下能显示当前已安装的更多类型字体了MacOS平台未测可用性未知 - Windows、MacOS平台下的字体列表改用原生方式获取现在Windows平台下能显示当前已安装的更多类型字体了MacOS平台未测可用性未知
- 移除桌面歌词窗口透明边距在Linux下的桌面歌词可以完全拖到贴合屏幕边缘了 - 移除桌面歌词窗口透明边距在Linux下的桌面歌词可以完全拖到贴合屏幕边缘了
- 过滤嵌入、下载的翻译、罗马音歌词时间标签,与主歌词时间不匹配的歌词将被丢弃,防止出现原歌词与翻译歌词顺序错乱的问题(#1358
### 修复 ### 修复
@ -20,6 +21,7 @@
- 修复Deepin 20下启用桌面歌词时可能会导致桌面卡死的问题#1288 - 修复Deepin 20下启用桌面歌词时可能会导致桌面卡死的问题#1288
- 修复添加单首歌曲弹窗列表创建按钮无法取消的问题 - 修复添加单首歌曲弹窗列表创建按钮无法取消的问题
- 修复mg歌单搜索歌单播放数量显示问题 - 修复mg歌单搜索歌单播放数量显示问题
- 修复tx翻译歌词解析丢失的问题更新版本后需手动清理一次歌词缓存
### 其他 ### 其他

View File

@ -77,6 +77,8 @@ export const DOWNLOAD_STATUS = {
export const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k'] as const export const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k'] as const
export const QUALITYS_SUPPORT = ['128k', '320k', 'flac', 'flac24bit'] as const
export const SYNC_CODE = { export const SYNC_CODE = {
helloMsg: 'Hello~::^-^::~v3~', helloMsg: 'Hello~::^-^::~v3~',
idPrefix: 'OjppZDo6', idPrefix: 'OjppZDo6',

View File

@ -24,7 +24,8 @@ const defaultSetting: LX.AppSetting = {
'player.startupAutoPlay': false, 'player.startupAutoPlay': false,
'player.togglePlayMethod': 'listLoop', 'player.togglePlayMethod': 'listLoop',
'player.highQuality': false, 'player.firstPlayQuality': '128k',
'player.autoLowerQualityOnError': false,
'player.isShowTaskProgess': true, 'player.isShowTaskProgess': true,
'player.volume': 1, 'player.volume': 1,
'player.isMute': false, 'player.isMute': false,

View File

@ -89,9 +89,14 @@ declare global {
'player.togglePlayMethod': 'listLoop' | 'random' | 'list' | 'singleLoop' | 'none' 'player.togglePlayMethod': 'listLoop' | 'random' | 'list' | 'singleLoop' | 'none'
/** /**
* 320k音 *
*/ */
'player.highQuality': boolean 'player.firstPlayQuality': LX.Quality
/**
*
*/
'player.autoLowerQualityOnError': boolean
/** /**
* *

View File

@ -488,6 +488,9 @@
"setting__play_lyric_roma": "Show lyrics roman", "setting__play_lyric_roma": "Show lyrics roman",
"setting__play_lyric_s2t": "Convert the playing and downloading lyrics to Traditional Chinese", "setting__play_lyric_s2t": "Convert the playing and downloading lyrics to Traditional Chinese",
"setting__play_lyric_transition": "Show lyrics translation", "setting__play_lyric_transition": "Show lyrics translation",
"setting__play_quality_auto_lower_quality_on_error": "Automatically lower the sound quality when getting the current sound quality error",
"setting__play_quality_first": "Play sound quality first",
"setting__play_quality_first_title": "Select priority playback sound quality",
"setting__play_mediaDevice": "Audio output", "setting__play_mediaDevice": "Audio output",
"setting__play_mediaDevice_remove_stop_play": "Pause the song when the current sound output device is changed", "setting__play_mediaDevice_remove_stop_play": "Pause the song when the current sound output device is changed",
"setting__play_mediaDevice_title": "Select a media device for audio output", "setting__play_mediaDevice_title": "Select a media device for audio output",

View File

@ -487,6 +487,9 @@
"setting__play_lyric_roma": "显示歌词罗马音(如果可用)", "setting__play_lyric_roma": "显示歌词罗马音(如果可用)",
"setting__play_lyric_s2t": "将播放与下载的歌词转换为繁体中文", "setting__play_lyric_s2t": "将播放与下载的歌词转换为繁体中文",
"setting__play_lyric_transition": "显示歌词翻译(如果可用)", "setting__play_lyric_transition": "显示歌词翻译(如果可用)",
"setting__play_quality_auto_lower_quality_on_error": "获取当前音质错误时自动降低音质",
"setting__play_quality_first": "优先播放音质",
"setting__play_quality_first_title": "选择优先播放音质",
"setting__play_mediaDevice": "音频输出", "setting__play_mediaDevice": "音频输出",
"setting__play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲", "setting__play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲",
"setting__play_mediaDevice_title": "选择声音输出的媒体设备", "setting__play_mediaDevice_title": "选择声音输出的媒体设备",

View File

@ -488,6 +488,9 @@
"setting__play_lyric_roma": "顯示歌詞羅馬音(如果可用)", "setting__play_lyric_roma": "顯示歌詞羅馬音(如果可用)",
"setting__play_lyric_s2t": "將播放與下載的歌詞轉換為繁體中文", "setting__play_lyric_s2t": "將播放與下載的歌詞轉換為繁體中文",
"setting__play_lyric_transition": "顯示歌詞翻譯(如果可用)", "setting__play_lyric_transition": "顯示歌詞翻譯(如果可用)",
"setting__play_quality_auto_lower_quality_on_error": "獲取當前音質錯誤時自動降低音質",
"setting__play_quality_first": "優先播放音質",
"setting__play_quality_first_title": "選擇優先播放音質",
"setting__play_mediaDevice": "音頻輸出", "setting__play_mediaDevice": "音頻輸出",
"setting__play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲", "setting__play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲",
"setting__play_mediaDevice_title": "選擇聲音輸出的媒體設備", "setting__play_mediaDevice_title": "選擇聲音輸出的媒體設備",

View File

@ -51,11 +51,11 @@ export const getMusicUrl = async({ musicInfo, quality, isRefresh, allowToggleSou
// // return Promise.reject(new Error('该歌曲没有可播放的音频')) // // return Promise.reject(new Error('该歌曲没有可播放的音频'))
// } // }
const targetQuality = quality ?? getPlayQuality(appSetting['player.highQuality'], musicInfo) const targetQuality = quality ?? getPlayQuality(musicInfo)
const cachedUrl = await getStoreMusicUrl(musicInfo, targetQuality) const cachedUrl = await getStoreMusicUrl(musicInfo, targetQuality)
if (cachedUrl && !isRefresh) return cachedUrl if (cachedUrl && !isRefresh) return cachedUrl
return await handleGetOnlineMusicUrl({ musicInfo, quality, onToggleSource, isRefresh, allowToggleSource }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => { return await handleGetOnlineMusicUrl({ musicInfo, quality: targetQuality, onToggleSource, isRefresh, allowToggleSource }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => {
if (targetMusicInfo.id != musicInfo.id && !isFromCache) void saveMusicUrl(targetMusicInfo, targetQuality, url) if (targetMusicInfo.id != musicInfo.id && !isFromCache) void saveMusicUrl(targetMusicInfo, targetQuality, url)
void saveMusicUrl(musicInfo, targetQuality, url) void saveMusicUrl(musicInfo, targetQuality, url)
return url return url

View File

@ -10,6 +10,7 @@ import {
import { appSetting } from '@renderer/store/setting' import { appSetting } from '@renderer/store/setting'
import { langS2T, toNewMusicInfo, toOldMusicInfo } from '@renderer/utils' import { langS2T, toNewMusicInfo, toOldMusicInfo } from '@renderer/utils'
import { requestMsg } from '@renderer/utils/message' import { requestMsg } from '@renderer/utils/message'
import { QUALITYS } from '@common/constants'
const getOtherSourcePromises = new Map() const getOtherSourcePromises = new Map()
@ -138,16 +139,33 @@ export const getCachedLyricInfo = async(musicInfo: LX.Music.MusicInfo): Promise<
return null return null
} }
export const getPlayQuality = (highQuality: boolean, musicInfo: LX.Music.MusicInfoOnline): LX.Quality => { const sliceQualityList = (startQuality: LX.Quality, skipNow?: boolean) => {
let type: LX.Quality = '128k' let startNum = QUALITYS.indexOf(startQuality)
let list = qualityList.value[musicInfo.source] if (skipNow) startNum = QUALITYS.indexOf(startQuality) + 1
if (highQuality && musicInfo.meta._qualitys['320k'] && list && list.includes('320k')) type = '320k'
return type const list = QUALITYS.slice(startNum)
return list
} }
export const getOnlineOtherSourceMusicUrl = async({ musicInfos, quality, onToggleSource, isRefresh, retryedSource = [] }: { export const getPlayQuality = (musicInfo: LX.Music.MusicInfoOnline): LX.Quality => {
let list = qualityList.value[musicInfo.source]
let firstPlayQuality: LX.Quality = appSetting['player.firstPlayQuality']
if (!list) return firstPlayQuality
if (!list.includes(firstPlayQuality)) firstPlayQuality = list[list.length - 1]
const rangeType = sliceQualityList(firstPlayQuality)
for (const quality of rangeType) {
if (musicInfo.meta._qualitys[quality]) return quality
}
return '128k'
}
export const getOnlineOtherSourceMusicUrl = async({ musicInfos, quality, onToggleSource, isRefresh, retryedSource = [], rawQuality = '128k' }: {
musicInfos: LX.Music.MusicInfoOnline[] musicInfos: LX.Music.MusicInfoOnline[]
quality?: LX.Quality quality?: LX.Quality
rawQuality?: LX.Quality
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
isRefresh: boolean isRefresh: boolean
retryedSource?: LX.OnlineSource[] retryedSource?: LX.OnlineSource[]
@ -164,14 +182,23 @@ export const getOnlineOtherSourceMusicUrl = async({ musicInfos, quality, onToggl
if (retryedSource.includes(musicInfo.source)) continue if (retryedSource.includes(musicInfo.source)) continue
retryedSource.push(musicInfo.source) retryedSource.push(musicInfo.source)
if (!assertApiSupport(musicInfo.source)) continue if (!assertApiSupport(musicInfo.source)) continue
itemQuality = quality ?? getPlayQuality(appSetting['player.highQuality'], musicInfo) itemQuality = quality ?? getPlayQuality(musicInfo)
if (!musicInfo.meta._qualitys[itemQuality]) continue if (!musicInfo.meta._qualitys[itemQuality]) continue
console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval) console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval)
onToggleSource(musicInfo) onToggleSource(musicInfo)
break break
} }
if (!musicInfo || !itemQuality) throw new Error(window.i18n.t('toggle_source_failed'))
if (!musicInfo || !itemQuality) {
const rangeType = sliceQualityList(rawQuality, true)
if (appSetting['player.autoLowerQualityOnError'] && rawQuality != '128k' && rangeType.length > 0) {
for (const type of rangeType) {
if (musicInfo.meta._qualitys[type]) return getOnlineOtherSourceMusicUrl({ musicInfos, quality: type, onToggleSource, isRefresh, retryedSource, rawQuality })
}
}
throw new Error(window.i18n.t('toggle_source_failed'))
}
const cachedUrl = await getStoreMusicUrl(musicInfo, itemQuality) const cachedUrl = await getStoreMusicUrl(musicInfo, itemQuality)
if (cachedUrl && !isRefresh) return { url: cachedUrl, musicInfo, quality: itemQuality, isFromCache: true } if (cachedUrl && !isRefresh) return { url: cachedUrl, musicInfo, quality: itemQuality, isFromCache: true }
@ -190,16 +217,17 @@ export const getOnlineOtherSourceMusicUrl = async({ musicInfos, quality, onToggl
}).catch((err: any) => { }).catch((err: any) => {
if (err.message == requestMsg.tooManyRequests) throw err if (err.message == requestMsg.tooManyRequests) throw err
console.log(err) console.log(err)
return getOnlineOtherSourceMusicUrl({ musicInfos, quality, onToggleSource, isRefresh, retryedSource }) return getOnlineOtherSourceMusicUrl({ musicInfos, quality, onToggleSource, isRefresh, retryedSource, rawQuality })
}) })
} }
/** /**
* 线URL * 线URL
*/ */
export const handleGetOnlineMusicUrl = async({ musicInfo, quality, onToggleSource, isRefresh, allowToggleSource }: { export const handleGetOnlineMusicUrl = async({ musicInfo, quality, onToggleSource, isRefresh, allowToggleSource, rawQuality }: {
musicInfo: LX.Music.MusicInfoOnline musicInfo: LX.Music.MusicInfoOnline
quality?: LX.Quality quality?: LX.Quality
rawQuality?: LX.Quality
isRefresh: boolean isRefresh: boolean
allowToggleSource: boolean allowToggleSource: boolean
onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void
@ -209,8 +237,9 @@ export const handleGetOnlineMusicUrl = async({ musicInfo, quality, onToggleSourc
quality: LX.Quality quality: LX.Quality
isFromCache: boolean isFromCache: boolean
}> => { }> => {
// console.log(musicInfo.source) const targetQuality = quality ?? getPlayQuality(musicInfo)
const targetQuality = quality ?? getPlayQuality(appSetting['player.highQuality'], musicInfo) rawQuality = rawQuality ?? targetQuality
console.log(targetQuality, rawQuality)
let reqPromise let reqPromise
try { try {
@ -218,12 +247,29 @@ export const handleGetOnlineMusicUrl = async({ musicInfo, quality, onToggleSourc
} catch (err: any) { } catch (err: any) {
reqPromise = Promise.reject(err) reqPromise = Promise.reject(err)
} }
return reqPromise.then(({ url, type }: { url: string, type: LX.Quality }) => { return reqPromise.then(({ url, type }: { url: string, type: LX.Quality }) => {
return { musicInfo, url, quality: type, isFromCache: false } return { musicInfo, url, quality: type, isFromCache: false }
}).catch(async(err: any) => { }).catch(async(err: any) => {
console.log(err) console.log(err)
const rangeType = sliceQualityList(targetQuality, true)
if (targetQuality != '128k' && rangeType.length > 0 && appSetting['player.autoLowerQualityOnError']) {
for (const type of rangeType) {
if (musicInfo.meta._qualitys[type]) return handleGetOnlineMusicUrl({
musicInfo,
quality: type,
onToggleSource,
isRefresh,
allowToggleSource,
rawQuality
})
}
}
if (!allowToggleSource || err.message == requestMsg.tooManyRequests) throw err if (!allowToggleSource || err.message == requestMsg.tooManyRequests) throw err
onToggleSource() onToggleSource()
// eslint-disable-next-line @typescript-eslint/promise-function-async // eslint-disable-next-line @typescript-eslint/promise-function-async
return await getOtherSource(musicInfo).then(otherSource => { return await getOtherSource(musicInfo).then(otherSource => {
console.log('find otherSource', otherSource) console.log('find otherSource', otherSource)
@ -231,7 +277,8 @@ export const handleGetOnlineMusicUrl = async({ musicInfo, quality, onToggleSourc
return getOnlineOtherSourceMusicUrl({ return getOnlineOtherSourceMusicUrl({
musicInfos: [...otherSource], musicInfos: [...otherSource],
onToggleSource, onToggleSource,
quality, quality: rawQuality ?? quality,
rawQuality: rawQuality ?? quality,
isRefresh, isRefresh,
retryedSource: [musicInfo.source], retryedSource: [musicInfo.source],
}) })

View File

@ -148,19 +148,22 @@ 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]) => {
let lyric: null | string = null const lrcData = {
if (lyrics?.lyric) { lrc: '',
lyric = fixKgLyric(lyrics.lyric) tlrc: null as string | null,
if (appSetting['download.isEmbedLyricT'] && lyrics.tlyric) lyric += '\n' + lyrics.tlyric + '\n' rlrc: null as string | null,
if (appSetting['download.isEmbedLyricR'] && lyrics.rlyric) lyric += '\n' + lyrics.rlyric + '\n' }
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, { void window.lx.worker.download.writeMeta(downloadInfo.metadata.filePath, {
title: downloadInfo.metadata.musicInfo.name, title: downloadInfo.metadata.musicInfo.name,
artist: downloadInfo.metadata.musicInfo.singer, artist: downloadInfo.metadata.musicInfo.singer,
album: downloadInfo.metadata.musicInfo.meta.albumName, album: downloadInfo.metadata.musicInfo.meta.albumName,
APIC: imgUrl, APIC: imgUrl,
lyrics: lyric, }, lrcData)
})
}) })
} }
@ -177,11 +180,13 @@ 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)
let lyric = lrcs.lyric const lrcData = {
if (appSetting['download.isDownloadTLrc'] && lrcs.tlyric) lyric += '\n' + lrcs.tlyric + '\n' lrc: lrcs.lyric,
if (appSetting['download.isDownloadRLrc'] && lrcs.rlyric) lyric += '\n' + lrcs.rlyric + '\n' tlrc: appSetting['download.isDownloadTLrc'] && lrcs.tlyric ? lrcs.tlyric : null,
void window.lx.worker.download.saveLrc(downloadInfo.metadata.filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'), rlrc: appSetting['download.isDownloadRLrc'] && lrcs.rlyric ? lrcs.rlyric : null,
lyric, appSetting['download.lrcFormat']) }
void window.lx.worker.download.saveLrc(lrcData, downloadInfo.metadata.filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'),
appSetting['download.lrcFormat'])
} }
}) })
} }

View File

@ -1,85 +0,0 @@
import { getMusicInfosByList } from './musicInfo'
import { createHttpFetch } from './util'
export default {
/**
* 获取歌手信息
* @param {*} id
*/
getInfo(id) {
if (id == 0) throw new Error('歌手不存在') // kg源某些歌曲在歌手没被kg收录时返回的歌手id为0
return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/info?singerid=${id}`).then(body => {
if (!body) throw new Error('get singer info faild.')
return {
source: 'kg',
id: body.singerid,
info: {
name: body.singername,
desc: body.intro,
avatar: body.imgurl.replace('{size}', 480),
gender: body.grade === 1 ? 'man' : 'woman',
},
count: {
music: body.songcount,
album: body.albumcount,
},
}
})
},
/**
* 获取歌手专辑列表
* @param {*} id
* @param {*} page
* @param {*} limit
*/
getAlbumList(id, page = 1, limit = 10) {
if (id == 0) throw new Error('歌手不存在')
return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/album?singerid=${id}&page=${page}&pagesize=${limit}`).then(body => {
if (!body.info) throw new Error('get singer album list faild.')
const list = this.filterAlbumList(body.info)
return {
source: 'kg',
list,
limit,
page,
total: body.total,
}
})
},
/**
* 获取歌手歌曲列表
* @param {*} id
* @param {*} page
* @param {*} limit
*/
async getSongList(id, page = 1, limit = 100) {
if (id == 0) throw new Error('歌手不存在')
const body = await createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/song?singerid=${id}&page=${page}&pagesize=${limit}`)
if (!body.info) throw new Error('get singer song list faild.')
const list = await getMusicInfosByList(body.info)
return {
source: 'kg',
list,
limit,
page,
total: body.total,
}
},
filterAlbumList(raw) {
return raw.map(item => {
return {
id: item.albumid,
count: item.songcount,
info: {
name: item.albumname,
author: item.singername,
img: item.replaceAll('{size}', '480'),
desc: item.intro,
},
}
})
},
}

View File

@ -100,8 +100,10 @@ const parseTools = {
return str.replace(/^[\S\s]*?LyricContent="/, '').replace(/"\/>[\S\s]*?$/, '') return str.replace(/^[\S\s]*?LyricContent="/, '').replace(/"\/>[\S\s]*?$/, '')
}, },
getIntv(interval) { getIntv(interval) {
let [m, s, ms] = interval.split(/:|\./) if (!interval.includes('.')) interval += '.0'
let arr = interval.split(/:|\./)
while (arr.length < 3) arr.unshift('0')
const [m, s, ms] = arr
return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms) return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms)
}, },
fixRlrcTimeTag(rlrc, lrc) { fixRlrcTimeTag(rlrc, lrc) {
@ -123,7 +125,7 @@ const parseTools = {
const lrcLineResult = this.rxps.lineTime2.exec(lrcLine) const lrcLineResult = this.rxps.lineTime2.exec(lrcLine)
if (!lrcLineResult) continue if (!lrcLineResult) continue
const t2 = this.getIntv(lrcLineResult[1]) const t2 = this.getIntv(lrcLineResult[1])
if (Math.abs(t1 - t2) < 10) { if (Math.abs(t1 - t2) < 100) {
newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0])) newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0]))
break break
} }
@ -140,21 +142,25 @@ const parseTools = {
const tlrcLines = tlrc.split('\n') const tlrcLines = tlrc.split('\n')
let lrcLines = lrc.split('\n') let lrcLines = lrc.split('\n')
// let temp = [] // let temp = []
const timeTagRxp = /^\[[\d:.]+\]/
let newLrc = [] let newLrc = []
tlrcLines.forEach((line) => { tlrcLines.forEach((line) => {
const result = timeTagRxp.exec(line) const result = this.rxps.lineTime2.exec(line)
if (!result) return if (!result) return
const words = line.replace(timeTagRxp, '') const words = line.replace(this.rxps.lineTime2, '')
if (!words.trim()) return if (!words.trim()) return
const tag = result[0].replace(/\d]/, '').replace(this.rxps.timeLabelFixRxp, '') let time = result[1]
if (time.includes('.')) {
time += ''.padStart(3 - time.split('.')[1].length, '0')
}
const t1 = this.getIntv(time)
while (lrcLines.length) { while (lrcLines.length) {
const lrcLine = lrcLines.shift() const lrcLine = lrcLines.shift()
const lrcLineResult = timeTagRxp.exec(lrcLine) const lrcLineResult = this.rxps.lineTime2.exec(lrcLine)
if (!lrcLineResult) continue if (!lrcLineResult) continue
if (lrcLineResult[0].includes(tag)) { const t2 = this.getIntv(lrcLineResult[1])
newLrc.push(line.replace(timeTagRxp, lrcLineResult[0])) if (Math.abs(t1 - t2) < 100) {
newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0]))
break break
} }
// temp.push(line) // temp.push(line)

View File

@ -126,7 +126,10 @@ const parseTools = {
}) })
}, },
getIntv(interval) { getIntv(interval) {
let [m, s, ms] = interval.split(/:|\./) if (!interval.includes('.')) interval += '.0'
let arr = interval.split(/:|\./)
while (arr.length < 3) arr.unshift('0')
const [m, s, ms] = arr
return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms) return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms)
}, },
fixTimeTag(lrc, targetlrc) { fixTimeTag(lrc, targetlrc) {

View File

@ -15,12 +15,18 @@ dd
base-checkbox(id="setting_player_lyric_s2t" :model-value="appSetting['player.isS2t']" :label="$t('setting__play_lyric_s2t')" @update:model-value="updateSetting({'player.isS2t': $event})") base-checkbox(id="setting_player_lyric_s2t" :model-value="appSetting['player.isS2t']" :label="$t('setting__play_lyric_s2t')" @update:model-value="updateSetting({'player.isS2t': $event})")
.gap-top .gap-top
base-checkbox(id="setting_player_lyric_play_lxlrc" :model-value="appSetting['player.isPlayLxlrc']" :label="$t('setting__play_lyric_lxlrc')" @update:model-value="updateSetting({'player.isPlayLxlrc': $event})") base-checkbox(id="setting_player_lyric_play_lxlrc" :model-value="appSetting['player.isPlayLxlrc']" :label="$t('setting__play_lyric_lxlrc')" @update:model-value="updateSetting({'player.isPlayLxlrc': $event})")
.gap-top
base-checkbox(id="setting_player_highQuality" :model-value="appSetting['player.highQuality']" :label="$t('setting__play_quality')" @update:model-value="updateSetting({'player.highQuality': $event})")
.gap-top .gap-top
base-checkbox(id="setting_player_showTaskProgess" :model-value="appSetting['player.isShowTaskProgess']" :label="$t('setting__play_task_bar')" @update:model-value="updateSetting({'player.isShowTaskProgess': $event})") base-checkbox(id="setting_player_showTaskProgess" :model-value="appSetting['player.isShowTaskProgess']" :label="$t('setting__play_task_bar')" @update:model-value="updateSetting({'player.isShowTaskProgess': $event})")
.gap-top .gap-top
base-checkbox(id="setting_player_isMediaDeviceRemovedStopPlay" :model-value="appSetting['player.isMediaDeviceRemovedStopPlay']" :label="$t('setting__play_mediaDevice_remove_stop_play')" @update:model-value="updateSetting({'player.isMediaDeviceRemovedStopPlay': $event})") base-checkbox(id="setting_player_isMediaDeviceRemovedStopPlay" :model-value="appSetting['player.isMediaDeviceRemovedStopPlay']" :label="$t('setting__play_mediaDevice_remove_stop_play')" @update:model-value="updateSetting({'player.isMediaDeviceRemovedStopPlay': $event})")
.gap-top
base-checkbox(id="setting__play_quality_auto_lower_quality_on_error" :model-value="appSetting['player.autoLowerQualityOnError']" :label="$t('setting__play_quality_auto_lower_quality_on_error')" @update:model-value="updateSetting({'player.autoLowerQualityOnError': $event})")
dd(:aria-label="$t('setting__play_quality_first_title')")
h3#play_firstPlayQuality {{ $t('setting__play_quality_first') }}
div
base-selection.gap-left(:list="playQualityList" item-key="qualityId" item-name="label" :model-value="appSetting['player.firstPlayQuality']" @update:model-value="updateSetting({'player.firstPlayQuality': $event})")
dd(:aria-label="$t('setting__play_mediaDevice_title')") dd(:aria-label="$t('setting__play_mediaDevice_title')")
h3#play_mediaDevice {{ $t('setting__play_mediaDevice') }} h3#play_mediaDevice {{ $t('setting__play_mediaDevice') }}
div div
@ -33,7 +39,7 @@ import { hasInitedAdvancedAudioFeatures } from '@renderer/plugins/player'
import { dialog } from '@renderer/plugins/Dialog' import { dialog } from '@renderer/plugins/Dialog'
import { useI18n } from '@renderer/plugins/i18n' import { useI18n } from '@renderer/plugins/i18n'
import { appSetting, updateSetting } from '@renderer/store/setting' import { appSetting, updateSetting } from '@renderer/store/setting'
import { QUALITYS_SUPPORT } from '@common/constants'
export default { export default {
name: 'SettingPlay', name: 'SettingPlay',
@ -83,12 +89,35 @@ export default {
}) })
const getQualityTypeName = (quality) => {
switch (quality) {
case 'flac24bit':
return t('download__lossless') + ' FLAC Hires'
case 'flac':
case 'ape':
case 'wav':
return t('download__lossless') + ' ' + quality.toUpperCase()
case '320k':
return t('download__high_quality') + ' ' + quality.toUpperCase()
case '192k':
case '128k':
return t('download__normal') + ' ' + quality.toUpperCase()
}
}
const playQualityList = ref([])
QUALITYS_SUPPORT.forEach(item => {
playQualityList.value.push({ qualityId: item, label: getQualityTypeName(item) })
})
return { return {
appSetting, appSetting,
updateSetting, updateSetting,
mediaDevices, mediaDevices,
mediaDeviceId, mediaDeviceId,
handleMediaDeviceIdChnage, handleMediaDeviceIdChnage,
playQualityList,
} }
}, },
} }

View File

@ -1,7 +1,8 @@
import { setMeta } from '@common/utils/musicMeta' import { setMeta } from '@common/utils/musicMeta'
import { mergeLyrics } from './lrcTool'
export const writeMeta = (filePath: string, meta: LX.Music.MusicFileMeta) => { export const writeMeta = (filePath: string, meta: Omit<LX.Music.MusicFileMeta, 'lyrics'>, lyricData: { lrc: string, tlrc: string | null, rlrc: string | null }) => {
setMeta(filePath, meta) setMeta(filePath, { ...meta, lyrics: mergeLyrics(lyricData.lrc, lyricData.tlrc, lyricData.rlrc) || null })
} }
export { saveLrc } from './utils' export { saveLrc } from './utils'

View File

@ -0,0 +1,74 @@
const timeFieldExp = /^(?:\[[\d:.]+\])+/g
const timeExp = /\d{1,3}(:\d{1,3}){0,2}(?:\.\d{1,3})/g
const t_rxp_1 = /^0+(\d+)/
const t_rxp_2 = /:0+(\d+)/g
const t_rxp_3 = /\.0+(\d+)/
const formatTimeLabel = (label: string) => {
return label.replace(t_rxp_1, '$1')
.replace(t_rxp_2, ':$1')
.replace(t_rxp_3, '.$1')
}
const filterExtendedLyricLabel = (lrcTimeLabels: Set<string>, extendedLyric: string) => {
const extendedLines = extendedLyric.split(/\r\n|\n|\r/)
const lines: string[] = []
for (let i = 0; i < extendedLines.length; i++) {
let line = extendedLines[i].trim()
let result = timeFieldExp.exec(line)
if (!result) continue
const timeField = result[0]
const text = line.replace(timeFieldExp, '').trim()
if (!text) continue
let times = timeField.match(timeExp)
if (times == null) continue
const newTimes = times.filter(time => {
const timeStr = formatTimeLabel(time)
return lrcTimeLabels.has(timeStr)
})
if (newTimes.length != times.length) {
if (!newTimes.length) continue
line = `[${newTimes.join('][')}]${text}`
}
lines.push(line)
}
return lines.join('\n')
}
const parseLrcTimeLabel = (lrc: string) => {
const lines = lrc.split(/\r\n|\n|\r/)
const linesSet = new Set<string>()
const length = lines.length
for (let i = 0; i < length; i++) {
const line = lines[i].trim()
let result = timeFieldExp.exec(line)
if (result) {
const timeField = result[0]
const text = line.replace(timeFieldExp, '').trim()
if (text) {
const times = timeField.match(timeExp)
if (times == null) continue
for (let time of times) {
linesSet.add(formatTimeLabel(time))
}
}
}
}
return linesSet
}
export const mergeLyrics = (lrc: string, tlrc: string | null, rlrc: string | null) => {
if (!tlrc && !rlrc) return lrc
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)
return lrc
}

View File

@ -1,6 +1,7 @@
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 { joinPath } from '@common/utils/nodejs' import { joinPath } from '@common/utils/nodejs'
import { mergeLyrics } from './lrcTool'
import fs from 'fs' import fs from 'fs'
/** /**
@ -9,8 +10,9 @@ import fs from 'fs'
* @param {*} lrc * @param {*} lrc
* @param {*} format * @param {*} format
*/ */
export const saveLrc = async(filePath: string, lrc: string, format: LX.LyricFormat) => { export const saveLrc = async(lrcData: { lrc: string, tlrc: string | null, rlrc: string | null }, filePath: string, format: LX.LyricFormat) => {
const iconv = await import('iconv-lite') const iconv = await import('iconv-lite')
const lrc = mergeLyrics(lrcData.lrc, lrcData.tlrc, lrcData.rlrc)
switch (format) { switch (format) {
case 'gbk': case 'gbk':
fs.writeFile(filePath, iconv.encode(lrc, 'gbk', { addBOM: true }), err => { fs.writeFile(filePath, iconv.encode(lrc, 'gbk', { addBOM: true }), err => {