diff --git a/internal/router/router.go b/internal/router/router.go index 287d867..fdd225d 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -28,6 +28,7 @@ func API() *http.ServeMux { router.HandleFunc("/quit", quit) router.HandleFunc("/getPopularVideos", getPopularVideos) router.HandleFunc("/deleteTask", deleteTask) + router.HandleFunc("/cancelTask", cancelTask) router.HandleFunc("/getRedirectedLocation", getRedirectedLocation) router.HandleFunc("/downloadVideo", downloadVideo) router.HandleFunc("/getSeasonsArchivesListFirstBvid", getSeasonsArchivesListFirstBvid) diff --git a/internal/router/task.go b/internal/router/task.go index 8a2ef9a..68fc8a7 100644 --- a/internal/router/task.go +++ b/internal/router/task.go @@ -170,3 +170,17 @@ func deleteTask(w http.ResponseWriter, r *http.Request) { } util.Res{Success: true, Message: "删除成功"}.Write(w) } + +func cancelTask(w http.ResponseWriter, r *http.Request) { + taskIDStr := r.FormValue("id") + taskID, err := strconv.ParseInt(taskIDStr, 10, 64) + if err != nil { + util.Res{Success: false, Message: "参数错误"}.Write(w) + return + } + if task.CancelTask(taskID) { + util.Res{Success: true, Message: "任务已取消"}.Write(w) + } else { + util.Res{Success: false, Message: "任务不存在或已完成"}.Write(w) + } +} diff --git a/internal/task/task.go b/internal/task/task.go index c6fa065..a9ed0ac 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -59,7 +59,7 @@ func (task *TaskInDB) FilePath() string { ) } -// done | waiting | running | error +// done | waiting | running | error | cancelled type TaskStatus string type Task struct { @@ -67,6 +67,7 @@ type Task struct { AudioProgress float64 `json:"audioProgress"` VideoProgress float64 `json:"videoProgress"` MergeProgress float64 `json:"mergeProgress"` + Cancelled bool `json:"cancelled"` } var GlobalTaskList = []*Task{} @@ -74,6 +75,20 @@ var GlobalTaskMux = &sync.Mutex{} var GlobalDownloadSem = util.NewSemaphore(3) var GlobalMergeSem = util.NewSemaphore(3) +// CancelTask 取消指定任务 +func CancelTask(taskID int64) bool { + GlobalTaskMux.Lock() + defer GlobalTaskMux.Unlock() + for _, task := range GlobalTaskList { + if task.ID == taskID && (task.Status == "waiting" || task.Status == "running") { + task.Cancelled = true + task.Status = "error" + return true + } + } + return false +} + func (task *Task) Create(db *sql.DB) error { util.SqliteLock.Lock() result, err := db.Exec(`INSERT INTO "task" ("bvid", "cid", "format", "title", "owner", "cover", "status", "folder", "duration", "download_type") @@ -109,6 +124,13 @@ func (task *Task) Start() { GlobalTaskMux.Unlock() db := util.MustGetDB() defer db.Close() + + // 检查是否已取消 + if task.Cancelled { + task.UpdateStatus(db, "error", fmt.Errorf("任务已取消")) + return + } + sessdata, err := bilibili.GetSessdata(db) if err != nil { task.UpdateStatus(db, "error", fmt.Errorf("bilibili.GetSessdata: %v", err)) @@ -124,14 +146,26 @@ func (task *Task) Start() { client := &bilibili.BiliClient{SESSDATA: sessdata} GlobalDownloadSem.Acquire() + + // 再次检查是否已取消 + if task.Cancelled { + GlobalDownloadSem.Release() + task.UpdateStatus(db, "error", fmt.Errorf("任务已取消")) + return + } + task.UpdateStatus(db, "running") if task.DownloadType == "audio" { // 仅音频模式:只下载音频,重命名音频文件为输出文件 err = DownloadMedia(client, task.Audio, task, "audio") - if err != nil { + if err != nil || task.Cancelled { GlobalDownloadSem.Release() - task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err)) + if task.Cancelled { + task.UpdateStatus(db, "error", fmt.Errorf("任务已取消")) + } else { + task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err)) + } return } GlobalDownloadSem.Release() @@ -151,9 +185,13 @@ func (task *Task) Start() { } else if task.DownloadType == "video" { // 仅视频模式:只下载视频,重命名视频文件为输出文件 err = DownloadMedia(client, task.Video, task, "video") - if err != nil { + if err != nil || task.Cancelled { GlobalDownloadSem.Release() - task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err)) + if task.Cancelled { + task.UpdateStatus(db, "error", fmt.Errorf("任务已取消")) + } else { + task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err)) + } return } GlobalDownloadSem.Release() @@ -173,19 +211,32 @@ func (task *Task) Start() { } else { // 合并模式:下载音频和视频,然后合并 err = DownloadMedia(client, task.Audio, task, "audio") - if err != nil { + if err != nil || task.Cancelled { GlobalDownloadSem.Release() - task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err)) + if task.Cancelled { + task.UpdateStatus(db, "error", fmt.Errorf("任务已取消")) + } else { + task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err)) + } return } err = DownloadMedia(client, task.Video, task, "video") - if err != nil { + if err != nil || task.Cancelled { GlobalDownloadSem.Release() - task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err)) + if task.Cancelled { + task.UpdateStatus(db, "error", fmt.Errorf("任务已取消")) + } else { + task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err)) + } return } GlobalDownloadSem.Release() + if task.Cancelled { + task.UpdateStatus(db, "error", fmt.Errorf("任务已取消")) + return + } + outputPath := task.TaskInDB.FilePath() videoPath := filepath.Join(task.Folder, strconv.FormatInt(task.ID, 10)+".video") audioPath := filepath.Join(task.Folder, strconv.FormatInt(task.ID, 10)+".audio") diff --git a/web/src/task/data.ts b/web/src/task/data.ts index a99a252..36b3f69 100644 --- a/web/src/task/data.ts +++ b/web/src/task/data.ts @@ -71,4 +71,9 @@ type ActiveTask = { export const deleteTask = async (id: number) => { const res = await fetch(`/api/deleteTask?id=${id}`).then(res => res.json()) as ResJSON if (!res.success) throw new Error(res.message) +} + +export const cancelTask = async (id: number) => { + const res = await fetch(`/api/cancelTask?id=${id}`).then(res => res.json()) as ResJSON + if (!res.success) throw new Error(res.message) } \ No newline at end of file diff --git a/web/src/task/index.ts b/web/src/task/index.ts index e68a897..c38d051 100644 --- a/web/src/task/index.ts +++ b/web/src/task/index.ts @@ -1,7 +1,7 @@ import van, { State } from 'vanjs-core' import { Route, goto, now } from 'vanjs-router' import { checkLogin, GLOBAL_HAS_LOGIN, GLOBAL_HIDE_PAGE, ResJSON, VanComponent } from '../mixin' -import { deleteTask, getActiveTask, getTaskList, showFile } from './data' +import { deleteTask, getActiveTask, getTaskList, showFile, cancelTask } from './data' import { TaskInDB, TaskStatus } from '../work/type' import { LoadingBox } from '../view' import { PlayerModalComp } from './playerModal' @@ -156,6 +156,24 @@ export class TaskRoute implements VanComponent { _that.DeleteSVG() ) ), + div({ + class: 'me-4', + hidden: task.statusState.val != 'waiting' && task.statusState.val != 'running' + }, + div({ + class: 'hover-btn text-danger', title: '取消任务', + onclick() { + if (!confirm('确定要取消该任务吗?')) return + cancelTask(task.id).then(() => { + task.statusState.val = 'error' + }).catch(error => { + alert(error.message) + }) + } + }, + _that.CancelSVG() + ) + ), ) }) ) @@ -226,6 +244,13 @@ export class TaskRoute implements VanComponent { ) } + CancelSVG() { + return svg({ style: `width: 1em; height: 1em`, fill: "currentColor", class: "bi bi-x-circle", viewBox: "0 0 16 16" }, + path({ "d": "M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" }), + path({ "d": "M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708" }), + ) + } + FolderSVG() { return svg({ style: `width: 1em; height: 1em`, fill: "currentColor", class: "bi bi-folder2", viewBox: "0 0 16 16" }, path({ "d": "M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5z" }),