新增下载的歌曲按列表名分组的功能(#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 - 新增托盘图标颜色 跟随系统亮暗模式 设置,可以在 设置-其他 启用 #2016
- 支持本地同名 `krc` 格式歌词文件的读取(#2053 - 支持本地同名 `krc` 格式歌词文件的读取(#2053
- Open API 新增播放器播放/暂停、切歌、收藏当前播放歌曲调用详情看开放API文档 (原始 PR #2077) - Open API 新增播放器播放/暂停、切歌、收藏当前播放歌曲调用详情看开放API文档 (原始 PR #2077)

View File

@ -107,6 +107,7 @@ const defaultSetting: LX.AppSetting = {
'list.actionButtonsVisible': false, 'list.actionButtonsVisible': false,
'download.enable': false, 'download.enable': false,
'download.isSavePathGroupByListName': false,
'download.savePath': path.join(os.homedir(), 'Desktop'), 'download.savePath': path.join(os.homedir(), 'Desktop'),
'download.fileName': '歌名 - 歌手', 'download.fileName': '歌名 - 歌手',
'download.maxDownloadNum': 3, 'download.maxDownloadNum': 3,

View File

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

View File

@ -59,6 +59,7 @@ declare global {
ext: FileExt ext: FileExt
fileName: string fileName: string
filePath: 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> => { export const getFileStats = async(path: string): Promise<fs.Stats | null> => {
return new Promise(resolve => { return new Promise(resolve => {
if (!path) { if (!path) {

View File

@ -130,3 +130,21 @@ export const filterMusicList = <T extends LX.Music.MusicInfo>(list: T[]): T[] =>
return true 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_try_auto_update": "Attempt to download updates automatically when a new version is found",
"setting__update_unknown": "Unknown", "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__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", "setting_sync_status_enabled": "connected",
"song_list": "Playlists", "song_list": "Playlists",
"songlist__import_input_btn_confirm": "Open", "songlist__import_input_btn_confirm": "Open",

View File

@ -586,6 +586,7 @@
"setting__update_try_auto_update": "发现新版本时尝试自动下载更新", "setting__update_try_auto_update": "发现新版本时尝试自动下载更新",
"setting__update_unknown": "未知", "setting__update_unknown": "未知",
"setting__update_unknown_tip": "❓ 获取最新版本信息失败,建议去「关于」页面打开项目发布地址查看当前版本是否最新", "setting__update_unknown_tip": "❓ 获取最新版本信息失败,建议去「关于」页面打开项目发布地址查看当前版本是否最新",
"setting_download_save_group_list_name": "将文件保存到以对应列表命名的子目录中",
"setting_sync_status_enabled": "已连接", "setting_sync_status_enabled": "已连接",
"song_list": "歌单", "song_list": "歌单",
"songlist__import_input_btn_confirm": "打开", "songlist__import_input_btn_confirm": "打开",

View File

@ -586,6 +586,7 @@
"setting__update_try_auto_update": "發現新版本時嘗試自動下載更新", "setting__update_try_auto_update": "發現新版本時嘗試自動下載更新",
"setting__update_unknown": "未知", "setting__update_unknown": "未知",
"setting__update_unknown_tip": "❓ 取得最新版本資訊失敗,建議去關於介面開啟專案發佈位址查看目前版本是否最新", "setting__update_unknown_tip": "❓ 取得最新版本資訊失敗,建議去關於介面開啟專案發佈位址查看目前版本是否最新",
"setting_download_save_group_list_name": "將檔案儲存到以對應清單命名的子目錄中",
"setting_sync_status_enabled": "已連接", "setting_sync_status_enabled": "已連接",
"song_list": "歌單", "song_list": "歌單",
"songlist__import_input_btn_confirm": "打開", "songlist__import_input_btn_confirm": "打開",

View File

@ -23,6 +23,10 @@ export default {
type: [Object, null], type: [Object, null],
required: true, required: true,
}, },
listId: {
type: String,
default: '',
},
bgClose: { bgClose: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -51,7 +55,7 @@ export default {
}, },
methods: { methods: {
handleClick(quality) { handleClick(quality) {
void createDownloadTasks([this.musicInfo], quality) void createDownloadTasks([this.musicInfo], quality, this.listId)
this.handleClose() this.handleClose()
}, },
handleClose() { handleClose() {

View File

@ -23,6 +23,10 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
listId: {
type: String,
default: '',
},
list: { list: {
type: Array, type: Array,
default() { default() {
@ -37,7 +41,7 @@ export default {
emits: ['update:show', 'confirm'], emits: ['update:show', 'confirm'],
methods: { methods: {
handleClick(quality) { 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.handleClose()
this.$emit('confirm') this.$emit('confirm')
}, },

View File

@ -1,4 +1,3 @@
import { appSetting } from '@renderer/store/setting'
import { getDownloadFilePath } from '@renderer/utils/music' import { getDownloadFilePath } from '@renderer/utils/music'
import { import {
@ -7,6 +6,7 @@ import {
getLyricInfo as getOnlineLyricInfo, getLyricInfo as getOnlineLyricInfo,
} from './online' } from './online'
import { buildLyricInfo, getCachedLyricInfo } from './utils' import { buildLyricInfo, getCachedLyricInfo } from './utils'
import { buildSavePath } from '@renderer/store/download/utils'
export const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: { export const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {
musicInfo: LX.Download.ListItem musicInfo: LX.Download.ListItem
@ -15,7 +15,7 @@ export const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = tru
allowToggleSource?: boolean allowToggleSource?: boolean
}): Promise<string> => { }): Promise<string> => {
if (!isRefresh) { if (!isRefresh) {
const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath']) const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo))
if (path) return path if (path) return path
} }
@ -29,7 +29,7 @@ export const getPicUrl = async({ musicInfo, isRefresh, listId, onToggleSource =
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
}): Promise<string> => { }): Promise<string> => {
if (!isRefresh) { if (!isRefresh) {
const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath']) const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo))
if (path) { if (path) {
const pic = await window.lx.worker.main.getMusicFilePic(path) const pic = await window.lx.worker.main.getMusicFilePic(path)
if (pic) return pic if (pic) return pic
@ -62,7 +62,7 @@ export const getLyricInfo = async({ musicInfo, isRefresh, onToggleSource = () =>
onToggleSource, onToggleSource,
}).catch(async() => { }).catch(async() => {
// 尝试读取文件内歌词 // 尝试读取文件内歌词
const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath']) const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo))
if (path) { if (path) {
const rawlrcInfo = await window.lx.worker.main.getMusicFileLyric(path) const rawlrcInfo = await window.lx.worker.main.getMusicFileLyric(path)
if (rawlrcInfo) return buildLyricInfo(rawlrcInfo) if (rawlrcInfo) return buildLyricInfo(rawlrcInfo)

View File

@ -29,7 +29,7 @@ export const filterList = async({ playedList, listId, list, playerMusicInfo, isN
listId, listId,
list: list.map(m => toRaw(m)), list: list.map(m => toRaw(m)),
playedList: toRaw(playedList), playedList: toRaw(playedList),
savePath: appSetting['download.savePath'], // savePath: appSetting['download.savePath'],
playerMusicInfo: toRaw(playerMusicInfo), playerMusicInfo: toRaw(playerMusicInfo),
dislikeInfo: { names: toRaw(dislikeInfo.names), musicNames: toRaw(dislikeInfo.musicNames), singerNames: toRaw(dislikeInfo.singerNames) }, dislikeInfo: { names: toRaw(dislikeInfo.names), musicNames: toRaw(dislikeInfo.musicNames), singerNames: toRaw(dislikeInfo.singerNames) },
isNext, isNext,

View File

@ -16,6 +16,7 @@ import { proxyCallback } from '@renderer/worker/utils'
import { arrPush, arrUnshift, joinPath } from '@renderer/utils' import { arrPush, arrUnshift, joinPath } from '@renderer/utils'
import { DOWNLOAD_STATUS } from '@common/constants' import { DOWNLOAD_STATUS } from '@common/constants'
import { proxy } from '../index' import { proxy } from '../index'
import { buildSavePath } from './utils'
const waitingUpdateTasks = new Map<string, LX.Download.ListItem>() const waitingUpdateTasks = new Map<string, LX.Download.ListItem>()
let timer: NodeJS.Timeout | null = null let timer: NodeJS.Timeout | null = null
@ -271,12 +272,13 @@ const handleStartTask = async(downloadInfo: LX.Download.ListItem) => {
if (downloadInfo.status != DOWNLOAD_STATUS.RUN) return 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) if (downloadInfo.metadata.filePath != filePath) updateFilePath(downloadInfo, filePath)
setStatusText(downloadInfo, window.i18n.t('download_status_start')) 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) // console.log(event)
switch (event.action) { switch (event.action) {
case 'start': case 'start':
@ -357,12 +359,11 @@ const filterTask = (list: LX.Download.ListItem[]) => {
* @param list * @param list
* @param quality * @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 if (!list.length) return
const tasks = filterTask(await window.lx.worker.download.createDownloadTasks(list, quality, const tasks = filterTask(await window.lx.worker.download.createDownloadTasks(list, quality,
appSetting['download.savePath'],
appSetting['download.fileName'], appSetting['download.fileName'],
toRaw(qualityList.value)), toRaw(qualityList.value), listId),
) )
if (tasks.length) await addTasks(tasks) 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" v-model:show="isShowListAddMultiple" :from-list-id="listId"
:is-move="isMoveMultiple" :music-list="selectedList" :exclude-list-id="excludeListIds" teleport="#view" @confirm="removeAllSelect" :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-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" @confirm="removeAllSelect" /> <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" /> <search-list :list="list" :visible="isShowSearchBar" @action="handleMusicSearchAction" />
<music-sort-modal v-model:show="isShowMusicSortModal" :music-info="selectedSortMusicInfo" :selected-num="selectedNum" @confirm="sortMusic" /> <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" /> <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})") 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 .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})") 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')") dd(:aria-label="$t('setting__download_path_title')")
h3#download_path {{ $t('setting__download_path') }} h3#download_path {{ $t('setting__download_path') }}
div div

View File

@ -8,7 +8,7 @@ import { createDownloadInfo } from './utils'
// assertApiSupport, // assertApiSupport,
// getExt, // getExt,
// } from '..' // } 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_STATUS } from '@common/constants'
// import { download as eventDownloadNames } from '@renderer/event/names' // import { download as eventDownloadNames } from '@renderer/event/names'
@ -40,12 +40,12 @@ const sendAction = (id: string, action: LX.Download.DownloadTaskActions) => {
export const createDownloadTasks = ( export const createDownloadTasks = (
list: LX.Music.MusicInfoOnline[], list: LX.Music.MusicInfoOnline[],
quality: LX.Quality, quality: LX.Quality,
savePath: string,
fileNameFormat: string, fileNameFormat: string,
qualityList: LX.QualityList, qualityList: LX.QualityList,
listId?: string,
): LX.Download.ListItem[] => { ): LX.Download.ListItem[] => {
return list.map(musicInfo => { return list.map(musicInfo => {
return createDownloadInfo(musicInfo, quality, fileNameFormat, savePath, qualityList) return createDownloadInfo(musicInfo, quality, fileNameFormat, qualityList, listId)
}).filter(task => task) }).filter(task => task)
// commit('addTasks', { list: taskList, addMusicLocationType: rootState.setting.list.addMusicLocationType }) // commit('addTasks', { list: taskList, addMusicLocationType: rootState.setting.list.addMusicLocationType })
// let result = getStartTask(downloadList, DOWNLOAD_STATUS, rootState.setting.download.maxDownloadNum) // 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('onStart', downloadInfo)
commit('setStatusText', { downloadInfo, text: '任务初始化中' }) */ commit('setStatusText', { downloadInfo, text: '任务初始化中' }) */
if (!await checkPath(savePath)) { if (!await checkAndCreateDir(savePath)) {
sendAction(downloadInfo.id, { sendAction(downloadInfo.id, {
action: 'error', action: 'error',
data: { data: {

View File

@ -1,8 +1,8 @@
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 { mergeLyrics } from './lrcTool' import { mergeLyrics } from './lrcTool'
import fs from 'fs' 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 => { // 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)) // 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 export const createDownloadInfo = (musicInfo: LX.Music.MusicInfoOnline, type: LX.Quality, fileName: string, qualityList: LX.QualityList, listId?: string) => {
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) => {
type = getMusicType(musicInfo, type, qualityList) type = getMusicType(musicInfo, type, qualityList)
let ext = getExt(type) let ext = getExt(type)
const key = `${musicInfo.id}_${type}_${ext}` const key = `${musicInfo.id}_${type}_${ext}`
@ -101,12 +87,13 @@ export const createDownloadInfo = (musicInfo: LX.Music.MusicInfoOnline, type: LX
quality: type, quality: type,
ext, ext,
filePath: '', filePath: '',
listId,
fileName: filterFileName(`${clipFileNameLength(fileName fileName: filterFileName(`${clipFileNameLength(fileName
.replace('歌名', musicInfo.name) .replace('歌名', musicInfo.name)
.replace('歌手', clipNameLength(musicInfo.singer)))}.${ext}`), .replace('歌手', clipNameLength(musicInfo.singer)))}.${ext}`),
}, },
} }
downloadInfo.metadata.filePath = joinPath(savePath, downloadInfo.metadata.fileName) // downloadInfo.metadata.filePath = joinPath(savePath, downloadInfo.metadata.fileName)
// commit('addTask', downloadInfo) // 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` * `playInfo.playerPlayIndex`
*/ */