diff --git a/internal/handler/routes_admin.go b/internal/handler/routes_admin.go index 6205d8a..2942b2f 100644 --- a/internal/handler/routes_admin.go +++ b/internal/handler/routes_admin.go @@ -55,6 +55,7 @@ func registerAdminStrmRoutes(admin *gin.RouterGroup, svc *service.Container) { admin.GET("/strm/uploads", uploadQueueHandler(svc)) admin.POST("/strm/uploads/:id/cancel", cancelStrmUploadHandler(svc)) admin.POST("/strm/uploads/:id/retry", retryStrmUploadHandler(svc)) + admin.POST("/strm/uploads/cancel-pending", cancelPendingUploadsHandler(svc)) } func registerAdminUserRoutes(admin *gin.RouterGroup, svc *service.Container) { diff --git a/internal/handler/strm.go b/internal/handler/strm.go index 12477fb..50ffb64 100644 --- a/internal/handler/strm.go +++ b/internal/handler/strm.go @@ -414,6 +414,17 @@ func cancelPendingDownloadsHandler(svc *service.Container) gin.HandlerFunc { } } +func cancelPendingUploadsHandler(svc *service.Container) gin.HandlerFunc { + return func(c *gin.Context) { + n, err := svc.Strm.CancelPendingUploadTasks(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"canceled": n}) + } +} + // ─── 公开播放端点 ────────────────────────────────────────────────────────────── // strmPlayHandler 处理 strm 文件指向的播放请求(Emby/Infuse 直接请求,无 JWT)。 diff --git a/internal/repository/strm_repository.go b/internal/repository/strm_repository.go index 55a111f..b660651 100644 --- a/internal/repository/strm_repository.go +++ b/internal/repository/strm_repository.go @@ -334,13 +334,13 @@ func (r *StrmDownloadTaskRepository) RetryAllFailed(ctx context.Context) (int64, return count, err } -// CancelPending 批量取消所有排队中的任务。 +// CancelPending 批量取消所有排队中和进行中的任务。 func (r *StrmDownloadTaskRepository) CancelPending(ctx context.Context) (int64, error) { now := time.Now() var count int64 err := withSQLiteBusyRetry(ctx, func() error { res := r.db.WithContext(ctx).Model(&model.StrmDownloadTask{}). - Where("status = ?", model.StrmTaskPending). + Where("status IN ?", []string{model.StrmTaskPending, model.StrmTaskRunning}). Updates(map[string]any{ "status": model.StrmTaskCanceled, "error": "已批量取消", @@ -526,6 +526,25 @@ func (r *StrmUploadTaskRepository) Delete(ctx context.Context, id string) error }) } +// CancelPending 批量取消所有排队中和进行中的任务。 +func (r *StrmUploadTaskRepository) CancelPending(ctx context.Context) (int64, error) { + now := time.Now() + var count int64 + err := withSQLiteBusyRetry(ctx, func() error { + res := r.db.WithContext(ctx).Model(&model.StrmUploadTask{}). + Where("status IN ?", []string{model.StrmTaskPending, model.StrmTaskRunning}). + Updates(map[string]any{ + "status": model.StrmTaskCanceled, + "error": "已批量取消", + "finished_at": now, + "updated_at": now, + }) + count = res.RowsAffected + return res.Error + }) + return count, err +} + // CountActive 统计某同步目录下目标仍在排队/进行的任务数(用于去重)。 func (r *StrmUploadTaskRepository) CountActive(ctx context.Context, syncPathID, localPath string) int64 { var count int64 diff --git a/internal/service/strm_queue.go b/internal/service/strm_queue.go index 20d87cc..f0cc6b7 100644 --- a/internal/service/strm_queue.go +++ b/internal/service/strm_queue.go @@ -511,6 +511,11 @@ func (s *StrmService) CancelPendingDownloadTasks(ctx context.Context) (int64, er return s.repo.StrmDownload.CancelPending(ctx) } +// CancelPendingUploadTasks 批量取消所有排队上传任务,返回取消数量。 +func (s *StrmService) CancelPendingUploadTasks(ctx context.Context) (int64, error) { + return s.repo.StrmUpload.CancelPending(ctx) +} + func sleepContext(ctx context.Context, d time.Duration) { select { case <-ctx.Done(): diff --git a/web/src/api/strm.ts b/web/src/api/strm.ts index 395f41d..d0d7856 100644 --- a/web/src/api/strm.ts +++ b/web/src/api/strm.ts @@ -159,6 +159,9 @@ export const strmAPI = { cancelUpload: (id: string) => api.post(`/admin/strm/uploads/${id}/cancel`).then((r) => r.data), + cancelPendingUploads: () => + api.post<{ canceled: number }>('/admin/strm/uploads/cancel-pending').then((r) => r.data), + retryUpload: (id: string) => api.post(`/admin/strm/uploads/${id}/retry`).then((r) => r.data), } \ No newline at end of file diff --git a/web/src/pages/StrmQueuePage.tsx b/web/src/pages/StrmQueuePage.tsx index ab9b09b..417dd4c 100644 --- a/web/src/pages/StrmQueuePage.tsx +++ b/web/src/pages/StrmQueuePage.tsx @@ -96,7 +96,7 @@ function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) { } } - const batchBtn = (title: string, cls: string, onClick: () => void) => ( + const batchBtn = (title: string, icon: 'trash' | 'ban' | 'refresh', cls: string, onClick: () => void) => ( ) + const cancelAllPendingAction = () => { + const action = isDownload + ? () => strmAPI.cancelPendingDownloads() + : () => strmAPI.cancelPendingUploads() + return runBatch(action, `确定取消所有排队中及进行中的${isDownload ? '下载' : '上传'}任务?`) + } + const batchActionByFilter = () => { + if (filter === 'pending' || filter === 'running') + return batchBtn( + '全部取消', + 'ban', + 'border-amber-200 text-amber-600 hover:bg-amber-50', + cancelAllPendingAction, + ) if (!isDownload) return null if (filter === 'done') return batchBtn( '清空成功记录', + 'trash', 'border-gray-200 text-rose-500 hover:bg-rose-50', () => runBatch(() => strmAPI.clearDoneDownloads(), '确定清空所有已完成下载记录?'), ) if (filter === 'failed') - return batchBtn('批量重试', 'border-gray-200 text-brand-500 hover:bg-brand-50', () => + return batchBtn('批量重试', 'refresh', 'border-gray-200 text-brand-500 hover:bg-brand-50', () => runBatch(() => strmAPI.retryFailedDownloads(), '确定重新入队所有失败下载任务?'), ) - if (filter === 'pending') - return batchBtn( - '批量取消', - 'border-gray-200 text-amber-600 hover:bg-amber-50', - () => runBatch(() => strmAPI.cancelPendingDownloads(), '确定取消所有排队中的下载任务?'), - ) return null } const counts = snapshot?.counts + const activeTaskCount = (counts?.pending ?? 0) + (counts?.running ?? 0) const tasks = snapshot?.tasks.filter((t) => filter === 'all' || t.status === filter) ?? [] const isDownload = kind === 'download' const Icon = isDownload ? Download : Upload @@ -152,23 +164,36 @@ function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) { : 'STRM 元数据上传情况(本地 → 远端网盘,3 秒自动刷新)'}

- {isDownload && ( - + )} + {isDownload && ( + + )} + - )} - +