diff --git a/publish/changeLog.md b/publish/changeLog.md index 9034a368..2c0ab6f1 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -5,6 +5,7 @@ Linux 系统至少需要 GLIBC_2.29 版本才能运行, ### 新增 +- 新增下载的歌曲按列表名分组的功能,默认关闭,可以到 设置-下载设置-将文件保存到以对应列表命名的子目录中 启用(#2145) - 新增托盘图标颜色 跟随系统亮暗模式 设置,可以在 设置-其他 启用 (#2016) - 支持本地同名 `krc` 格式歌词文件的读取(#2053) - Open API 新增播放器播放/暂停、切歌、收藏当前播放歌曲调用,详情看开放API文档 (原始 PR #2077) diff --git a/src/common/defaultSetting.ts b/src/common/defaultSetting.ts index 033f9294..9d39197b 100644 --- a/src/common/defaultSetting.ts +++ b/src/common/defaultSetting.ts @@ -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, diff --git a/src/common/types/app_setting.d.ts b/src/common/types/app_setting.d.ts index d7b7970e..8e2c0813 100644 --- a/src/common/types/app_setting.d.ts +++ b/src/common/types/app_setting.d.ts @@ -485,6 +485,11 @@ declare global { */ 'download.enable': boolean + /** + * 按列表名分组保存 + */ + 'download.isSavePathGroupByListName': boolean + /** * 下载路径 */ diff --git a/src/common/types/download_list.d.ts b/src/common/types/download_list.d.ts index 94b1cf70..ba0f11ed 100644 --- a/src/common/types/download_list.d.ts +++ b/src/common/types/download_list.d.ts @@ -59,6 +59,7 @@ declare global { ext: FileExt fileName: string filePath: string + listId?: string } } diff --git a/src/common/utils/nodejs.ts b/src/common/utils/nodejs.ts index ce279cfc..f4f22a16 100644 --- a/src/common/utils/nodejs.ts +++ b/src/common/utils/nodejs.ts @@ -31,6 +31,25 @@ export const checkPath = async(path: string): Promise => { }) } +/** + * 检查路径并创建目录 + * @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 => { return new Promise(resolve => { if (!path) { diff --git a/src/common/utils/tools.ts b/src/common/utils/tools.ts index 73fc72b6..3ec8428e 100644 --- a/src/common/utils/tools.ts +++ b/src/common/utils/tools.ts @@ -130,3 +130,21 @@ export const filterMusicList = (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 +} + diff --git a/src/lang/en-us.json b/src/lang/en-us.json index 72809318..8cae4571 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -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", diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index c59f0ab7..273121b6 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -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": "暗色主题", diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index be6e9d53..49f31404 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -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": "打開", diff --git a/src/renderer/components/common/DownloadModal.vue b/src/renderer/components/common/DownloadModal.vue index cbee8704..c2f3c44b 100644 --- a/src/renderer/components/common/DownloadModal.vue +++ b/src/renderer/components/common/DownloadModal.vue @@ -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() { diff --git a/src/renderer/components/common/DownloadMultipleModal.vue b/src/renderer/components/common/DownloadMultipleModal.vue index 023e198e..d4cb899e 100644 --- a/src/renderer/components/common/DownloadMultipleModal.vue +++ b/src/renderer/components/common/DownloadMultipleModal.vue @@ -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') }, diff --git a/src/renderer/core/music/download.ts b/src/renderer/core/music/download.ts index a6404871..2a8a9327 100644 --- a/src/renderer/core/music/download.ts +++ b/src/renderer/core/music/download.ts @@ -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 => { 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 => { 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) diff --git a/src/renderer/core/player/utils.ts b/src/renderer/core/player/utils.ts index 24949a66..3ec96db6 100644 --- a/src/renderer/core/player/utils.ts +++ b/src/renderer/core/player/utils.ts @@ -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, diff --git a/src/renderer/store/download/action.ts b/src/renderer/store/download/action.ts index 45cf5881..15618e9e 100644 --- a/src/renderer/store/download/action.ts +++ b/src/renderer/store/download/action.ts @@ -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() 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) diff --git a/src/renderer/store/download/utils.ts b/src/renderer/store/download/utils.ts new file mode 100644 index 00000000..17278b95 --- /dev/null +++ b/src/renderer/store/download/utils.ts @@ -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 +} diff --git a/src/renderer/views/List/MusicList/index.vue b/src/renderer/views/List/MusicList/index.vue index bfd229f0..9681972d 100644 --- a/src/renderer/views/List/MusicList/index.vue +++ b/src/renderer/views/List/MusicList/index.vue @@ -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" /> - - + + diff --git a/src/renderer/views/Setting/components/SettingDownload.vue b/src/renderer/views/Setting/components/SettingDownload.vue index 88d8437e..f15d4044 100644 --- a/src/renderer/views/Setting/components/SettingDownload.vue +++ b/src/renderer/views/Setting/components/SettingDownload.vue @@ -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 diff --git a/src/renderer/worker/download/download.ts b/src/renderer/worker/download/download.ts index 1304ca66..d69df6a9 100644 --- a/src/renderer/worker/download/download.ts +++ b/src/renderer/worker/download/download.ts @@ -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: { diff --git a/src/renderer/worker/download/utils.ts b/src/renderer/worker/download/utils.ts index 036770a3..c487bf4d 100644 --- a/src/renderer/worker/download/utils.ts +++ b/src/renderer/worker/download/utils.ts @@ -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) // 删除同路径下的同名文件 diff --git a/src/renderer/worker/main/list.ts b/src/renderer/worker/main/list.ts index 5cff001d..3cbcb616 100644 --- a/src/renderer/worker/main/list.ts +++ b/src/renderer/worker/main/list.ts @@ -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`指向的歌曲) */