新增下载的歌曲按列表名分组的功能(#2145)
parent
63b2c2fb2f
commit
6f74003e14
|
@ -5,6 +5,7 @@ Linux 系统至少需要 GLIBC_2.29 版本才能运行,
|
|||
|
||||
### 新增
|
||||
|
||||
- 新增下载的歌曲按列表名分组的功能,默认关闭,可以到 设置-下载设置-将文件保存到以对应列表命名的子目录中 启用(#2145)
|
||||
- 新增托盘图标颜色 跟随系统亮暗模式 设置,可以在 设置-其他 启用 (#2016)
|
||||
- 支持本地同名 `krc` 格式歌词文件的读取(#2053)
|
||||
- Open API 新增播放器播放/暂停、切歌、收藏当前播放歌曲调用,详情看开放API文档 (原始 PR #2077)
|
||||
|
|
|
@ -107,6 +107,7 @@ const defaultSetting: LX.AppSetting = {
|
|||
'list.actionButtonsVisible': false,
|
||||
|
||||
'download.enable': false,
|
||||
'download.isSavePathGroupByListName': false,
|
||||
'download.savePath': path.join(os.homedir(), 'Desktop'),
|
||||
'download.fileName': '歌名 - 歌手',
|
||||
'download.maxDownloadNum': 3,
|
||||
|
|
|
@ -485,6 +485,11 @@ declare global {
|
|||
*/
|
||||
'download.enable': boolean
|
||||
|
||||
/**
|
||||
* 按列表名分组保存
|
||||
*/
|
||||
'download.isSavePathGroupByListName': boolean
|
||||
|
||||
/**
|
||||
* 下载路径
|
||||
*/
|
||||
|
|
|
@ -59,6 +59,7 @@ declare global {
|
|||
ext: FileExt
|
||||
fileName: string
|
||||
filePath: string
|
||||
listId?: string
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,25 @@ export const checkPath = async(path: string): Promise<boolean> => {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径并创建目录
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
export const checkAndCreateDir = async(path: string) => {
|
||||
return fs.promises.access(path, fs.constants.F_OK | fs.constants.W_OK)
|
||||
.catch(async(err: NodeJS.ErrnoException) => {
|
||||
if (err.code != 'ENOENT') throw err as Error
|
||||
return fs.promises.mkdir(path, { recursive: true })
|
||||
})
|
||||
.then(() => true)
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const getFileStats = async(path: string): Promise<fs.Stats | null> => {
|
||||
return new Promise(resolve => {
|
||||
if (!path) {
|
||||
|
|
|
@ -130,3 +130,21 @@ export const filterMusicList = <T extends LX.Music.MusicInfo>(list: T[]): T[] =>
|
|||
return true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const MAX_NAME_LENGTH = 80
|
||||
const MAX_FILE_NAME_LENGTH = 150
|
||||
export const clipNameLength = (name: string) => {
|
||||
if (name.length <= MAX_NAME_LENGTH || !name.includes('、')) return name
|
||||
const names = name.split('、')
|
||||
let newName = names.shift()!
|
||||
for (const name of names) {
|
||||
if (newName.length + name.length > MAX_NAME_LENGTH) break
|
||||
newName = newName + '、' + name
|
||||
}
|
||||
return newName
|
||||
}
|
||||
export const clipFileNameLength = (name: string) => {
|
||||
return name.length > MAX_FILE_NAME_LENGTH ? name.substring(0, MAX_FILE_NAME_LENGTH) : name
|
||||
}
|
||||
|
||||
|
|
|
@ -586,6 +586,7 @@
|
|||
"setting__update_try_auto_update": "Attempt to download updates automatically when a new version is found",
|
||||
"setting__update_unknown": "Unknown",
|
||||
"setting__update_unknown_tip": "❓ Failed to obtain the latest version information, it is recommended to go to the About interface to open the project release address to check whether the current version is the latest",
|
||||
"setting_download_save_group_list_name": "Save files to a subdirectory named after the corresponding list",
|
||||
"setting_sync_status_enabled": "connected",
|
||||
"song_list": "Playlists",
|
||||
"songlist__import_input_btn_confirm": "Open",
|
||||
|
|
|
@ -586,6 +586,7 @@
|
|||
"setting__update_try_auto_update": "发现新版本时尝试自动下载更新",
|
||||
"setting__update_unknown": "未知",
|
||||
"setting__update_unknown_tip": "❓ 获取最新版本信息失败,建议去「关于」页面打开项目发布地址查看当前版本是否最新",
|
||||
"setting_download_save_group_list_name": "将文件保存到以对应列表命名的子目录中",
|
||||
"setting_sync_status_enabled": "已连接",
|
||||
"song_list": "歌单",
|
||||
"songlist__import_input_btn_confirm": "打开",
|
||||
|
@ -688,7 +689,7 @@
|
|||
"theme_more_btn_show": "更多主题",
|
||||
"theme_naruto": "木叶之村",
|
||||
"theme_orange": "橙黄橘绿",
|
||||
"theme_pink": "粉装玉琢",
|
||||
"theme_pink": "粉装玉琢",
|
||||
"theme_purple": "重斤球紫",
|
||||
"theme_red": "热情似火",
|
||||
"theme_selector_modal__dark_title": "暗色主题",
|
||||
|
|
|
@ -586,6 +586,7 @@
|
|||
"setting__update_try_auto_update": "發現新版本時嘗試自動下載更新",
|
||||
"setting__update_unknown": "未知",
|
||||
"setting__update_unknown_tip": "❓ 取得最新版本資訊失敗,建議去關於介面開啟專案發佈位址查看目前版本是否最新",
|
||||
"setting_download_save_group_list_name": "將檔案儲存到以對應清單命名的子目錄中",
|
||||
"setting_sync_status_enabled": "已連接",
|
||||
"song_list": "歌單",
|
||||
"songlist__import_input_btn_confirm": "打開",
|
||||
|
|
|
@ -23,6 +23,10 @@ export default {
|
|||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
listId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bgClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
@ -51,7 +55,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
handleClick(quality) {
|
||||
void createDownloadTasks([this.musicInfo], quality)
|
||||
void createDownloadTasks([this.musicInfo], quality, this.listId)
|
||||
this.handleClose()
|
||||
},
|
||||
handleClose() {
|
||||
|
|
|
@ -23,6 +23,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
listId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
list: {
|
||||
type: Array,
|
||||
default() {
|
||||
|
@ -37,7 +41,7 @@ export default {
|
|||
emits: ['update:show', 'confirm'],
|
||||
methods: {
|
||||
handleClick(quality) {
|
||||
void createDownloadTasks(this.list.filter(item => item.source != 'local'), quality)
|
||||
void createDownloadTasks(this.list.filter(item => item.source != 'local'), quality, this.listId)
|
||||
this.handleClose()
|
||||
this.$emit('confirm')
|
||||
},
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { appSetting } from '@renderer/store/setting'
|
||||
import { getDownloadFilePath } from '@renderer/utils/music'
|
||||
|
||||
import {
|
||||
|
@ -7,6 +6,7 @@ import {
|
|||
getLyricInfo as getOnlineLyricInfo,
|
||||
} from './online'
|
||||
import { buildLyricInfo, getCachedLyricInfo } from './utils'
|
||||
import { buildSavePath } from '@renderer/store/download/utils'
|
||||
|
||||
export const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {
|
||||
musicInfo: LX.Download.ListItem
|
||||
|
@ -15,7 +15,7 @@ export const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = tru
|
|||
allowToggleSource?: boolean
|
||||
}): Promise<string> => {
|
||||
if (!isRefresh) {
|
||||
const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath'])
|
||||
const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo))
|
||||
if (path) return path
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ export const getPicUrl = async({ musicInfo, isRefresh, listId, onToggleSource =
|
|||
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
||||
}): Promise<string> => {
|
||||
if (!isRefresh) {
|
||||
const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath'])
|
||||
const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo))
|
||||
if (path) {
|
||||
const pic = await window.lx.worker.main.getMusicFilePic(path)
|
||||
if (pic) return pic
|
||||
|
@ -62,7 +62,7 @@ export const getLyricInfo = async({ musicInfo, isRefresh, onToggleSource = () =>
|
|||
onToggleSource,
|
||||
}).catch(async() => {
|
||||
// 尝试读取文件内歌词
|
||||
const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath'])
|
||||
const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo))
|
||||
if (path) {
|
||||
const rawlrcInfo = await window.lx.worker.main.getMusicFileLyric(path)
|
||||
if (rawlrcInfo) return buildLyricInfo(rawlrcInfo)
|
||||
|
|
|
@ -29,7 +29,7 @@ export const filterList = async({ playedList, listId, list, playerMusicInfo, isN
|
|||
listId,
|
||||
list: list.map(m => toRaw(m)),
|
||||
playedList: toRaw(playedList),
|
||||
savePath: appSetting['download.savePath'],
|
||||
// savePath: appSetting['download.savePath'],
|
||||
playerMusicInfo: toRaw(playerMusicInfo),
|
||||
dislikeInfo: { names: toRaw(dislikeInfo.names), musicNames: toRaw(dislikeInfo.musicNames), singerNames: toRaw(dislikeInfo.singerNames) },
|
||||
isNext,
|
||||
|
|
|
@ -16,6 +16,7 @@ import { proxyCallback } from '@renderer/worker/utils'
|
|||
import { arrPush, arrUnshift, joinPath } from '@renderer/utils'
|
||||
import { DOWNLOAD_STATUS } from '@common/constants'
|
||||
import { proxy } from '../index'
|
||||
import { buildSavePath } from './utils'
|
||||
|
||||
const waitingUpdateTasks = new Map<string, LX.Download.ListItem>()
|
||||
let timer: NodeJS.Timeout | null = null
|
||||
|
@ -271,12 +272,13 @@ const handleStartTask = async(downloadInfo: LX.Download.ListItem) => {
|
|||
if (downloadInfo.status != DOWNLOAD_STATUS.RUN) return
|
||||
}
|
||||
|
||||
const filePath = joinPath(appSetting['download.savePath'], downloadInfo.metadata.fileName)
|
||||
const savePath = buildSavePath(downloadInfo)
|
||||
const filePath = joinPath(savePath, downloadInfo.metadata.fileName)
|
||||
if (downloadInfo.metadata.filePath != filePath) updateFilePath(downloadInfo, filePath)
|
||||
|
||||
setStatusText(downloadInfo, window.i18n.t('download_status_start'))
|
||||
|
||||
await window.lx.worker.download.startTask(toRaw(downloadInfo), appSetting['download.savePath'], appSetting['download.skipExistFile'], proxyCallback((event: LX.Download.DownloadTaskActions) => {
|
||||
await window.lx.worker.download.startTask(toRaw(downloadInfo), savePath, appSetting['download.skipExistFile'], proxyCallback((event: LX.Download.DownloadTaskActions) => {
|
||||
// console.log(event)
|
||||
switch (event.action) {
|
||||
case 'start':
|
||||
|
@ -357,12 +359,11 @@ const filterTask = (list: LX.Download.ListItem[]) => {
|
|||
* @param list 要下载的歌曲
|
||||
* @param quality 下载音质
|
||||
*/
|
||||
export const createDownloadTasks = async(list: LX.Music.MusicInfoOnline[], quality: LX.Quality) => {
|
||||
export const createDownloadTasks = async(list: LX.Music.MusicInfoOnline[], quality: LX.Quality, listId?: string) => {
|
||||
if (!list.length) return
|
||||
const tasks = filterTask(await window.lx.worker.download.createDownloadTasks(list, quality,
|
||||
appSetting['download.savePath'],
|
||||
appSetting['download.fileName'],
|
||||
toRaw(qualityList.value)),
|
||||
toRaw(qualityList.value), listId),
|
||||
)
|
||||
|
||||
if (tasks.length) await addTasks(tasks)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { appSetting } from '@renderer/store/setting'
|
||||
import { defaultList, loveList, userLists } from '@renderer/store/list/listManage'
|
||||
import { filterFileName } from '@common/utils/common'
|
||||
import { clipFileNameLength } from '@common/utils/tools'
|
||||
import { joinPath } from '@common/utils/nodejs'
|
||||
|
||||
export const buildSavePath = (musicInfo: LX.Download.ListItem) => {
|
||||
let savePath = appSetting['download.savePath']
|
||||
if (appSetting['download.isSavePathGroupByListName']) {
|
||||
let dirName: string | undefined
|
||||
const listId = musicInfo.metadata.listId
|
||||
switch (listId) {
|
||||
case defaultList.id:
|
||||
dirName = window.i18n.t(defaultList.name)
|
||||
break
|
||||
case loveList.id:
|
||||
dirName = window.i18n.t(loveList.name)
|
||||
break
|
||||
default:
|
||||
dirName = userLists.find(list => list.id === listId)?.name
|
||||
break
|
||||
}
|
||||
if (dirName) dirName = filterFileName(dirName)
|
||||
savePath = joinPath(savePath, clipFileNameLength(dirName ?? window.i18n.t(defaultList.name)))
|
||||
}
|
||||
return savePath
|
||||
}
|
|
@ -94,8 +94,8 @@
|
|||
v-model:show="isShowListAddMultiple" :from-list-id="listId"
|
||||
:is-move="isMoveMultiple" :music-list="selectedList" :exclude-list-id="excludeListIds" teleport="#view" @confirm="removeAllSelect"
|
||||
/>
|
||||
<common-download-modal v-model:show="isShowDownload" :music-info="selectedDownloadMusicInfo" teleport="#view" />
|
||||
<common-download-multiple-modal v-model:show="isShowDownloadMultiple" :list="selectedList" teleport="#view" @confirm="removeAllSelect" />
|
||||
<common-download-modal v-model:show="isShowDownload" :music-info="selectedDownloadMusicInfo" teleport="#view" :list-id="listId" />
|
||||
<common-download-multiple-modal v-model:show="isShowDownloadMultiple" :list="selectedList" teleport="#view" :list-id="listId" @confirm="removeAllSelect" />
|
||||
<search-list :list="list" :visible="isShowSearchBar" @action="handleMusicSearchAction" />
|
||||
<music-sort-modal v-model:show="isShowMusicSortModal" :music-info="selectedSortMusicInfo" :selected-num="selectedNum" @confirm="sortMusic" />
|
||||
<music-toggle-modal v-model:show="isShowMusicToggleModal" :music-info="selectedToggleMusicInfo" @toggle="toggleSource" />
|
||||
|
|
|
@ -5,6 +5,8 @@ dd
|
|||
base-checkbox(id="setting_download_enable" :model-value="appSetting['download.enable']" :label="$t('setting__download_enable')" @update:model-value="updateSetting({'download.enable': $event})")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_download_skip_exist_file" :model-value="appSetting['download.skipExistFile']" :label="$t('setting__download_skip_exist_file')" @update:model-value="updateSetting({'download.skipExistFile': $event})")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_download_save_group_list_name" :model-value="appSetting['download.isSavePathGroupByListName']" :label="$t('setting_download_save_group_list_name')" @update:model-value="updateSetting({'download.isSavePathGroupByListName': $event})")
|
||||
dd(:aria-label="$t('setting__download_path_title')")
|
||||
h3#download_path {{ $t('setting__download_path') }}
|
||||
div
|
||||
|
|
|
@ -8,7 +8,7 @@ import { createDownloadInfo } from './utils'
|
|||
// assertApiSupport,
|
||||
// getExt,
|
||||
// } from '..'
|
||||
import { checkPath, getFileStats, removeFile } from '@common/utils/nodejs'
|
||||
import { checkAndCreateDir, checkPath, getFileStats, removeFile } from '@common/utils/nodejs'
|
||||
import { DOWNLOAD_STATUS } from '@common/constants'
|
||||
// import { download as eventDownloadNames } from '@renderer/event/names'
|
||||
|
||||
|
@ -40,12 +40,12 @@ const sendAction = (id: string, action: LX.Download.DownloadTaskActions) => {
|
|||
export const createDownloadTasks = (
|
||||
list: LX.Music.MusicInfoOnline[],
|
||||
quality: LX.Quality,
|
||||
savePath: string,
|
||||
fileNameFormat: string,
|
||||
qualityList: LX.QualityList,
|
||||
listId?: string,
|
||||
): LX.Download.ListItem[] => {
|
||||
return list.map(musicInfo => {
|
||||
return createDownloadInfo(musicInfo, quality, fileNameFormat, savePath, qualityList)
|
||||
return createDownloadInfo(musicInfo, quality, fileNameFormat, qualityList, listId)
|
||||
}).filter(task => task)
|
||||
// commit('addTasks', { list: taskList, addMusicLocationType: rootState.setting.list.addMusicLocationType })
|
||||
// let result = getStartTask(downloadList, DOWNLOAD_STATUS, rootState.setting.download.maxDownloadNum)
|
||||
|
@ -60,7 +60,7 @@ const createTask = async(downloadInfo: LX.Download.ListItem, savePath: string, s
|
|||
// 开始任务
|
||||
/* commit('onStart', downloadInfo)
|
||||
commit('setStatusText', { downloadInfo, text: '任务初始化中' }) */
|
||||
if (!await checkPath(savePath)) {
|
||||
if (!await checkAndCreateDir(savePath)) {
|
||||
sendAction(downloadInfo.id, {
|
||||
action: 'error',
|
||||
data: {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { DOWNLOAD_STATUS, QUALITYS } from '@common/constants'
|
||||
import { filterFileName } from '@common/utils/common'
|
||||
import { joinPath } from '@common/utils/nodejs'
|
||||
import { mergeLyrics } from './lrcTool'
|
||||
import fs from 'fs'
|
||||
import { clipFileNameLength, clipNameLength } from '@common/utils/tools'
|
||||
|
||||
/**
|
||||
* 保存歌词文件
|
||||
|
@ -65,22 +65,8 @@ export const getMusicType = (musicInfo: LX.Music.MusicInfoOnline, type: LX.Quali
|
|||
// const checkExistList = (list: LX.Download.ListItem[], musicInfo: LX.Music.MusicInfo, type: LX.Quality, ext: string): boolean => {
|
||||
// return list.some(s => s.id === musicInfo.id && (s.metadata.type === type || s.metadata.ext === ext))
|
||||
// }
|
||||
const MAX_NAME_LENGTH = 80
|
||||
const MAX_FILE_NAME_LENGTH = 150
|
||||
const clipNameLength = (name: string) => {
|
||||
if (name.length <= MAX_NAME_LENGTH || !name.includes('、')) return name
|
||||
const names = name.split('、')
|
||||
let newName = names.shift()!
|
||||
for (const name of names) {
|
||||
if (newName.length + name.length > MAX_NAME_LENGTH) break
|
||||
newName = newName + '、' + name
|
||||
}
|
||||
return newName
|
||||
}
|
||||
const clipFileNameLength = (name: string) => {
|
||||
return name.length > MAX_FILE_NAME_LENGTH ? name.substring(0, MAX_FILE_NAME_LENGTH) : name
|
||||
}
|
||||
export const createDownloadInfo = (musicInfo: LX.Music.MusicInfoOnline, type: LX.Quality, fileName: string, savePath: string, qualityList: LX.QualityList) => {
|
||||
|
||||
export const createDownloadInfo = (musicInfo: LX.Music.MusicInfoOnline, type: LX.Quality, fileName: string, qualityList: LX.QualityList, listId?: string) => {
|
||||
type = getMusicType(musicInfo, type, qualityList)
|
||||
let ext = getExt(type)
|
||||
const key = `${musicInfo.id}_${type}_${ext}`
|
||||
|
@ -101,12 +87,13 @@ export const createDownloadInfo = (musicInfo: LX.Music.MusicInfoOnline, type: LX
|
|||
quality: type,
|
||||
ext,
|
||||
filePath: '',
|
||||
listId,
|
||||
fileName: filterFileName(`${clipFileNameLength(fileName
|
||||
.replace('歌名', musicInfo.name)
|
||||
.replace('歌手', clipNameLength(musicInfo.singer)))}.${ext}`),
|
||||
},
|
||||
}
|
||||
downloadInfo.metadata.filePath = joinPath(savePath, downloadInfo.metadata.fileName)
|
||||
// downloadInfo.metadata.filePath = joinPath(savePath, downloadInfo.metadata.fileName)
|
||||
// commit('addTask', downloadInfo)
|
||||
|
||||
// 删除同路径下的同名文件
|
||||
|
|
|
@ -9,7 +9,7 @@ import { createLocalMusicInfo } from '@renderer/utils/music'
|
|||
/**
|
||||
* 过滤列表中已播放的歌曲
|
||||
*/
|
||||
export const filterMusicList = async({ playedList, listId, list, savePath, playerMusicInfo, dislikeInfo, isNext }: {
|
||||
export const filterMusicList = async({ playedList, listId, list, playerMusicInfo, dislikeInfo, isNext }: {
|
||||
/**
|
||||
* 已播放列表
|
||||
*/
|
||||
|
@ -25,7 +25,7 @@ export const filterMusicList = async({ playedList, listId, list, savePath, playe
|
|||
/**
|
||||
* 下载目录
|
||||
*/
|
||||
savePath: string
|
||||
// savePath: string
|
||||
/**
|
||||
* 播放器内当前歌曲(`playInfo.playerPlayIndex`指向的歌曲)
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue