新增下载的歌曲按列表名分组的功能(#2145)

pull/2166/head
lyswhut 2024-12-14 12:43:33 +08:00
parent 63b2c2fb2f
commit 6f74003e14
20 changed files with 112 additions and 39 deletions

View File

@ -5,6 +5,7 @@ Linux 系统至少需要 GLIBC_2.29 版本才能运行,
### 新增
- 新增下载的歌曲按列表名分组的功能,默认关闭,可以到 设置-下载设置-将文件保存到以对应列表命名的子目录中 启用(#2145
- 新增托盘图标颜色 跟随系统亮暗模式 设置,可以在 设置-其他 启用 #2016
- 支持本地同名 `krc` 格式歌词文件的读取(#2053
- Open API 新增播放器播放/暂停、切歌、收藏当前播放歌曲调用详情看开放API文档 (原始 PR #2077)

View File

@ -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,

View File

@ -485,6 +485,11 @@ declare global {
*/
'download.enable': boolean
/**
*
*/
'download.isSavePathGroupByListName': boolean
/**
*
*/

View File

@ -59,6 +59,7 @@ declare global {
ext: FileExt
fileName: string
filePath: string
listId?: string
}
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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",

View File

@ -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": "暗色主题",

View File

@ -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": "打開",

View File

@ -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() {

View File

@ -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')
},

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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
}

View File

@ -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" />

View File

@ -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

View File

@ -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: {

View File

@ -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)
// 删除同路径下的同名文件

View File

@ -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`
*/