diff --git a/packages/main/src/controller/DownloadController.ts b/packages/main/src/controller/DownloadController.ts index 6f88b79..f3f3bc1 100644 --- a/packages/main/src/controller/DownloadController.ts +++ b/packages/main/src/controller/DownloadController.ts @@ -56,4 +56,9 @@ export default class DownloadController implements Controller { await this.videoRepository.changeVideoStatus(vid, DownloadStatus.Watting); this.downloadService.addTask(task); } + + @handle("stop-download") + async stopDownload(e: IpcMainEvent, id: number) { + this.downloadService.stopTask(id); + } } diff --git a/packages/main/src/helper/download.ts b/packages/main/src/helper/download.ts index 0de68b2..c833b9d 100644 --- a/packages/main/src/helper/download.ts +++ b/packages/main/src/helper/download.ts @@ -8,6 +8,7 @@ const progressReg = /Progress:\s(\d+)\/(\d+)\s\(.+?\).+?\((.+?\/s).*?\)/g; export const spawnDownload = ( id: number, + abortSignal: AbortController, url: string, local: string, name: string @@ -21,13 +22,13 @@ export const spawnDownload = ( // name, // ]); - const downloader = spawn(downloaderPath, [ - url, - "--workDir", - local, - "--saveName", - name, - ]); + const downloader = spawn( + downloaderPath, + [url, "--workDir", local, "--saveName", name], + { + signal: abortSignal.signal, + } + ); downloader.stdout.on("data", (data) => { const str = iconv.decode(Buffer.from(data), "gbk"); @@ -41,26 +42,18 @@ export const spawnDownload = ( const progress: DownloadProgress = { id, cur, total, speed }; event.emit("download-progress", progress); } - console.log("str: ", item); }); }); - // TODO: 错误处理 - downloader.stderr.on("data", (data) => { - const str = iconv.decode(Buffer.from(data), "gbk"); - str.split("\n").forEach((item) => { - if (item.trim() == "") { - return; - } - }); - console.log(str); + downloader.on("error", (err) => { + reject(err); }); downloader.on("close", (code) => { if (code === 0) { resolve(); } else { - reject(); + reject(new Error("未知错误")); } }); }); diff --git a/packages/main/src/interfaces.ts b/packages/main/src/interfaces.ts index 3e63562..4094e45 100644 --- a/packages/main/src/interfaces.ts +++ b/packages/main/src/interfaces.ts @@ -108,12 +108,14 @@ export enum DownloadStatus { Ready = "ready", Watting = "watting", Downloading = "downloading", + Stopped = "stopped", Success = "success", Failed = "failed", } export interface DownloadService extends EventEmitter { addTask: (task: Task) => Promise; + stopTask: (id: number) => Promise; } export type Task = { diff --git a/packages/main/src/main.d.ts b/packages/main/src/main.d.ts index 8c3a7ef..8838818 100644 --- a/packages/main/src/main.d.ts +++ b/packages/main/src/main.d.ts @@ -36,6 +36,7 @@ declare interface ElectronAPI { ) => Promise; startDownload: (vid: number) => Promise; openUrl: (url: string) => Promise; + stopDownload: (id: number) => Promise; } declare interface LinkMessage { diff --git a/packages/main/src/preload.ts b/packages/main/src/preload.ts index 6b9bbf7..d7c5fee 100644 --- a/packages/main/src/preload.ts +++ b/packages/main/src/preload.ts @@ -22,6 +22,7 @@ const handleApi: ElectronAPI = { openUrl: (url: string) => ipcRenderer.invoke("open-url", url), rendererEvent: (c, l) => ipcRenderer.on(c, l), removeEventListener: (c, l) => ipcRenderer.removeListener(c, l), + stopDownload: (id) => ipcRenderer.invoke("stop-download", id), }; contextBridge.exposeInMainWorld(apiKey, handleApi); diff --git a/packages/main/src/services/DownloadServiceImpl.ts b/packages/main/src/services/DownloadServiceImpl.ts index fe84ee9..5e47b6d 100644 --- a/packages/main/src/services/DownloadServiceImpl.ts +++ b/packages/main/src/services/DownloadServiceImpl.ts @@ -22,6 +22,8 @@ export default class DownloadServiceImpl private debug = process.env.DOWNLOAD_DEBUG; + private signal: Record = {}; + constructor( @inject(TYPES.LoggerService) private readonly logger: LoggerServiceImpl, @@ -36,6 +38,13 @@ export default class DownloadServiceImpl this.runTask(); } + async stopTask(id: number) { + if (this.signal[id]) { + this.log(`taskId: ${id} stop`); + this.signal[id].abort(); + } + } + async execute(task: Task) { try { await this.videoRepository.changeVideoStatus( @@ -45,7 +54,10 @@ export default class DownloadServiceImpl this.emit("download-start", task.id); this.log(`taskId: ${task.id} start`); - await task.process(task.id, ...task.params); + const controller = new AbortController(); + this.signal[task.id] = controller; + await task.process(task.id, controller, ...task.params); + delete this.signal[task.id]; this.log(`taskId: ${task.id} success`); await this.videoRepository.changeVideoStatus( @@ -53,15 +65,23 @@ export default class DownloadServiceImpl DownloadStatus.Success ); this.emit("download-success", task.id); - } catch (err) { + } catch (err: any) { this.log(`taskId: ${task.id} failed`); - // 传输失败 - // TODO: 下载失败的任务 - await this.videoRepository.changeVideoStatus( - task.id, - DownloadStatus.Failed - ); - // this.emit("download-failed", task.id); + if (err.name === "AbortError") { + // 下载暂停 + await this.videoRepository.changeVideoStatus( + task.id, + DownloadStatus.Stopped + ); + this.emit("download-stop", task.id); + } else { + // 下载失败 + await this.videoRepository.changeVideoStatus( + task.id, + DownloadStatus.Failed + ); + this.emit("download-failed", task.id, err); + } } finally { // 处理当前正在活动的任务 const doneId = this.active.findIndex((i) => i.id === task.id); diff --git a/packages/main/src/services/MainWindowServiceImpl.ts b/packages/main/src/services/MainWindowServiceImpl.ts index 0365ded..7164016 100644 --- a/packages/main/src/services/MainWindowServiceImpl.ts +++ b/packages/main/src/services/MainWindowServiceImpl.ts @@ -55,6 +55,7 @@ export default class MainWindowServiceImpl this.downloadService.on("download-success", this.onDownloadSuccess); this.downloadService.on("download-failed", this.onDownloadFailed); this.downloadService.on("download-start", this.onDownloadStart); + this.downloadService.on("download-stop", this.onDownloadStart); } readyToShow = () => { @@ -80,7 +81,7 @@ export default class MainWindowServiceImpl this.webContents.send("download-success", id); }; - onDownloadFailed = async (id: number) => { + onDownloadFailed = async (id: number, err: any) => { const promptTone = this.storeService.get("promptTone"); if (promptTone) { const video = await this.videoRepository.findVideo(id); @@ -90,11 +91,15 @@ export default class MainWindowServiceImpl body: `${video?.name} 下载失败`, }).show(); } - + this.logger.error("下载失败:", err); this.webContents.send("download-failed", id); }; onDownloadStart = async (id: number) => { this.webContents.send("download-start", id); }; + + onDownloadStop = async (id: number) => { + this.webContents.send("download-stop", id); + }; } diff --git a/packages/renderer/src/nodes/HomePage/index.tsx b/packages/renderer/src/nodes/HomePage/index.tsx index 3b305fa..560065b 100644 --- a/packages/renderer/src/nodes/HomePage/index.tsx +++ b/packages/renderer/src/nodes/HomePage/index.tsx @@ -1,12 +1,25 @@ import React, { FC, ReactNode, useEffect, useRef, useState } from "react"; -import { message, Progress, Radio, RadioChangeEvent, Space, Tag } from "antd"; +import { + Button, + message, + Progress, + Radio, + RadioChangeEvent, + Space, + Tag, +} from "antd"; import "./index.scss"; import PageContainer from "../../components/PageContainer"; import { usePagination } from "ahooks"; import useElectron from "../../hooks/electron"; import { DownloadStatus } from "../../types"; import { ProList } from "@ant-design/pro-components"; -import { SyncOutlined } from "@ant-design/icons"; +import { + FolderOpenOutlined, + PauseCircleOutlined, + PlayCircleOutlined, + SyncOutlined, +} from "@ant-design/icons"; import { useSelector } from "react-redux"; import { selectStore } from "../../store/appSlice"; import { tdApp } from "../../utils"; @@ -23,6 +36,7 @@ const HomePage: FC = () => { rendererEvent, removeEventListener, openDir, + stopDownload, } = useElectron(); const appStore = useSelector(selectStore); const [filter, setFilter] = useState(DownloadFilter.list); @@ -88,34 +102,70 @@ const HomePage: FC = () => { await openDir(appStore.local); }; + const onClickStopDownload = async (item: DownloadItem) => { + await stopDownload(item.id); + refresh(); + }; + const renderActionButtons = ( dom: ReactNode, item: DownloadItem ): ReactNode => { if (item.status === DownloadStatus.Ready) { return [ - onStartDownload(item)}> - 开始下载 - , +