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.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) {
+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)。
+21 -2
View File
@@ -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
+5
View File
@@ -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():
+3
View File
@@ -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),
}
+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
type="button"
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
}
>
<Trash2 size={12} />
{icon === 'trash' && <Trash2 size={12} />}
{icon === 'ban' && <Ban size={12} />}
{icon === 'refresh' && <RefreshCw size={12} />}
{title}
</button>
)
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 秒自动刷新)'}
</p>
</div>
{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} />
清空失败与完成记录
<div className="ml-auto flex items-center gap-2">
{activeTaskCount > 0 && (
<button
type="button"
disabled={batchBusy}
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"
>
<Ban 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 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>
</div>
</header>
<div className="flex flex-wrap items-center gap-2">