This commit is contained in:
truewhile
2026-08-26 09:51:27 +08:00
parent 0e7dbd6215
commit 9d359c40dd
6 changed files with 91 additions and 27 deletions
+1
View File
@@ -55,6 +55,7 @@ func registerAdminStrmRoutes(admin *gin.RouterGroup, svc *service.Container) {
admin.GET("/strm/uploads", uploadQueueHandler(svc)) admin.GET("/strm/uploads", uploadQueueHandler(svc))
admin.POST("/strm/uploads/:id/cancel", cancelStrmUploadHandler(svc)) admin.POST("/strm/uploads/:id/cancel", cancelStrmUploadHandler(svc))
admin.POST("/strm/uploads/:id/retry", retryStrmUploadHandler(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) { func registerAdminUserRoutes(admin *gin.RouterGroup, svc *service.Container) {
+11
View File
@@ -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)。 // strmPlayHandler 处理 strm 文件指向的播放请求(Emby/Infuse 直接请求,无 JWT)。
+21 -2
View File
@@ -334,13 +334,13 @@ func (r *StrmDownloadTaskRepository) RetryAllFailed(ctx context.Context) (int64,
return count, err return count, err
} }
// CancelPending 批量取消所有排队中的任务。 // CancelPending 批量取消所有排队中和进行中的任务。
func (r *StrmDownloadTaskRepository) CancelPending(ctx context.Context) (int64, error) { func (r *StrmDownloadTaskRepository) CancelPending(ctx context.Context) (int64, error) {
now := time.Now() now := time.Now()
var count int64 var count int64
err := withSQLiteBusyRetry(ctx, func() error { err := withSQLiteBusyRetry(ctx, func() error {
res := r.db.WithContext(ctx).Model(&model.StrmDownloadTask{}). res := r.db.WithContext(ctx).Model(&model.StrmDownloadTask{}).
Where("status = ?", model.StrmTaskPending). Where("status IN ?", []string{model.StrmTaskPending, model.StrmTaskRunning}).
Updates(map[string]any{ Updates(map[string]any{
"status": model.StrmTaskCanceled, "status": model.StrmTaskCanceled,
"error": "已批量取消", "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 统计某同步目录下目标仍在排队/进行的任务数(用于去重)。 // CountActive 统计某同步目录下目标仍在排队/进行的任务数(用于去重)。
func (r *StrmUploadTaskRepository) CountActive(ctx context.Context, syncPathID, localPath string) int64 { func (r *StrmUploadTaskRepository) CountActive(ctx context.Context, syncPathID, localPath string) int64 {
var count int64 var count int64
+5
View File
@@ -511,6 +511,11 @@ func (s *StrmService) CancelPendingDownloadTasks(ctx context.Context) (int64, er
return s.repo.StrmDownload.CancelPending(ctx) 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) { func sleepContext(ctx context.Context, d time.Duration) {
select { select {
case <-ctx.Done(): case <-ctx.Done():
+3
View File
@@ -159,6 +159,9 @@ export const strmAPI = {
cancelUpload: (id: string) => cancelUpload: (id: string) =>
api.post(`/admin/strm/uploads/${id}/cancel`).then((r) => r.data), 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) => retryUpload: (id: string) =>
api.post(`/admin/strm/uploads/${id}/retry`).then((r) => r.data), api.post(`/admin/strm/uploads/${id}/retry`).then((r) => r.data),
} }
+50 -25
View File
@@ -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) => (
<button <button
type="button" type="button"
disabled={batchBusy} disabled={batchBusy}
@@ -105,33 +105,45 @@ function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) {
'ml-auto inline-flex items-center gap-1 rounded-lg border px-2 py-1 text-xs font-semibold transition disabled:opacity-50 ' + cls 'ml-auto inline-flex items-center gap-1 rounded-lg border px-2 py-1 text-xs font-semibold transition disabled:opacity-50 ' + cls
} }
> >
<Trash2 size={12} /> {icon === 'trash' && <Trash2 size={12} />}
{icon === 'ban' && <Ban size={12} />}
{icon === 'refresh' && <RefreshCw size={12} />}
{title} {title}
</button> </button>
) )
const cancelAllPendingAction = () => {
const action = isDownload
? () => strmAPI.cancelPendingDownloads()
: () => strmAPI.cancelPendingUploads()
return runBatch(action, `确定取消所有排队中及进行中的${isDownload ? '下载' : '上传'}任务?`)
}
const batchActionByFilter = () => { 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 (!isDownload) return null
if (filter === 'done') if (filter === 'done')
return batchBtn( return batchBtn(
'清空成功记录', '清空成功记录',
'trash',
'border-gray-200 text-rose-500 hover:bg-rose-50', 'border-gray-200 text-rose-500 hover:bg-rose-50',
() => runBatch(() => strmAPI.clearDoneDownloads(), '确定清空所有已完成下载记录?'), () => runBatch(() => strmAPI.clearDoneDownloads(), '确定清空所有已完成下载记录?'),
) )
if (filter === 'failed') 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(), '确定重新入队所有失败下载任务?'), runBatch(() => strmAPI.retryFailedDownloads(), '确定重新入队所有失败下载任务?'),
) )
if (filter === 'pending')
return batchBtn(
'批量取消',
'border-gray-200 text-amber-600 hover:bg-amber-50',
() => runBatch(() => strmAPI.cancelPendingDownloads(), '确定取消所有排队中的下载任务?'),
)
return null return null
} }
const counts = snapshot?.counts const counts = snapshot?.counts
const activeTaskCount = (counts?.pending ?? 0) + (counts?.running ?? 0)
const tasks = snapshot?.tasks.filter((t) => filter === 'all' || t.status === filter) ?? [] const tasks = snapshot?.tasks.filter((t) => filter === 'all' || t.status === filter) ?? []
const isDownload = kind === 'download' const isDownload = kind === 'download'
const Icon = isDownload ? Download : Upload const Icon = isDownload ? Download : Upload
@@ -152,23 +164,36 @@ function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) {
: 'STRM 元数据上传情况(本地 → 远端网盘,3 秒自动刷新)'} : 'STRM 元数据上传情况(本地 → 远端网盘,3 秒自动刷新)'}
</p> </p>
</div> </div>
{isDownload && ( <div className="ml-auto flex items-center gap-2">
<button {activeTaskCount > 0 && (
type="button" <button
disabled={batchBusy} type="button"
onClick={() => disabled={batchBusy}
runBatch(() => strmAPI.clearFinishedDownloads(), '确定清空所有失败和成功的下载记录?') onClick={cancelAllPendingAction}
} className="inline-flex items-center gap-1.5 rounded-xl border border-amber-300 bg-amber-500/10 px-3 py-2 text-sm font-semibold text-amber-600 transition hover:bg-amber-500/20 disabled:opacity-50"
className="inline-flex items-center gap-1.5 rounded-xl border border-rose-200 px-3 py-2 text-sm font-semibold text-rose-500 transition hover:bg-rose-50 disabled:opacity-50" >
> <Ban size={14} />
<Trash2 size={14} /> 全部取消 ({activeTaskCount})
清空失败与完成记录 </button>
)}
{isDownload && (
<button
type="button"
disabled={batchBusy}
onClick={() =>
runBatch(() => strmAPI.clearFinishedDownloads(), '确定清空所有失败和成功的下载记录?')
}
className="inline-flex items-center gap-1.5 rounded-xl border border-rose-200 px-3 py-2 text-sm font-semibold text-rose-500 transition hover:bg-rose-50 disabled:opacity-50"
>
<Trash2 size={14} />
清空完成与失败
</button>
)}
<button type="button" onClick={refresh} className="inline-flex items-center gap-1.5 rounded-xl border border-gray-200 px-3 py-2 text-sm font-semibold text-ink-100 hover:bg-gray-50">
<RefreshCw size={14} />
刷新
</button> </button>
)} </div>
<button type="button" onClick={refresh} className="ml-auto inline-flex items-center gap-1.5 rounded-xl border border-gray-200 px-3 py-2 text-sm font-semibold text-ink-100 hover:bg-gray-50">
<RefreshCw size={14} />
刷新
</button>
</header> </header>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">