mirror of
https://github.com/truewhile/MeBox.git
synced 2026-11-16 04:44:23 +08:00
feat: 完善媒体维护、备份恢复与播放诊断
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/truewhile/MeBox/internal/service"
|
||||
)
|
||||
|
||||
func backgroundTasksHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if svc == nil || svc.Tasks == nil {
|
||||
c.JSON(http.StatusOK, service.TaskSnapshot{Active: []service.BackgroundTask{}, Recent: []service.BackgroundTask{}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, svc.Tasks.Snapshot())
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -41,12 +42,28 @@ func deleteBackupHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func downloadBackupHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
filename := c.Query("filename")
|
||||
path, err := svc.Backup.ResolvePath(filename)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.FileAttachment(path, filepath.Base(path))
|
||||
}
|
||||
}
|
||||
|
||||
func restoreBackupHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if err := svc.Backup.Restore(c.Request.Context(), c.Query("filename")); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "restored — restart the server to apply"})
|
||||
message := "数据库已恢复,请重启 MeBox 以刷新运行时配置"
|
||||
if svc.Repo.DB.Dialector.Name() == "sqlite" {
|
||||
message = "备份已校验并暂存,请重启 MeBox 后应用恢复"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": message, "restart_required": true})
|
||||
}
|
||||
}
|
||||
|
||||
+146
-105
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -654,6 +655,9 @@ func paginateSlice[T any](items []T, page, size int) []T {
|
||||
if len(items) == 0 {
|
||||
return []T{}
|
||||
}
|
||||
if page-1 > len(items)/size {
|
||||
return []T{}
|
||||
}
|
||||
start := (page - 1) * size
|
||||
if start >= len(items) {
|
||||
return []T{}
|
||||
@@ -668,127 +672,164 @@ func paginateSlice[T any](items []T, page, size int) []T {
|
||||
func searchMediaHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
q := c.Query("q")
|
||||
query := strings.TrimSpace(c.Query("q"))
|
||||
visibility := mediaVisibilityForRequest(c, svc)
|
||||
groupVersions := c.DefaultQuery("group_versions", "1") != "0"
|
||||
|
||||
fetchRemote := func(limit int) []model.Media {
|
||||
if svc.EmbyRemote == nil || strings.TrimSpace(q) == "" {
|
||||
return nil
|
||||
}
|
||||
remoteItems, _ := svc.EmbyRemote.RemoteSearchMedia(ctx, q, limit, visibility)
|
||||
return remoteItems
|
||||
filter := service.MediaSearchFilter{
|
||||
LibraryID: strings.TrimSpace(c.Query("library_id")),
|
||||
YearMin: boundedSearchYear(c.Query("year_min")),
|
||||
YearMax: boundedSearchYear(c.Query("year_max")),
|
||||
}
|
||||
|
||||
if c.DefaultQuery("group_series", "0") != "0" {
|
||||
localItems, err := svc.Media.SearchMediaVisible(ctx, q, 50000, visibility)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
for _, genre := range c.QueryArray("genre") {
|
||||
if genre = strings.TrimSpace(genre); genre != "" && len(filter.Genres) < 8 {
|
||||
filter.Genres = append(filter.Genres, genre)
|
||||
}
|
||||
remoteItems := fetchRemote(50000)
|
||||
all := service.GroupMediaSeriesItems(append(localItems, remoteItems...))
|
||||
|
||||
if c.Query("page") != "" || c.Query("page_size") != "" {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||||
paged := paginateSlice(all, page, size)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": paged,
|
||||
"total": len(all),
|
||||
"page": page,
|
||||
"page_size": size,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if len(all) > limit {
|
||||
all = all[:limit]
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": all})
|
||||
return
|
||||
}
|
||||
|
||||
if c.Query("page") != "" || c.Query("page_size") != "" {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||||
if !groupVersions {
|
||||
localItems, _, err := svc.Media.SearchMediaVisiblePage(ctx, q, 1, 50000, visibility)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
remoteItems := fetchRemote(size * 2)
|
||||
all := append(localItems, remoteItems...)
|
||||
paged := paginateSlice(all, page, size)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": paged,
|
||||
"total": len(all),
|
||||
"page": page,
|
||||
"page_size": size,
|
||||
})
|
||||
return
|
||||
}
|
||||
localItems, err := svc.Media.SearchMediaVisible(ctx, q, 50000, visibility)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
remoteItems := fetchRemote(size * 2)
|
||||
all := append(localItems, remoteItems...)
|
||||
grouped := service.GroupMediaVersions(all)
|
||||
paged := service.PaginateMediaItems(grouped, page, size)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": paged,
|
||||
"total": len(grouped),
|
||||
"page": page,
|
||||
"page_size": size,
|
||||
})
|
||||
return
|
||||
if filter.YearMin > 0 && filter.YearMax > 0 && filter.YearMin > filter.YearMax {
|
||||
filter.YearMin, filter.YearMax = filter.YearMax, filter.YearMin
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||||
if size < 1 {
|
||||
size = 50
|
||||
} else if size > 2000 {
|
||||
size = 2000
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
||||
if limit <= 0 {
|
||||
if limit < 1 {
|
||||
limit = 50
|
||||
} else if limit > 50000 {
|
||||
limit = 50000
|
||||
}
|
||||
if !groupVersions {
|
||||
localItems, err := svc.Media.SearchMediaVisible(ctx, q, limit, visibility)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
remoteItems := fetchRemote(limit)
|
||||
all := append(localItems, remoteItems...)
|
||||
if len(all) > limit {
|
||||
all = all[:limit]
|
||||
}
|
||||
if all == nil {
|
||||
all = []model.Media{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": all})
|
||||
return
|
||||
}
|
||||
|
||||
localItems, err := svc.Media.SearchMediaVisible(ctx, q, 50000, visibility)
|
||||
paginated := c.Query("page") != "" || c.Query("page_size") != ""
|
||||
sortBy := normalizeMediaSearchSort(c.Query("sort"))
|
||||
// Sorting and grouping must operate on the same candidate set before
|
||||
// either a page size or the caller's output limit is applied.
|
||||
items, err := svc.Media.SearchMediaVisibleFiltered(ctx, query, 50000, visibility, filter)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
remoteItems := fetchRemote(limit)
|
||||
all := append(localItems, remoteItems...)
|
||||
grouped := service.GroupMediaVersions(all)
|
||||
items := service.FirstMediaItems(grouped, limit)
|
||||
if items == nil {
|
||||
items = []service.MediaItem{}
|
||||
if svc.EmbyRemote != nil && query != "" && (filter.LibraryID == "" || service.IsEmbyRemoteID(filter.LibraryID)) {
|
||||
remote, _ := svc.EmbyRemote.RemoteSearchMedia(ctx, query, 50000, visibility)
|
||||
for _, item := range remote {
|
||||
if remoteSearchMatchesFilter(item, filter) {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
if c.DefaultQuery("group_series", "0") != "0" {
|
||||
items = service.GroupMediaSeriesItems(items)
|
||||
sortSearchMedia(items, sortBy)
|
||||
writeMediaSearchResults(c, items, page, size, limit, paginated)
|
||||
return
|
||||
}
|
||||
if c.DefaultQuery("group_versions", "1") != "0" {
|
||||
grouped := service.GroupMediaVersions(items)
|
||||
if sortBy != "" {
|
||||
sort.SliceStable(grouped, func(i, j int) bool {
|
||||
return mediaSearchSortLess(grouped[i].Media, grouped[j].Media, sortBy)
|
||||
})
|
||||
}
|
||||
writeMediaSearchResults(c, grouped, page, size, limit, paginated)
|
||||
return
|
||||
}
|
||||
sortSearchMedia(items, sortBy)
|
||||
writeMediaSearchResults(c, items, page, size, limit, paginated)
|
||||
}
|
||||
}
|
||||
|
||||
func writeMediaSearchResults[T any](c *gin.Context, items []T, page, size, limit int, paginated bool) {
|
||||
if items == nil {
|
||||
items = []T{}
|
||||
}
|
||||
if paginated {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"items": paginateSlice(items, page, size),
|
||||
"total": len(items), "page": page, "page_size": size,
|
||||
})
|
||||
return
|
||||
}
|
||||
if len(items) > limit {
|
||||
items = items[:limit]
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func remoteSearchMatchesFilter(item model.Media, filter service.MediaSearchFilter) bool {
|
||||
if filter.LibraryID != "" && item.LibraryID != filter.LibraryID && item.DisplayLibraryID != filter.LibraryID {
|
||||
return false
|
||||
}
|
||||
if filter.YearMin > 0 && item.Year < filter.YearMin || filter.YearMax > 0 && item.Year > filter.YearMax {
|
||||
return false
|
||||
}
|
||||
if len(filter.Genres) == 0 {
|
||||
return true
|
||||
}
|
||||
normalize := func(value string) string {
|
||||
return strings.ToLower(strings.ReplaceAll(strings.TrimSpace(value), " ", ""))
|
||||
}
|
||||
for _, genre := range strings.Split(strings.ReplaceAll(item.Genres, ",", ","), ",") {
|
||||
for _, wanted := range filter.Genres {
|
||||
if normalize(genre) == normalize(wanted) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeMediaSearchSort(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
switch value {
|
||||
case "title_asc", "year_desc", "rating_desc":
|
||||
return value
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func sortSearchMedia(items []model.Media, sortBy string) {
|
||||
if sortBy == "" {
|
||||
return
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
return mediaSearchSortLess(items[i], items[j], sortBy)
|
||||
})
|
||||
}
|
||||
|
||||
func mediaSearchSortLess(a, b model.Media, sortBy string) bool {
|
||||
switch sortBy {
|
||||
case "title_asc":
|
||||
left, right := strings.ToLower(strings.TrimSpace(a.Title)), strings.ToLower(strings.TrimSpace(b.Title))
|
||||
if left != right {
|
||||
return left < right
|
||||
}
|
||||
case "year_desc":
|
||||
if a.Year != b.Year {
|
||||
return a.Year > b.Year
|
||||
}
|
||||
case "rating_desc":
|
||||
if a.Rating != b.Rating {
|
||||
return a.Rating > b.Rating
|
||||
}
|
||||
}
|
||||
if !a.CreatedAt.Equal(b.CreatedAt) {
|
||||
return a.CreatedAt.After(b.CreatedAt)
|
||||
}
|
||||
return a.ID < b.ID
|
||||
}
|
||||
|
||||
func boundedSearchYear(value string) int {
|
||||
year, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil || year < 1870 || year > 2200 {
|
||||
return 0
|
||||
}
|
||||
return year
|
||||
}
|
||||
|
||||
func streamHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/truewhile/MeBox/internal/service"
|
||||
)
|
||||
|
||||
func mediaQualityHandler(svc *service.Container) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
} else if page > 1000000 {
|
||||
page = 1000000
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 50
|
||||
} else if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
issue := c.DefaultQuery("issue", "unmatched")
|
||||
if issue != "unmatched" && issue != "poster" && issue != "missing" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "未知检查分类"})
|
||||
return
|
||||
}
|
||||
items, total, err := svc.Media.QualityIssues(c.Request.Context(), issue, page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items, "total": total, "page": page, "page_size": pageSize})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/truewhile/MeBox/internal/config"
|
||||
"github.com/truewhile/MeBox/internal/middleware"
|
||||
"github.com/truewhile/MeBox/internal/model"
|
||||
"github.com/truewhile/MeBox/internal/repository"
|
||||
"github.com/truewhile/MeBox/internal/service"
|
||||
)
|
||||
|
||||
func TestSearchMediaSortLimitFiltersAndDeepPagination(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sqlDB, _ := db.DB()
|
||||
defer sqlDB.Close()
|
||||
if err := db.AutoMigrate(&model.Library{}, &model.Media{}, &model.User{}, &model.Setting{}, &model.PlayProfile{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repos := repository.New(db)
|
||||
lib := model.Library{Name: "Review", Path: "/review", Type: "movie", Enabled: true}
|
||||
if err := repos.Library.Create(t.Context(), &lib); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows := make([]model.Media, 2001)
|
||||
for i := range rows {
|
||||
rows[i] = model.Media{
|
||||
Base: model.Base{ID: fmt.Sprintf("review-%04d", i)}, LibraryID: lib.ID,
|
||||
Title: fmt.Sprintf("Review %04d", i), Path: fmt.Sprintf("/review/%04d.mp4", i), Year: 2020, Genres: "Comedy",
|
||||
}
|
||||
}
|
||||
rows[2000].Year = 2025
|
||||
rows[2000].Genres = "Drama"
|
||||
if err := db.CreateInBatches(rows, 100).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
svc := &service.Container{Repo: repos, Media: service.NewMediaService(&config.Config{}, zap.NewNop(), repos)}
|
||||
cases := []struct {
|
||||
name, query, expectedID string
|
||||
count, total, page, size int
|
||||
}{
|
||||
{name: "sorted output limit", query: "sort=year_desc&limit=1", expectedID: "review-2000", count: 1},
|
||||
{name: "page past old 2000 cap", query: "sort=title_asc&page=2001&page_size=1", expectedID: "review-2000", count: 1, total: 2001, page: 2001, size: 1},
|
||||
{name: "genre filtering", query: "genre=Drama&library_id=" + lib.ID, expectedID: "review-2000", count: 1},
|
||||
{name: "normalized page", query: "page=0&page_size=0", count: 50, total: 2001, page: 1, size: 50},
|
||||
{name: "bounded page size", query: "page=1&page_size=99999", count: 2000, total: 2001, page: 1, size: 2000},
|
||||
{name: "huge page does not overflow", query: "page=" + strconv.Itoa(int(^uint(0)>>1)) + "&page_size=2000", count: 0, total: 2001, page: int(^uint(0) >> 1), size: 2000},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Set(middleware.CtxUserID, "admin")
|
||||
ctx.Set(middleware.CtxUserRole, "admin")
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/api/media?q=Review&group_versions=0&"+tc.query, nil)
|
||||
searchMediaHandler(svc)(ctx)
|
||||
var response struct {
|
||||
Items []model.Media `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"page_size"`
|
||||
}
|
||||
if w.Code != http.StatusOK || json.Unmarshal(w.Body.Bytes(), &response) != nil {
|
||||
t.Fatalf("search failed: status=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if len(response.Items) != tc.count || response.Total != tc.total || response.Page != tc.page || response.Size != tc.size {
|
||||
t.Fatalf("response count=%d total=%d page=%d size=%d, want %#v", len(response.Items), response.Total, response.Page, response.Size, tc)
|
||||
}
|
||||
if tc.expectedID != "" && response.Items[0].ID != tc.expectedID {
|
||||
t.Fatalf("first ID=%q want %q", response.Items[0].ID, tc.expectedID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -27,7 +28,8 @@ func TestSearchMediaSeriesKeepsRemoteLibrariesSeparate(t *testing.T) {
|
||||
"TotalRecordCount": 1,
|
||||
"Items": []map[string]any{{
|
||||
"Id": "same-series", "Name": "龙珠改", "Type": "Series",
|
||||
"ProviderIds": map[string]string{"Tmdb": "61709"},
|
||||
"ProviderIds": map[string]string{"Tmdb": "61709"},
|
||||
"ProductionYear": 2024, "Genres": []string{"Science Fiction"},
|
||||
}},
|
||||
})
|
||||
}))
|
||||
@@ -108,4 +110,9 @@ func TestSearchMediaSeriesKeepsRemoteLibrariesSeparate(t *testing.T) {
|
||||
}
|
||||
seen[item.DisplayLibraryID] = true
|
||||
}
|
||||
remoteLibraryID := service.EncodeEmbyRemoteID("mount-a", "view-a")
|
||||
items, total := request("page=1&page_size=8&library_id=" + url.QueryEscape(remoteLibraryID) + "&year_min=2020&genre=Science+Fiction")
|
||||
if total != 1 || len(items) != 1 || items[0].DisplayLibraryID != remoteLibraryID {
|
||||
t.Fatalf("remote library/year/genre filters lost results: total=%d items=%#v", total, items)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,10 +149,10 @@ func readerDeleteSourceHandler(svc *service.Container) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
func readerDebugSourceHandler(svc *service.Container) gin.HandlerFunc {
|
||||
var body struct {
|
||||
Key string `json:"key" binding:"required"`
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
var body struct {
|
||||
Key string `json:"key" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
@@ -13,6 +13,8 @@ func registerAdminRoutes(api *gin.RouterGroup, cfg *config.Config, svc *service.
|
||||
admin := api.Group("/admin")
|
||||
admin.Use(middleware.AuthRequired(cfg.Secrets.JWTSecret), middleware.AdminRequired())
|
||||
registerAdminUserRoutes(admin, svc)
|
||||
admin.GET("/tasks", backgroundTasksHandler(svc))
|
||||
admin.GET("/quality", mediaQualityHandler(svc))
|
||||
registerAdminPermissionRoutes(admin, svc)
|
||||
registerAdminSystemRoutes(admin, svc)
|
||||
registerAdminBackupRoutes(admin, svc)
|
||||
@@ -143,6 +145,7 @@ func registerAdminSystemRoutes(admin *gin.RouterGroup, svc *service.Container) {
|
||||
func registerAdminBackupRoutes(admin *gin.RouterGroup, svc *service.Container) {
|
||||
admin.GET("/backups", listBackupsHandler(svc))
|
||||
admin.POST("/backups", createBackupHandler(svc))
|
||||
admin.GET("/backups/download", downloadBackupHandler(svc))
|
||||
admin.DELETE("/backups", deleteBackupHandler(svc))
|
||||
admin.POST("/backups/restore", restoreBackupHandler(svc))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user