diff --git a/internal/router/task.go b/internal/router/task.go index b7194f1..6d3a7fa 100644 --- a/internal/router/task.go +++ b/internal/router/task.go @@ -7,6 +7,7 @@ import ( "os/exec" "runtime" "strconv" + "time" "bilidown/internal/task" "bilidown/internal/util" @@ -24,54 +25,90 @@ func createTask(w http.ResponseWriter, r *http.Request) { util.Res{Success: false, Message: "参数错误"}.Write(w) return } + + if len(body) == 0 { + util.Res{Success: false, Message: "任务列表为空"}.Write(w) + return + } + db := util.MustGetDB() defer db.Close() - // 预先获取下载目录,避免循环中重复查询 + // 预先获取下载目录 folder, err := util.GetCurrentFolder(db) if err != nil { util.Res{Success: false, Message: fmt.Sprintf("获取下载目录失败: %v", err)}.Write(w) return } + // 批量验证参数 for _, item := range body { if !util.CheckBvidFormat(item.Bvid) { - util.Res{Success: false, Message: "bvid 格式错误"}.Write(w) + util.Res{Success: false, Message: fmt.Sprintf("bvid 格式错误: %s", item.Bvid)}.Write(w) return } - if item.Cover == "" || item.Title == "" || item.Owner == "" { - util.Res{Success: false, Message: "参数错误"}.Write(w) + if item.Title == "" || item.Owner == "" { + util.Res{Success: false, Message: "标题或作者为空"}.Write(w) + return + } + } + + // 批量数据库插入(单事务) + tx, err := db.Begin() + if err != nil { + util.Res{Success: false, Message: fmt.Sprintf("开启事务失败: %v", err)}.Write(w) + return + } + + stmt, err := tx.Prepare(`INSERT INTO "task" ("bvid", "cid", "format", "title", "owner", "cover", "status", "folder", "duration", "download_type") + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) + if err != nil { + tx.Rollback() + util.Res{Success: false, Message: fmt.Sprintf("准备语句失败: %v", err)}.Write(w) + return + } + defer stmt.Close() + + tasks := make([]*task.Task, 0, len(body)) + for _, item := range body { + item.Folder = folder + item.Status = "waiting" + item.Title = util.FilterFileName(item.Title) + _task := task.Task{TaskInDB: item} + + util.SqliteLock.Lock() + result, err := stmt.Exec( + item.Bvid, item.Cid, item.Format, item.Title, item.Owner, + item.Cover, item.Status, item.Folder, item.Duration, item.DownloadType, + ) + util.SqliteLock.Unlock() + + if err != nil { + tx.Rollback() + util.Res{Success: false, Message: fmt.Sprintf("插入任务失败: %v", err)}.Write(w) return } - if !util.IsValidURL(item.Cover) { - util.Res{Success: false, Message: "封面链接格式错误"}.Write(w) - return - } - if !util.IsValidURL(item.Audio) { - util.Res{Success: false, Message: "音频链接格式错误"}.Write(w) - return - } - if !util.IsValidURL(item.Video) { - util.Res{Success: false, Message: "视频链接格式错误"}.Write(w) - return - } - if !util.IsValidFormatCode(item.Format) { - util.Res{Success: false, Message: "清晰度代码错误"}.Write(w) - return - } - item.Folder = folder - item.Status = "waiting" - _task := task.Task{TaskInDB: item} - _task.Title = util.FilterFileName(_task.Title) - err = _task.Create(db) - if err != nil { - util.Res{Success: false, Message: fmt.Sprintf("_task.Create: %v.", err)}.Write(w) - return - } + _task.ID, _ = result.LastInsertId() + _task.CreateAt = time.Now() + tasks = append(tasks, &_task) + } + + util.SqliteLock.Lock() + err = tx.Commit() + util.SqliteLock.Unlock() + + if err != nil { + util.Res{Success: false, Message: fmt.Sprintf("提交事务失败: %v", err)}.Write(w) + return + } + + // 批量启动下载任务 + for _, _task := range tasks { go _task.Start() } - util.Res{Success: true, Message: "创建成功"}.Write(w) + + util.Res{Success: true, Message: fmt.Sprintf("成功创建 %d 个任务", len(tasks))}.Write(w) } func getActiveTask(w http.ResponseWriter, r *http.Request) { diff --git a/web/src/task/index.ts b/web/src/task/index.ts index b0fdaa6..38c33a4 100644 --- a/web/src/task/index.ts +++ b/web/src/task/index.ts @@ -55,34 +55,10 @@ export class TaskRoute implements VanComponent { t.selected.val && !t.deleting.val && (t.statusState.val == 'waiting' || t.statusState.val == 'running') )) - /** 批量操作进度 */ - batchProgress = van.state(0) - batchTotal = van.state(0) - batchOperation = van.state<'cancel' | 'delete' | ''>('') - batchInProgress = van.derive(() => this.batchTotal.val > 0 && this.batchProgress.val < this.batchTotal.val) - constructor() { this.element = this.Root() } - /** 固定在顶部的进度条 */ - BatchProgressBar() { - return div({ - class: 'sticky-top bg-body p-3 border-bottom', - hidden: () => !this.batchInProgress.val, - style: 'z-index: 10;' - }, - div({ class: 'text-center fs-5' }, () => { - const op = this.batchOperation.val == 'cancel' ? '取消' : '删除' - return `正在批量${op}任务 (${this.batchProgress.val}/${this.batchTotal.val})` - }), - div({ class: 'progress' }, div({ - class: 'progress-bar progress-bar-striped progress-bar-animated bg-primary', - style: () => `width: ${this.batchProgress.val / this.batchTotal.val * 100}%` - })), - ) - } - Root() { const _that = this return Route({ @@ -91,8 +67,6 @@ export class TaskRoute implements VanComponent { return div( () => _that.loading.val ? LoadingBox() : '', () => div({ class: 'vstack gap-3', hidden: _that.loading.val }, - // 固定在顶部的批量操作进度条 - _that.BatchProgressBar(), // 批量操作工具栏 div({ class: 'hstack gap-2', hidden: () => _that.taskList.val.length == 0 }, input({ @@ -110,54 +84,29 @@ export class TaskRoute implements VanComponent { button({ class: 'btn btn-sm btn-outline-danger', hidden: () => _that.selectedCancellable.val.length == 0, - disabled: () => _that.batchInProgress.val, onclick() { if (!confirm(`确定要取消选中的 ${_that.selectedCancellable.val.length} 个任务吗?`)) return - const tasks = [..._that.selectedCancellable.val] - _that.batchOperation.val = 'cancel' - _that.batchTotal.val = tasks.length - _that.batchProgress.val = 0 - - let completed = 0 - tasks.forEach(t => { - cancelTask(t.id).then(() => { + const ids = _that.selectedCancellable.val.map(t => t.id) + cancelTasks(ids).then(() => { + _that.selectedCancellable.val.forEach(t => { t.statusState.val = 'error' t.selected.val = false - }).finally(() => { - completed++ - _that.batchProgress.val = completed - if (completed >= tasks.length) { - setTimeout(() => { - _that.batchTotal.val = 0 - _that.batchProgress.val = 0 - }, 500) - } }) + }).catch(error => { + alert(error.message) }) } }, () => `批量取消 (${_that.selectedCancellable.val.length})`), button({ class: 'btn btn-sm btn-outline-secondary', hidden: () => _that.selectedDeletable.val.length == 0, - disabled: () => _that.batchInProgress.val, onclick() { if (!confirm(`确定要删除选中的 ${_that.selectedDeletable.val.length} 个任务吗?`)) return - const tasks = [..._that.selectedDeletable.val] - _that.batchOperation.val = 'delete' - _that.batchTotal.val = tasks.length - _that.batchProgress.val = 0 - - const ids = tasks.map(t => t.id) + const ids = _that.selectedDeletable.val.map(t => t.id) deleteTasks(ids).then(() => { - _that.batchProgress.val = tasks.length _that.taskList.val = _that.taskList.val.filter(taskInDB => !ids.includes(taskInDB.id)) }).catch(error => { alert(error.message) - }).finally(() => { - setTimeout(() => { - _that.batchTotal.val = 0 - _that.batchProgress.val = 0 - }, 500) }) } }, () => `批量删除 (${_that.selectedDeletable.val.length})`),