Skip to content

Commit 50d03c1

Browse files
authored
Merge pull request #609 from imsyy/dev-dl
✨ feat: 完善下载管理页面
2 parents af55159 + b0c2aae commit 50d03c1

21 files changed

Lines changed: 1053 additions & 628 deletions

File tree

components.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ declare module 'vue' {
2323
CoverList: typeof import('./src/components/List/CoverList.vue')['default']
2424
CoverMenu: typeof import('./src/components/Menu/CoverMenu.vue')['default']
2525
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
26+
DownloadManager: typeof import('./src/components/Global/DownloadManager.vue')['default']
27+
DownloadModal: typeof import('./src/components/Modal/DownloadModal.vue')['default']
2628
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
2729
Equalizer: typeof import('./src/components/Modal/Equalizer.vue')['default']
2830
ExcludeLyrics: typeof import('./src/components/Modal/ExcludeLyrics.vue')['default']

electron/main/ipc/ipc-file.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
22
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "path";
3-
import { access, readdir, readFile, stat, unlink, writeFile } from "fs/promises";
3+
import { access, mkdir, readdir, readFile, stat, unlink, writeFile } from "fs/promises";
44
import { parseFile } from "music-metadata";
55
import { getFileID, getFileMD5, metaDataLyricsArrayToLrc } from "../utils/helper";
66
import { File, Picture, Id3v2Settings, TagTypes } from "node-taglib-sharp";
@@ -379,10 +379,10 @@ const initFileIpc = (): void => {
379379
songData?: any;
380380
skipIfExist?: boolean;
381381
} = {
382-
fileName: "未知文件名",
383-
fileType: "mp3",
384-
path: app.getPath("downloads"),
385-
},
382+
fileName: "未知文件名",
383+
fileType: "mp3",
384+
path: app.getPath("downloads"),
385+
},
386386
): Promise<{ status: "success" | "skipped" | "error"; message?: string }> => {
387387
try {
388388
// 获取窗口
@@ -403,11 +403,11 @@ const initFileIpc = (): void => {
403403
} = options;
404404
// 规范化路径
405405
const downloadPath = resolve(path);
406-
// 检查文件夹是否存在
406+
// 检查文件夹是否存在,不存在则自动递归创建
407407
try {
408408
await access(downloadPath);
409409
} catch {
410-
throw new Error("❌ Folder not found");
410+
await mkdir(downloadPath, { recursive: true });
411411
}
412412

413413
// 检查文件是否存在
@@ -425,8 +425,9 @@ const initFileIpc = (): void => {
425425
const songDownload = await download(win, url, {
426426
directory: downloadPath,
427427
filename: `${fileName}.${fileType}`,
428+
showProgressBar: false,
428429
onProgress: (progress) => {
429-
win.webContents.send("download-progress", progress);
430+
win.webContents.send("download-progress", { ...progress, id: songData?.id });
430431
},
431432
});
432433
if (!downloadMeta || !songData?.cover) return { status: "success" };
@@ -435,6 +436,7 @@ const initFileIpc = (): void => {
435436
const coverDownload = await download(win, coverUrl, {
436437
directory: downloadPath,
437438
filename: `${fileName}.jpg`,
439+
showProgressBar: false,
438440
});
439441
// 读取歌曲文件
440442
let songFile = File.createFromPath(songDownload.getSavePath());
@@ -471,7 +473,10 @@ const initFileIpc = (): void => {
471473
return { status: "success" };
472474
} catch (error) {
473475
ipcLog.error("❌ Error downloading file:", error);
474-
return { status: "error", message: error instanceof Error ? error.message : "Unknown error" };
476+
return {
477+
status: "error",
478+
message: error instanceof Error ? error.message : "Unknown error",
479+
};
475480
}
476481
},
477482
);

src/assets/icons/DownloadDone.svg

Lines changed: 1 addition & 0 deletions
Loading

src/components/Layout/Menu.vue

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
NButton,
2626
NEllipsis,
2727
NAvatar,
28+
NBadge,
2829
} from "naive-ui";
2930
import type { CoverType } from "@/types/main";
3031
import { useStatusStore, useSettingStore, useDataStore, useMusicStore } from "@/stores";
@@ -134,9 +135,17 @@ const menuOptions = computed<MenuOption[] | MenuGroupOption[]>(() => {
134135
},
135136
{
136137
key: "download",
137-
link: "download",
138-
label: "下载管理",
139-
show: isElectron,
138+
label: () =>
139+
h(
140+
NBadge,
141+
{
142+
show: dataStore.downloadingSongs.length > 0,
143+
value: dataStore.downloadingSongs.length,
144+
offset: [22, 13],
145+
},
146+
() => "下载管理",
147+
),
148+
show: isElectron && !settingStore.hideDownload,
140149
icon: renderIcon("Download"),
141150
},
142151
{
@@ -282,6 +291,13 @@ const menuUpdate = (key: string, item: MenuOption) => {
282291
name: "like-songs",
283292
});
284293
break;
294+
// 下载管理
295+
case "download":
296+
router.push({
297+
name:
298+
dataStore.downloadingSongs.length > 0 ? "download-downloading" : "download-downloaded",
299+
});
300+
break;
285301
default:
286302
break;
287303
}

src/components/Modal/BatchList.vue

Lines changed: 10 additions & 232 deletions
Original file line numberDiff line numberDiff line change
@@ -100,97 +100,20 @@
100100
</n-button>
101101
</n-flex>
102102
</n-flex>
103-
104-
<!-- 音质选择弹窗 -->
105-
<n-modal
106-
v-model:show="showQualityModal"
107-
preset="card"
108-
title="批量下载"
109-
:closable="false"
110-
:mask-closable="false"
111-
style="width: 500px"
112-
>
113-
<n-alert type="warning" title="请知悉" closable style="margin-top: 20px">
114-
本软件仅支持从官方途径合法合规的下载歌曲,并用于学习研究用途。本功能将严格按照相应账户的权限来提供基础的下载功能
115-
</n-alert>
116-
<n-collapse
117-
:default-expanded-names="['level', 'path']"
118-
arrow-placement="right"
119-
style="margin-top: 20px"
120-
>
121-
<n-collapse-item title="音质选择" name="level">
122-
<n-radio-group v-model:value="selectedQuality" name="quality">
123-
<n-flex>
124-
<n-radio v-for="(item, index) in qualityOptions" :key="index" :value="item.value">
125-
<n-flex>
126-
<n-text class="name">{{ item.label }}</n-text>
127-
</n-flex>
128-
</n-radio>
129-
</n-flex>
130-
</n-radio-group>
131-
<n-text depth="3" style="font-size: 12px; margin-top: 10px; display: block">
132-
注意:如果歌曲没有对应的音质,将自动下载最高可用音质
133-
</n-text>
134-
</n-collapse-item>
135-
<n-collapse-item v-if="isElectron" title="下载路径" name="path">
136-
<n-input-group>
137-
<n-input :value="downloadPath || '未配置下载目录'" disabled>
138-
<template #prefix>
139-
<SvgIcon name="Folder" />
140-
</template>
141-
</n-input>
142-
<n-button type="primary" strong secondary @click="changeDownloadPath">
143-
<template #icon>
144-
<SvgIcon name="Folder" />
145-
</template>
146-
</n-button>
147-
<n-button type="primary" strong secondary @click="openSetting('local')">
148-
<template #icon>
149-
<SvgIcon name="Settings" />
150-
</template>
151-
更多设置
152-
</n-button>
153-
</n-input-group>
154-
</n-collapse-item>
155-
</n-collapse>
156-
157-
<template #action>
158-
<n-flex justify="end">
159-
<n-button @click="showQualityModal = false">取消</n-button>
160-
<n-button type="primary" @click="startBatchDownload">确认下载</n-button>
161-
</n-flex>
162-
</template>
163-
</n-modal>
164103
</div>
165104
</template>
166105

167106
<script setup lang="ts">
168107
import type { DataTableColumns, DataTableRowKey } from "naive-ui";
169-
import type { SongType, SongLevelType } from "@/types/main";
170-
import { isArray, isObject, pick } from "lodash-es";
171-
import { openPlaylistAdd, openSetting } from "@/utils/modal";
108+
import type { SongType } from "@/types/main";
109+
import { isArray, isObject } from "lodash-es";
110+
import { openPlaylistAdd } from "@/utils/modal";
172111
import { deleteSongs } from "@/utils/auth";
173-
import {
174-
NInput,
175-
NInputNumber,
176-
NRadioGroup,
177-
NRadio,
178-
NCollapse,
179-
NCollapseItem,
180-
NInputGroup,
181-
NButton,
182-
NAlert,
183-
NText,
184-
NFlex,
185-
} from "naive-ui";
186-
import { useLocalStore, useSettingStore, useDataStore } from "@/stores";
187-
import { isElectron } from "@/utils/env";
188-
import { songLevelData, getSongLevelsData } from "@/utils/meta";
189-
import { downloadSong } from "@/utils/download";
112+
import { NInput, NInputNumber, NButton, NText, NFlex } from "naive-ui";
113+
import { useLocalStore } from "@/stores";
114+
import { openDownloadSongs } from "@/utils/modal";
190115
191116
const localStore = useLocalStore();
192-
const settingStore = useSettingStore();
193-
const dataStore = useDataStore();
194117
195118
interface DataType {
196119
key?: number;
@@ -217,23 +140,6 @@ const checkedRowKeys = ref<DataTableRowKey[]>([]);
217140
const startRange = ref<number | null>(null);
218141
const endRange = ref<number | null>(null);
219142
220-
// 下载相关
221-
const showQualityModal = ref(false);
222-
const selectedQuality = ref<SongLevelType>("h");
223-
const downloadPath = ref<string>(settingStore.downloadPath);
224-
225-
// 音质选项
226-
const qualityOptions = computed(() => {
227-
// 批量下载时,默认显示所有常用音质选项
228-
// 这里模拟一个包含所有常用音质的 level 对象
229-
const levels = pick(songLevelData, ["l", "m", "h", "sq", "hr", "je", "sk", "db", "jm"]);
230-
return getSongLevelsData(levels).map((item) => ({
231-
label: item.name,
232-
value: item.value,
233-
level: item.level,
234-
}));
235-
});
236-
237143
// 表头数据
238144
const columnsData = computed<DataTableColumns<DataType>>(() => [
239145
{
@@ -395,139 +301,11 @@ const handleDeleteLocalSongs = () => {
395301
396302
// 批量下载处理
397303
const handleBatchDownloadClick = () => {
398-
if (settingStore.downloadPath) downloadPath.value = settingStore.downloadPath;
399-
showQualityModal.value = true;
400-
};
401-
402-
// 更改下载路径
403-
const changeDownloadPath = async () => {
404-
const path = await window.electron.ipcRenderer.invoke("choose-path");
405-
if (path) downloadPath.value = path;
406-
};
407-
408-
const startBatchDownload = () => {
409-
showQualityModal.value = false;
410-
executeBatchDownload(checkSongData.value);
411-
};
412-
413-
const executeBatchDownload = async (songs: SongType[]) => {
414-
if (!songs.length) return;
415-
416-
// 重置状态
417-
dataStore.batchDownload.isDownloading = true;
418-
dataStore.batchDownload.total = songs.length;
419-
dataStore.batchDownload.processed = 0;
420-
dataStore.batchDownload.success = 0;
421-
dataStore.batchDownload.percent = 0;
422-
dataStore.batchDownload.transferred = "0MB";
423-
dataStore.batchDownload.totalSize = "0MB";
424-
425-
let failCount = 0;
426-
const failedSongs: SongType[] = [];
427-
428-
// 监听下载进度
429-
const onProgress = (
430-
_event: any,
431-
progress: { percent: number; transferredBytes: number; totalBytes: number },
432-
) => {
433-
const { percent, transferredBytes, totalBytes } = progress;
434-
dataStore.batchDownload.percent = Number((percent * 100).toFixed(0));
435-
dataStore.batchDownload.transferred = (transferredBytes / 1024 / 1024).toFixed(2) + "MB";
436-
dataStore.batchDownload.totalSize = (totalBytes / 1024 / 1024).toFixed(2) + "MB";
437-
};
438-
439-
if (isElectron) {
440-
window.electron.ipcRenderer.on("download-progress", onProgress);
441-
}
442-
443-
try {
444-
for (const song of songs) {
445-
dataStore.batchDownload.currentSong = song.name;
446-
try {
447-
const result = await downloadSong({
448-
song,
449-
quality: selectedQuality.value,
450-
downloadPath: downloadPath.value,
451-
skipIfExist: true,
452-
});
453-
454-
if (result.success) {
455-
dataStore.batchDownload.success++;
456-
if (result.skipped) {
457-
window.$notification.create({
458-
title: "已跳过重复文件",
459-
content: `${song.name} 已存在`,
460-
duration: 2000,
461-
});
462-
}
463-
if (!isElectron) {
464-
// Browser download delay
465-
await new Promise((resolve) => setTimeout(resolve, 500));
466-
}
467-
} else {
468-
console.error(`Failed to download song ${song.name}: ${result.message}`);
469-
failCount++;
470-
failedSongs.push(song);
471-
}
472-
} catch (err) {
473-
console.error(`Error downloading song ${song.name}:`, err);
474-
failCount++;
475-
failedSongs.push(song);
476-
} finally {
477-
dataStore.batchDownload.processed++;
478-
// Reset progress for next song
479-
dataStore.batchDownload.percent = 0;
480-
dataStore.batchDownload.transferred = "0MB";
481-
dataStore.batchDownload.totalSize = "0MB";
482-
}
483-
}
484-
485-
if (failCount > 0) {
486-
window.$dialog.warning({
487-
title: "下载完成,但有部分失败",
488-
content: () =>
489-
h("div", [
490-
h(
491-
"div",
492-
{ style: "margin-bottom: 10px" },
493-
`${dataStore.batchDownload.success} 首下载成功,${failCount} 首下载失败。`,
494-
),
495-
h(
496-
"div",
497-
{
498-
style:
499-
"max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.05); padding: 8px; border-radius: 4px;",
500-
},
501-
[
502-
h("div", { style: "font-weight: bold; margin-bottom: 4px" }, "失败列表:"),
503-
...failedSongs.map((s) =>
504-
h(
505-
"div",
506-
{ style: "font-size: 12px" },
507-
`${s.name} - ${isArray(s.artists) ? s.artists[0]?.name : s.artists || "未知歌手"}`,
508-
),
509-
),
510-
],
511-
),
512-
]),
513-
positiveText: "重试失败歌曲",
514-
negativeText: "取消",
515-
onPositiveClick: () => {
516-
executeBatchDownload(failedSongs);
517-
},
518-
});
519-
} else {
520-
window.$message.success(`批量下载完成,共 ${dataStore.batchDownload.success} 首`);
521-
}
522-
} catch (error) {
523-
console.error("Batch download error:", error);
524-
window.$message.error("批量下载过程中出现错误");
525-
} finally {
526-
if (isElectron) {
527-
window.electron.ipcRenderer.removeListener("download-progress", onProgress);
528-
}
529-
dataStore.batchDownload.isDownloading = false;
304+
if (checkSongData.value.length === 0) {
305+
window.$message.warning("请选择要下载的歌曲");
306+
return;
530307
}
308+
openDownloadSongs(checkSongData.value);
531309
};
532310
</script>
533311

0 commit comments

Comments
 (0)