mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 20:46:37 +08:00
5
This commit is contained in:
@@ -50,12 +50,14 @@ func registerAdminStrmRoutes(admin *gin.RouterGroup, svc *service.Container) {
|
|||||||
admin.POST("/strm/downloads/:id/retry", retryStrmDownloadHandler(svc))
|
admin.POST("/strm/downloads/:id/retry", retryStrmDownloadHandler(svc))
|
||||||
admin.POST("/strm/downloads/clear-done", clearDoneDownloadsHandler(svc))
|
admin.POST("/strm/downloads/clear-done", clearDoneDownloadsHandler(svc))
|
||||||
admin.POST("/strm/downloads/clear-finished", clearFinishedDownloadsHandler(svc))
|
admin.POST("/strm/downloads/clear-finished", clearFinishedDownloadsHandler(svc))
|
||||||
|
admin.POST("/strm/downloads/clear-canceled", clearCanceledDownloadsHandler(svc))
|
||||||
admin.POST("/strm/downloads/retry-failed", retryAllFailedDownloadsHandler(svc))
|
admin.POST("/strm/downloads/retry-failed", retryAllFailedDownloadsHandler(svc))
|
||||||
admin.POST("/strm/downloads/cancel-pending", cancelPendingDownloadsHandler(svc))
|
admin.POST("/strm/downloads/cancel-pending", cancelPendingDownloadsHandler(svc))
|
||||||
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))
|
admin.POST("/strm/uploads/cancel-pending", cancelPendingUploadsHandler(svc))
|
||||||
|
admin.POST("/strm/uploads/clear-canceled", clearCanceledUploadsHandler(svc))
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerAdminUserRoutes(admin *gin.RouterGroup, svc *service.Container) {
|
func registerAdminUserRoutes(admin *gin.RouterGroup, svc *service.Container) {
|
||||||
|
|||||||
@@ -392,6 +392,28 @@ func clearFinishedDownloadsHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearCanceledDownloadsHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
n, err := svc.Strm.ClearCanceledDownloadTasks(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"deleted": n})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearCanceledUploadsHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
n, err := svc.Strm.ClearCanceledUploadTasks(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"deleted": n})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func retryAllFailedDownloadsHandler(svc *service.Container) gin.HandlerFunc {
|
func retryAllFailedDownloadsHandler(svc *service.Container) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
n, err := svc.Strm.RetryAllFailedDownloadTasks(c.Request.Context())
|
n, err := svc.Strm.RetryAllFailedDownloadTasks(c.Request.Context())
|
||||||
|
|||||||
@@ -50,10 +50,16 @@ func TestStrmAdminRoutesAreRegistered(t *testing.T) {
|
|||||||
"GET /api/admin/strm/downloads",
|
"GET /api/admin/strm/downloads",
|
||||||
"POST /api/admin/strm/downloads/:id/cancel",
|
"POST /api/admin/strm/downloads/:id/cancel",
|
||||||
"POST /api/admin/strm/downloads/:id/retry",
|
"POST /api/admin/strm/downloads/:id/retry",
|
||||||
"GET /api/admin/strm/uploads",
|
"POST /api/admin/strm/downloads/clear-finished",
|
||||||
"POST /api/admin/strm/uploads/:id/cancel",
|
"POST /api/admin/strm/downloads/clear-canceled",
|
||||||
"POST /api/admin/strm/uploads/:id/retry",
|
"POST /api/admin/strm/downloads/retry-failed",
|
||||||
"GET /api/strm/play/:provider/:file",
|
"POST /api/admin/strm/downloads/cancel-pending",
|
||||||
|
"GET /api/admin/strm/uploads",
|
||||||
|
"POST /api/admin/strm/uploads/:id/cancel",
|
||||||
|
"POST /api/admin/strm/uploads/:id/retry",
|
||||||
|
"POST /api/admin/strm/uploads/cancel-pending",
|
||||||
|
"POST /api/admin/strm/uploads/clear-canceled",
|
||||||
|
"GET /api/strm/play/:provider/:file",
|
||||||
} {
|
} {
|
||||||
if !routes[want] {
|
if !routes[want] {
|
||||||
t.Fatalf("%s route is not registered", want)
|
t.Fatalf("%s route is not registered", want)
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ func historyDeleteHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "status must be completed or incomplete"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "status must be completed or incomplete"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res := q.Delete(&model.PlaybackHistory{})
|
res := q.Unscoped().Delete(&model.PlaybackHistory{})
|
||||||
if err := res.Error; err != nil {
|
if err := res.Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ func (r *ApiConfigRepository) Update(ctx context.Context, c *model.ApiConfig) er
|
|||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes an API config.
|
// Delete 物理删除 API 配置。
|
||||||
func (r *ApiConfigRepository) Delete(ctx context.Context, provider string) error {
|
func (r *ApiConfigRepository) Delete(ctx context.Context, provider string) error {
|
||||||
return r.db.WithContext(ctx).Where("provider = ?", provider).Delete(&model.ApiConfig{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("provider = ?", provider).Delete(&model.ApiConfig{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTestResult 更新测试结果。
|
// UpdateTestResult 更新测试结果。
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (r *FavoriteRepository) Toggle(ctx context.Context, userID, mediaID string)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return false, r.db.WithContext(ctx).Delete(&f).Error
|
return false, r.db.WithContext(ctx).Unscoped().Delete(&f).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListByUser returns all favourite media IDs for a user.
|
// ListByUser returns all favourite media IDs for a user.
|
||||||
|
|||||||
@@ -79,10 +79,9 @@ func (r *LibraryRepository) FindByID(ctx context.Context, id string) (*model.Lib
|
|||||||
return &l, nil
|
return &l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes a library and (soft) cascades to its media via repository
|
// Delete 物理删除媒体库。
|
||||||
// callers; we do not run CASCADE here to keep this method narrow.
|
|
||||||
func (r *LibraryRepository) Delete(ctx context.Context, id string) error {
|
func (r *LibraryRepository) Delete(ctx context.Context, id string) error {
|
||||||
return r.db.WithContext(ctx).Delete(&model.Library{}, "id = ?", id).Error
|
return r.db.WithContext(ctx).Unscoped().Delete(&model.Library{}, "id = ?", id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LibraryRepository) ListRoots(ctx context.Context, libraryID string) ([]model.LibraryRoot, error) {
|
func (r *LibraryRepository) ListRoots(ctx context.Context, libraryID string) ([]model.LibraryRoot, error) {
|
||||||
@@ -149,7 +148,7 @@ func (r *LibraryRepository) DeleteRoot(ctx context.Context, libraryID, rootID st
|
|||||||
if !r.hasLibraryRootsTable() {
|
if !r.hasLibraryRootsTable() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return r.db.WithContext(ctx).Where("library_id = ?", libraryID).Delete(&model.LibraryRoot{}, "id = ?", rootID).Error
|
return r.db.WithContext(ctx).Unscoped().Where("library_id = ?", libraryID).Delete(&model.LibraryRoot{}, "id = ?", rootID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LibraryRepository) hasLibraryRootsTable() bool {
|
func (r *LibraryRepository) hasLibraryRootsTable() bool {
|
||||||
|
|||||||
@@ -116,12 +116,12 @@ func (r *MediaRepository) ListByLibrariesFiltered(ctx context.Context, libraryID
|
|||||||
|
|
||||||
// DeleteByLibrary purges all media tied to a library.
|
// DeleteByLibrary purges all media tied to a library.
|
||||||
func (r *MediaRepository) DeleteByLibrary(ctx context.Context, libraryID string) error {
|
func (r *MediaRepository) DeleteByLibrary(ctx context.Context, libraryID string) error {
|
||||||
// FTS 行由 media 表上的触发器同步清理(软删/硬删都覆盖)。
|
// FTS 行由 media 表上的触发器同步清理(物理删除触发 FTS 清理)。
|
||||||
return r.db.WithContext(ctx).Where("library_id = ?", libraryID).Delete(&model.Media{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("library_id = ?", libraryID).Delete(&model.Media{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *MediaRepository) DeleteByLibraryRoot(ctx context.Context, libraryID, rootID string) error {
|
func (r *MediaRepository) DeleteByLibraryRoot(ctx context.Context, libraryID, rootID string) error {
|
||||||
return r.db.WithContext(ctx).
|
return r.db.WithContext(ctx).Unscoped().
|
||||||
Where("library_id = ? AND library_root_id = ?", libraryID, rootID).
|
Where("library_id = ? AND library_root_id = ?", libraryID, rootID).
|
||||||
Delete(&model.Media{}).Error
|
Delete(&model.Media{}).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ func (r *PermissionRepository) Upsert(ctx context.Context, p *model.UserPermissi
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes a permission record.
|
// Delete 物理删除权限记录。
|
||||||
func (r *PermissionRepository) Delete(ctx context.Context, userID string) error {
|
func (r *PermissionRepository) Delete(ctx context.Context, userID string) error {
|
||||||
return withSQLiteBusyRetry(ctx, func() error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
return r.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&model.UserPermission{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("user_id = ?", userID).Delete(&model.UserPermission{}).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ func (r *PlayProfileRepository) Update(ctx context.Context, id string, patch map
|
|||||||
Where("id = ?", id).Updates(patch).Error
|
Where("id = ?", id).Updates(patch).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete soft-deletes a profile.
|
// Delete 物理删除播放档案。
|
||||||
func (r *PlayProfileRepository) Delete(ctx context.Context, id string) error {
|
func (r *PlayProfileRepository) Delete(ctx context.Context, id string) error {
|
||||||
return r.db.WithContext(ctx).Delete(&model.PlayProfile{}, "id = ?", id).Error
|
return r.db.WithContext(ctx).Unscoped().Delete(&model.PlayProfile{}, "id = ?", id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearDefaultsFor resets is_default for all of a user's profiles.
|
// ClearDefaultsFor resets is_default for all of a user's profiles.
|
||||||
|
|||||||
@@ -72,10 +72,10 @@ func (r *RefreshTokenRepository) RevokeOldestActiveByUserID(ctx context.Context,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteExpired removes all expired refresh tokens.
|
// DeleteExpired 物理清理所有过期的 refresh tokens。
|
||||||
func (r *RefreshTokenRepository) DeleteExpired(ctx context.Context) error {
|
func (r *RefreshTokenRepository) DeleteExpired(ctx context.Context) error {
|
||||||
return withSQLiteBusyRetry(ctx, func() error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
return r.db.WithContext(ctx).Where("expires_at < ?", time.Now()).Delete(&model.RefreshToken{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("expires_at < ?", time.Now()).Delete(&model.RefreshToken{}).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ func (r *SettingRepository) Set(ctx context.Context, key, value string) error {
|
|||||||
return r.db.WithContext(ctx).Save(&s).Error
|
return r.db.WithContext(ctx).Save(&s).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes a setting key.
|
// Delete 物理删除设置键。
|
||||||
func (r *SettingRepository) Delete(ctx context.Context, key string) error {
|
func (r *SettingRepository) Delete(ctx context.Context, key string) error {
|
||||||
return r.db.WithContext(ctx).Where("key = ?", key).Delete(&model.Setting{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("key = ?", key).Delete(&model.Setting{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// All returns every key/value pair (used by the admin UI).
|
// All returns every key/value pair (used by the admin UI).
|
||||||
|
|||||||
@@ -66,9 +66,9 @@ func (r *StorageConfigRepository) Upsert(ctx context.Context, c *model.StorageCo
|
|||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes a storage config by ID.
|
// Delete 物理删除存储配置。
|
||||||
func (r *StorageConfigRepository) Delete(ctx context.Context, id string) error {
|
func (r *StorageConfigRepository) Delete(ctx context.Context, id string) error {
|
||||||
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.StorageConfig{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.StorageConfig{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindByID returns a storage config by ID.
|
// FindByID returns a storage config by ID.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func (r *StrmAccountRepository) Update(ctx context.Context, a *model.StrmAccount
|
|||||||
|
|
||||||
func (r *StrmAccountRepository) Delete(ctx context.Context, id string) error {
|
func (r *StrmAccountRepository) Delete(ctx context.Context, id string) error {
|
||||||
return withSQLiteBusyRetry(ctx, func() error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.StrmAccount{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.StrmAccount{}).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ func (r *StrmSyncPathRepository) Update(ctx context.Context, p *model.StrmSyncPa
|
|||||||
|
|
||||||
func (r *StrmSyncPathRepository) Delete(ctx context.Context, id string) error {
|
func (r *StrmSyncPathRepository) Delete(ctx context.Context, id string) error {
|
||||||
return withSQLiteBusyRetry(ctx, func() error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.StrmSyncPath{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.StrmSyncPath{}).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@ func (r *StrmDownloadTaskRepository) Update(ctx context.Context, t *model.StrmDo
|
|||||||
|
|
||||||
func (r *StrmDownloadTaskRepository) Delete(ctx context.Context, id string) error {
|
func (r *StrmDownloadTaskRepository) Delete(ctx context.Context, id string) error {
|
||||||
return withSQLiteBusyRetry(ctx, func() error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.StrmDownloadTask{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.StrmDownloadTask{}).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@ func (r *StrmDownloadTaskRepository) Delete(ctx context.Context, id string) erro
|
|||||||
func (r *StrmDownloadTaskRepository) ClearDone(ctx context.Context) (int64, error) {
|
func (r *StrmDownloadTaskRepository) ClearDone(ctx context.Context) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
err := withSQLiteBusyRetry(ctx, func() error {
|
err := withSQLiteBusyRetry(ctx, func() error {
|
||||||
res := r.db.WithContext(ctx).Where("status = ?", model.StrmTaskDone).Delete(&model.StrmDownloadTask{})
|
res := r.db.WithContext(ctx).Unscoped().Where("status = ?", model.StrmTaskDone).Delete(&model.StrmDownloadTask{})
|
||||||
count = res.RowsAffected
|
count = res.RowsAffected
|
||||||
return res.Error
|
return res.Error
|
||||||
})
|
})
|
||||||
@@ -305,7 +305,7 @@ func (r *StrmDownloadTaskRepository) ClearDone(ctx context.Context) (int64, erro
|
|||||||
func (r *StrmDownloadTaskRepository) ClearFinished(ctx context.Context) (int64, error) {
|
func (r *StrmDownloadTaskRepository) ClearFinished(ctx context.Context) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
err := withSQLiteBusyRetry(ctx, func() error {
|
err := withSQLiteBusyRetry(ctx, func() error {
|
||||||
res := r.db.WithContext(ctx).Where("status IN ?", []string{model.StrmTaskDone, model.StrmTaskFailed}).
|
res := r.db.WithContext(ctx).Unscoped().Where("status IN ?", []string{model.StrmTaskDone, model.StrmTaskFailed, model.StrmTaskCanceled}).
|
||||||
Delete(&model.StrmDownloadTask{})
|
Delete(&model.StrmDownloadTask{})
|
||||||
count = res.RowsAffected
|
count = res.RowsAffected
|
||||||
return res.Error
|
return res.Error
|
||||||
@@ -313,6 +313,17 @@ func (r *StrmDownloadTaskRepository) ClearFinished(ctx context.Context) (int64,
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearCanceled 清空全部已取消下载任务。
|
||||||
|
func (r *StrmDownloadTaskRepository) ClearCanceled(ctx context.Context) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := withSQLiteBusyRetry(ctx, func() error {
|
||||||
|
res := r.db.WithContext(ctx).Unscoped().Where("status = ?", model.StrmTaskCanceled).Delete(&model.StrmDownloadTask{})
|
||||||
|
count = res.RowsAffected
|
||||||
|
return res.Error
|
||||||
|
})
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
// RetryAllFailed 把所有失败任务重置回待处理,清空错误与重试计数。
|
// RetryAllFailed 把所有失败任务重置回待处理,清空错误与重试计数。
|
||||||
func (r *StrmDownloadTaskRepository) RetryAllFailed(ctx context.Context) (int64, error) {
|
func (r *StrmDownloadTaskRepository) RetryAllFailed(ctx context.Context) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
@@ -381,7 +392,7 @@ func (r *StrmDownloadTaskRepository) GetActiveLocalPathMap(ctx context.Context,
|
|||||||
|
|
||||||
func (r *StrmDownloadTaskRepository) DeleteFinishedOlderThan(ctx context.Context, before time.Time) error {
|
func (r *StrmDownloadTaskRepository) DeleteFinishedOlderThan(ctx context.Context, before time.Time) error {
|
||||||
return withSQLiteBusyRetry(ctx, func() error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
return r.db.WithContext(ctx).Where("status IN ? AND finished_at < ?",
|
return r.db.WithContext(ctx).Unscoped().Where("status IN ? AND finished_at < ?",
|
||||||
[]string{model.StrmTaskDone, model.StrmTaskFailed, model.StrmTaskCanceled}, before).
|
[]string{model.StrmTaskDone, model.StrmTaskFailed, model.StrmTaskCanceled}, before).
|
||||||
Delete(&model.StrmDownloadTask{}).Error
|
Delete(&model.StrmDownloadTask{}).Error
|
||||||
})
|
})
|
||||||
@@ -522,10 +533,21 @@ func (r *StrmUploadTaskRepository) Update(ctx context.Context, t *model.StrmUplo
|
|||||||
|
|
||||||
func (r *StrmUploadTaskRepository) Delete(ctx context.Context, id string) error {
|
func (r *StrmUploadTaskRepository) Delete(ctx context.Context, id string) error {
|
||||||
return withSQLiteBusyRetry(ctx, func() error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.StrmUploadTask{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.StrmUploadTask{}).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearCanceled 清空全部已取消上传任务。
|
||||||
|
func (r *StrmUploadTaskRepository) ClearCanceled(ctx context.Context) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := withSQLiteBusyRetry(ctx, func() error {
|
||||||
|
res := r.db.WithContext(ctx).Unscoped().Where("status = ?", model.StrmTaskCanceled).Delete(&model.StrmUploadTask{})
|
||||||
|
count = res.RowsAffected
|
||||||
|
return res.Error
|
||||||
|
})
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
// CancelPending 批量取消所有排队中和进行中的任务。
|
// CancelPending 批量取消所有排队中和进行中的任务。
|
||||||
func (r *StrmUploadTaskRepository) CancelPending(ctx context.Context) (int64, error) {
|
func (r *StrmUploadTaskRepository) CancelPending(ctx context.Context) (int64, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -573,7 +595,7 @@ func (r *StrmUploadTaskRepository) GetActiveLocalPathMap(ctx context.Context, sy
|
|||||||
|
|
||||||
func (r *StrmUploadTaskRepository) DeleteFinishedOlderThan(ctx context.Context, before time.Time) error {
|
func (r *StrmUploadTaskRepository) DeleteFinishedOlderThan(ctx context.Context, before time.Time) error {
|
||||||
return withSQLiteBusyRetry(ctx, func() error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
return r.db.WithContext(ctx).Where("status IN ? AND finished_at < ?",
|
return r.db.WithContext(ctx).Unscoped().Where("status IN ? AND finished_at < ?",
|
||||||
[]string{model.StrmTaskDone, model.StrmTaskFailed, model.StrmTaskCanceled}, before).
|
[]string{model.StrmTaskDone, model.StrmTaskFailed, model.StrmTaskCanceled}, before).
|
||||||
Delete(&model.StrmUploadTask{}).Error
|
Delete(&model.StrmUploadTask{}).Error
|
||||||
})
|
})
|
||||||
@@ -614,7 +636,7 @@ func (r *StrmDirCacheRepository) Set(ctx context.Context, syncPathID, dirID, pat
|
|||||||
|
|
||||||
func (r *StrmDirCacheRepository) DeleteBySyncPathID(ctx context.Context, syncPathID string) error {
|
func (r *StrmDirCacheRepository) DeleteBySyncPathID(ctx context.Context, syncPathID string) error {
|
||||||
return withSQLiteBusyRetry(ctx, func() error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
return r.db.WithContext(ctx).Where("sync_path_id = ?", syncPathID).Delete(&model.StrmDirCache{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("sync_path_id = ?", syncPathID).Delete(&model.StrmDirCache{}).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,26 +133,17 @@ func (r *UserRepository) TouchLogin(ctx context.Context, id string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes a user (soft-delete via gorm.DeletedAt), releases the unique
|
// Delete 物理删除用户并级联清理其关联记录。
|
||||||
// username, and drops Telegram bindings so future re-created users bind cleanly.
|
|
||||||
func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
||||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
var user model.User
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Where("id = ?", id).First(&user).Error; err != nil {
|
_ = tx.Unscoped().Where("user_id = ?", id).Delete(&model.RefreshToken{})
|
||||||
return err
|
_ = tx.Unscoped().Where("user_id = ?", id).Delete(&model.UserPermission{})
|
||||||
}
|
_ = tx.Unscoped().Where("user_id = ?", id).Delete(&model.PlayProfile{})
|
||||||
released := user.Username + "__deleted__" + time.Now().Format("20060102150405.000000000")
|
_ = tx.Unscoped().Where("user_id = ?", id).Delete(&model.PlaybackHistory{})
|
||||||
if len(released) > 64 {
|
_ = tx.Unscoped().Where("user_id = ?", id).Delete(&model.Favorite{})
|
||||||
sum := sha256.Sum256([]byte(user.ID + user.Username))
|
_ = tx.Unscoped().Where("user_id = ?", id).Delete(&model.UserDevice{})
|
||||||
base := user.Username
|
return tx.Unscoped().Delete(&model.User{}, "id = ?", id).Error
|
||||||
if len(base) > 43 {
|
})
|
||||||
base = base[:43]
|
|
||||||
}
|
|
||||||
released = base + "__deleted__" + hex.EncodeToString(sum[:])[:10]
|
|
||||||
}
|
|
||||||
if err := tx.Model(&model.User{}).Where("id = ?", id).Update("username", released).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Delete(&model.User{}, "id = ?", id).Error
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ func (s *MediaService) DeleteLibrary(ctx context.Context, id string) error {
|
|||||||
if err := tx.Unscoped().Where("library_id = ?", id).Delete(&model.Media{}).Error; err != nil {
|
if err := tx.Unscoped().Where("library_id = ?", id).Delete(&model.Media{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := hardDeleteLibraryRoots(ctx, tx, id); err != nil {
|
if err := hardDeleteLibraryRoots(ctx, tx, id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return tx.Delete(&model.Library{}, "id = ?", id).Error
|
return tx.Unscoped().Delete(&model.Library{}, "id = ?", id).Error
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.invalidateMediaCache(ctx)
|
s.invalidateMediaCache(ctx)
|
||||||
|
|||||||
@@ -10,25 +10,10 @@ import (
|
|||||||
|
|
||||||
const maxRecycleBinRecords = 200
|
const maxRecycleBinRecords = 200
|
||||||
|
|
||||||
// SoftDelete moves a media row to the recycle bin (gorm soft delete).
|
// SoftDelete 物理删除媒体记录(统一硬删除以降低 SQLite 存储与索引压力)。
|
||||||
// The on-disk file is kept; admins can purge it later.
|
|
||||||
func (s *MediaService) SoftDelete(ctx context.Context, id string) error {
|
func (s *MediaService) SoftDelete(ctx context.Context, id string) error {
|
||||||
media, err := s.repo.Media.FindByID(ctx, id)
|
err := s.repo.DB.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.Media{}).Error
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if media != nil && isCloudMediaPath(media.Path) {
|
|
||||||
err := s.repo.DB.WithContext(ctx).Unscoped().Where("id = ?", id).Delete(&model.Media{}).Error
|
|
||||||
if err == nil {
|
|
||||||
s.invalidateMediaCache(ctx)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = s.repo.DB.WithContext(ctx).Where("id = ?", id).Delete(&model.Media{}).Error
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if pruneErr := pruneRecycleBinRows(ctx, s.repo.DB, maxRecycleBinRecords); pruneErr != nil {
|
|
||||||
return pruneErr
|
|
||||||
}
|
|
||||||
s.invalidateMediaCache(ctx)
|
s.invalidateMediaCache(ctx)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -229,9 +229,9 @@ func (o *OrganizerService) replaceVersions(ctx context.Context, src string, exis
|
|||||||
o.log.Warn("organize replace remove existing failed",
|
o.log.Warn("organize replace remove existing failed",
|
||||||
zap.String("path", e), zap.Error(err))
|
zap.String("path", e), zap.Error(err))
|
||||||
}
|
}
|
||||||
if o.repo != nil && o.repo.DB != nil {
|
if o.repo != nil && o.repo.DB != nil {
|
||||||
_ = o.repo.DB.WithContext(ctx).Where("path = ?", e).Delete(&model.Media{}).Error
|
_ = o.repo.DB.WithContext(ctx).Unscoped().Where("path = ?", e).Delete(&model.Media{}).Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Move staged file + sidecars into the final path.
|
// Move staged file + sidecars into the final path.
|
||||||
if err := os.Rename(stage, dst); err != nil {
|
if err := os.Rename(stage, dst); err != nil {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func (o *OrganizerService) deleteMediaRowForPath(ctx context.Context, path strin
|
|||||||
if o == nil || o.repo == nil || o.repo.DB == nil {
|
if o == nil || o.repo == nil || o.repo.DB == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = o.repo.DB.WithContext(ctx).Where("path = ?", path).Delete(&model.Media{}).Error
|
_ = o.repo.DB.WithContext(ctx).Unscoped().Where("path = ?", path).Delete(&model.Media{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OrganizerService) mediaPathExists(ctx context.Context, path string) bool {
|
func (o *OrganizerService) mediaPathExists(ctx context.Context, path string) bool {
|
||||||
|
|||||||
@@ -196,18 +196,18 @@ func (p *PlaybackService) AddToPlaylist(ctx context.Context, playlistID, mediaID
|
|||||||
return p.repo.DB.Create(item).Error
|
return p.repo.DB.Create(item).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveFromPlaylist removes a media item from a playlist (idempotent).
|
// RemoveFromPlaylist 物理删除播放列表项(幂等)。
|
||||||
func (p *PlaybackService) RemoveFromPlaylist(ctx context.Context, playlistID, mediaID string) error {
|
func (p *PlaybackService) RemoveFromPlaylist(ctx context.Context, playlistID, mediaID string) error {
|
||||||
return p.repo.DB.
|
return p.repo.DB.WithContext(ctx).Unscoped().
|
||||||
Where("playlist_id = ? AND media_id = ?", playlistID, mediaID).
|
Where("playlist_id = ? AND media_id = ?", playlistID, mediaID).
|
||||||
Delete(&model.PlaylistItem{}).Error
|
Delete(&model.PlaylistItem{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePlaylist removes a playlist and all of its items.
|
// DeletePlaylist 物理删除播放列表及其全部条目。
|
||||||
func (p *PlaybackService) DeletePlaylist(ctx context.Context, playlistID string) error {
|
func (p *PlaybackService) DeletePlaylist(ctx context.Context, playlistID string) error {
|
||||||
if err := p.repo.DB.Where("playlist_id = ?", playlistID).
|
if err := p.repo.DB.WithContext(ctx).Unscoped().Where("playlist_id = ?", playlistID).
|
||||||
Delete(&model.PlaylistItem{}).Error; err != nil {
|
Delete(&model.PlaylistItem{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return p.repo.DB.Where("id = ?", playlistID).Delete(&model.Playlist{}).Error
|
return p.repo.DB.WithContext(ctx).Unscoped().Where("id = ?", playlistID).Delete(&model.Playlist{}).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,12 @@ import (
|
|||||||
"github.com/ShukeBta/MMTL/internal/model"
|
"github.com/ShukeBta/MMTL/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemovePath deletes the media row for a path that has disappeared from disk
|
// RemovePath 物理删除磁盘上已不存在的媒体记录。
|
||||||
// (incremental delete used by the watcher on Remove/Rename events).
|
|
||||||
func (s *ScannerService) RemovePath(ctx context.Context, path string) (int64, error) {
|
func (s *ScannerService) RemovePath(ctx context.Context, path string) (int64, error) {
|
||||||
if _, err := os.Stat(path); err == nil {
|
if _, err := os.Stat(path); err == nil {
|
||||||
return 0, nil // still exists; nothing to remove
|
return 0, nil // still exists; nothing to remove
|
||||||
}
|
}
|
||||||
res := s.repo.DB.WithContext(ctx).
|
res := s.repo.DB.WithContext(ctx).Unscoped().
|
||||||
Where("path = ?", path).
|
Where("path = ?", path).
|
||||||
Delete(&model.Media{})
|
Delete(&model.Media{})
|
||||||
if res.Error == nil && res.RowsAffected > 0 {
|
if res.Error == nil && res.RowsAffected > 0 {
|
||||||
@@ -55,7 +54,7 @@ func (s *ScannerService) pruneMissingMedia(ctx context.Context, libraryID string
|
|||||||
}
|
}
|
||||||
stale = append(stale, row.ID)
|
stale = append(stale, row.ID)
|
||||||
}
|
}
|
||||||
return s.deleteMediaByIDs(ctx, stale, false)
|
return s.deleteMediaByIDs(ctx, stale, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScannerService) pruneMissingMediaForRoot(ctx context.Context, libraryID, rootID, rootPath string, seen map[string]struct{}) (int64, error) {
|
func (s *ScannerService) pruneMissingMediaForRoot(ctx context.Context, libraryID, rootID, rootPath string, seen map[string]struct{}) (int64, error) {
|
||||||
@@ -92,7 +91,7 @@ func (s *ScannerService) pruneMissingMediaForRoot(ctx context.Context, libraryID
|
|||||||
}
|
}
|
||||||
stale = append(stale, row.ID)
|
stale = append(stale, row.ID)
|
||||||
}
|
}
|
||||||
return s.deleteMediaByIDs(ctx, stale, false)
|
return s.deleteMediaByIDs(ctx, stale, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathBelongsToRoot(pathValue, rootPath string) bool {
|
func pathBelongsToRoot(pathValue, rootPath string) bool {
|
||||||
|
|||||||
@@ -501,6 +501,16 @@ func (s *StrmService) ClearFinishedDownloadTasks(ctx context.Context) (int64, er
|
|||||||
return s.repo.StrmDownload.ClearFinished(ctx)
|
return s.repo.StrmDownload.ClearFinished(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearCanceledDownloadTasks 清空全部已取消的下载记录,返回删除数量。
|
||||||
|
func (s *StrmService) ClearCanceledDownloadTasks(ctx context.Context) (int64, error) {
|
||||||
|
return s.repo.StrmDownload.ClearCanceled(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearCanceledUploadTasks 清空全部已取消的上传记录,返回删除数量。
|
||||||
|
func (s *StrmService) ClearCanceledUploadTasks(ctx context.Context) (int64, error) {
|
||||||
|
return s.repo.StrmUpload.ClearCanceled(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// RetryAllFailedDownloadTasks 批量重试所有失败下载任务,返回重新入队数量。
|
// RetryAllFailedDownloadTasks 批量重试所有失败下载任务,返回重新入队数量。
|
||||||
func (s *StrmService) RetryAllFailedDownloadTasks(ctx context.Context) (int64, error) {
|
func (s *StrmService) RetryAllFailedDownloadTasks(ctx context.Context) (int64, error) {
|
||||||
return s.repo.StrmDownload.RetryAllFailed(ctx)
|
return s.repo.StrmDownload.RetryAllFailed(ctx)
|
||||||
|
|||||||
@@ -143,6 +143,9 @@ export const strmAPI = {
|
|||||||
clearFinishedDownloads: () =>
|
clearFinishedDownloads: () =>
|
||||||
api.post<{ deleted: number }>('/admin/strm/downloads/clear-finished').then((r) => r.data),
|
api.post<{ deleted: number }>('/admin/strm/downloads/clear-finished').then((r) => r.data),
|
||||||
|
|
||||||
|
clearCanceledDownloads: () =>
|
||||||
|
api.post<{ deleted: number }>('/admin/strm/downloads/clear-canceled').then((r) => r.data),
|
||||||
|
|
||||||
retryFailedDownloads: () =>
|
retryFailedDownloads: () =>
|
||||||
api.post<{ retried: number }>('/admin/strm/downloads/retry-failed').then((r) => r.data),
|
api.post<{ retried: number }>('/admin/strm/downloads/retry-failed').then((r) => r.data),
|
||||||
|
|
||||||
@@ -162,6 +165,9 @@ export const strmAPI = {
|
|||||||
cancelPendingUploads: () =>
|
cancelPendingUploads: () =>
|
||||||
api.post<{ canceled: number }>('/admin/strm/uploads/cancel-pending').then((r) => r.data),
|
api.post<{ canceled: number }>('/admin/strm/uploads/cancel-pending').then((r) => r.data),
|
||||||
|
|
||||||
|
clearCanceledUploads: () =>
|
||||||
|
api.post<{ deleted: number }>('/admin/strm/uploads/clear-canceled').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),
|
||||||
}
|
}
|
||||||
@@ -127,6 +127,19 @@ function StrmQueuePanel({ kind }: { kind: 'download' | 'upload' }) {
|
|||||||
'border-amber-200 text-amber-600 hover:bg-amber-50',
|
'border-amber-200 text-amber-600 hover:bg-amber-50',
|
||||||
cancelAllPendingAction,
|
cancelAllPendingAction,
|
||||||
)
|
)
|
||||||
|
if (filter === 'canceled')
|
||||||
|
return batchBtn(
|
||||||
|
'清空已取消记录',
|
||||||
|
'trash',
|
||||||
|
'border-gray-200 text-rose-500 hover:bg-rose-50',
|
||||||
|
() =>
|
||||||
|
runBatch(
|
||||||
|
isDownload
|
||||||
|
? () => strmAPI.clearCanceledDownloads()
|
||||||
|
: () => strmAPI.clearCanceledUploads(),
|
||||||
|
`确定清空所有已取消的${isDownload ? '下载' : '上传'}记录?`,
|
||||||
|
),
|
||||||
|
)
|
||||||
if (!isDownload) return null
|
if (!isDownload) return null
|
||||||
if (filter === 'done')
|
if (filter === 'done')
|
||||||
return batchBtn(
|
return batchBtn(
|
||||||
|
|||||||
Reference in New Issue
Block a user