Sync from /srv/compose/unified-media-manager
This commit is contained in:
220
internal/api/router.go
Normal file
220
internal/api/router.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/TopherMayor/unified-media-manager/internal/config"
|
||||
"github.com/TopherMayor/unified-media-manager/internal/db"
|
||||
"github.com/TopherMayor/unified-media-manager/internal/service"
|
||||
"github.com/TopherMayor/unified-media-manager/internal/worker"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
type Services struct {
|
||||
DB *db.DB
|
||||
Media *service.MediaService
|
||||
Queue *service.QueueService
|
||||
Indexer *service.IndexerService
|
||||
Blocklist *service.BlocklistService
|
||||
Dashboard *service.DashboardService
|
||||
Quality *service.QualityService
|
||||
DownloadClient *service.DownloadClientService
|
||||
Search *service.SearchService
|
||||
Import *service.ImportService
|
||||
Metadata *service.MetadataService
|
||||
Subtitle *service.SubtitleService
|
||||
RootFolder *service.RootFolderService
|
||||
Tag *service.TagService
|
||||
Scheduler *worker.Scheduler
|
||||
User *service.UserService
|
||||
Activity *service.ActivityService
|
||||
Safety *service.SafetyService
|
||||
Request *service.RequestService
|
||||
Notification *service.NotificationService
|
||||
Discover *service.DiscoverService
|
||||
MediaDetail *service.MediaDetailService
|
||||
Calendar *service.CalendarService
|
||||
}
|
||||
|
||||
func NewRouter(cfg *config.Config, svc *Services) *echo.Echo {
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(middleware.Recover())
|
||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
AllowOrigins: []string{cfg.FrontendURL},
|
||||
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
|
||||
}))
|
||||
e.Use(cacheControlMiddleware())
|
||||
|
||||
e.GET("/health/live", healthLive)
|
||||
e.GET("/health/ready", healthReady(svc.DB, cfg))
|
||||
|
||||
g := e.Group("/api")
|
||||
|
||||
g.GET("/media", listMedia(svc.Media))
|
||||
g.GET("/media/:type/:id", getMedia(svc.Media))
|
||||
g.GET("/media/:type/:id/detail", getFullMediaDetail(svc.MediaDetail))
|
||||
g.POST("/media", createMedia(svc.Media))
|
||||
g.PUT("/media/:type/:id", updateMedia(svc.Media))
|
||||
g.DELETE("/media/:type/:id", deleteMedia(svc.Media))
|
||||
|
||||
g.GET("/search", searchMedia(svc.Media))
|
||||
g.GET("/search/missing", searchMissing(svc.Media))
|
||||
g.GET("/search/upgrades", searchUpgrades(svc.Media))
|
||||
|
||||
g.GET("/queue", listQueue(svc.Queue))
|
||||
g.DELETE("/queue/:id", deleteQueueItem(svc.Queue))
|
||||
g.DELETE("/queue/batch", batchDeleteQueue(svc.Queue))
|
||||
g.POST("/queue/clear", clearQueue(svc.Queue))
|
||||
g.POST("/queue/:id/retry", retryQueueItem(svc.Queue))
|
||||
g.POST("/queue/retry-failed", retryFailedQueue(svc.Queue))
|
||||
|
||||
g.GET("/blocklist", listBlocklist(svc.Blocklist))
|
||||
g.DELETE("/blocklist/:id", deleteBlocklistItem(svc.Blocklist))
|
||||
g.DELETE("/blocklist", clearBlocklist(svc.Blocklist))
|
||||
g.DELETE("/blocklist/expired", clearExpiredBlocklist(svc.Blocklist))
|
||||
g.POST("/blocklist", addBlocklistItem(svc.Blocklist))
|
||||
|
||||
g.GET("/indexers", listIndexers(svc.Indexer))
|
||||
g.POST("/indexers", createIndexer(svc.Indexer))
|
||||
g.POST("/indexers/validate-cardigann", validateCardigannDefinition())
|
||||
g.PUT("/indexers/:id", updateIndexer(svc.Indexer))
|
||||
g.DELETE("/indexers/:id", deleteIndexer(svc.Indexer))
|
||||
g.POST("/indexers/:id/test", testIndexer(svc.Indexer))
|
||||
g.GET("/indexers/:id/stats", indexerStats(svc.Indexer))
|
||||
|
||||
g.GET("/dashboard", dashboard(svc.Dashboard))
|
||||
|
||||
g.GET("/activity", listActivity(svc.Activity))
|
||||
|
||||
g.GET("/quality-profiles", listQualityProfiles(svc.Quality))
|
||||
g.POST("/quality-profiles", createQualityProfile(svc.Quality))
|
||||
g.PUT("/quality-profiles/:id", updateQualityProfile(svc.Quality))
|
||||
g.DELETE("/quality-profiles/:id", deleteQualityProfile(svc.Quality))
|
||||
|
||||
g.GET("/download-clients", listDownloadClients(svc.DownloadClient))
|
||||
g.POST("/download-clients", createDownloadClient(svc.DownloadClient))
|
||||
g.PUT("/download-clients/:id", updateDownloadClient(svc.DownloadClient))
|
||||
g.DELETE("/download-clients/:id", deleteDownloadClient(svc.DownloadClient))
|
||||
g.POST("/download-clients/:id/test", testDownloadClient(svc.DownloadClient))
|
||||
|
||||
g.GET("/releases/search", searchReleases(svc.Search))
|
||||
g.POST("/releases/grab", grabRelease(svc.Search, svc.DownloadClient, svc.Queue, svc.Safety, svc.Activity))
|
||||
|
||||
g.POST("/imports/trigger", triggerImport(svc.Import))
|
||||
g.GET("/imports/history", listImportHistory(svc.Import, svc.DB))
|
||||
|
||||
g.POST("/media/:type/:id/refresh-metadata", refreshMetadata(svc.Metadata))
|
||||
g.POST("/media/refresh-all", refreshAllMetadata(svc.Metadata))
|
||||
g.GET("/images/:type/:filename", serveImage(cfg.ImageDir))
|
||||
|
||||
g.GET("/media/:type/:id/subtitles/search", searchSubtitles(svc.Subtitle, svc.Media))
|
||||
g.POST("/media/:type/:id/subtitles/download", downloadSubtitle(svc.Subtitle, svc.Media))
|
||||
g.POST("/media/:type/:id/subtitles/extract", extractSubtitles(svc.Subtitle, svc.Media))
|
||||
|
||||
g.GET("/root-folders", listRootFolders(svc.RootFolder))
|
||||
g.POST("/root-folders", createRootFolder(svc.RootFolder))
|
||||
g.DELETE("/root-folders/:id", deleteRootFolder(svc.RootFolder))
|
||||
|
||||
g.GET("/tags", listTags(svc.Tag))
|
||||
g.POST("/tags", createTag(svc.Tag))
|
||||
g.DELETE("/tags/:id", deleteTag(svc.Tag))
|
||||
|
||||
if svc.Scheduler != nil {
|
||||
g.GET("/workers", listWorkers(svc.Scheduler))
|
||||
g.GET("/workers/:name/history", workerHistory(svc.Scheduler))
|
||||
g.PUT("/workers/:name", updateWorker(svc.Scheduler))
|
||||
g.POST("/workers/:name/trigger", triggerWorker(svc.Scheduler))
|
||||
}
|
||||
|
||||
// Notification routes
|
||||
g.GET("/notifications/channels", listNotificationChannels(svc.Notification))
|
||||
g.POST("/notifications/channels", createNotificationChannel(svc.Notification))
|
||||
g.PUT("/notifications/channels/:id", updateNotificationChannel(svc.Notification))
|
||||
g.DELETE("/notifications/channels/:id", deleteNotificationChannel(svc.Notification))
|
||||
g.POST("/notifications/channels/:id/test", testNotificationChannel(svc.Notification))
|
||||
g.GET("/notifications/queue", listNotificationQueue(svc.Notification))
|
||||
|
||||
// Discover routes
|
||||
if svc.Discover != nil {
|
||||
g.GET("/discover/trending", listTrending(svc.Discover))
|
||||
g.GET("/discover/popular", listPopular(svc.Discover))
|
||||
g.POST("/discover/add", addFromDiscover(svc.Discover))
|
||||
}
|
||||
|
||||
// Calendar route
|
||||
g.GET("/calendar", listCalendarEvents(svc.Calendar))
|
||||
|
||||
// Request routes — protected by API key auth
|
||||
apiKeyAuth := newAPIKeyAuth(svc.User)
|
||||
g.GET("/requests", listRequests(svc.Request, svc.User), apiKeyAuth)
|
||||
g.POST("/requests", createRequest(svc.Request, svc.User), apiKeyAuth)
|
||||
g.GET("/requests/stats", requestStats(svc.Request, svc.User), apiKeyAuth)
|
||||
g.PUT("/requests/:id/approve", approveRequest(svc.Request, svc.User), apiKeyAuth)
|
||||
g.PUT("/requests/:id/reject", rejectRequest(svc.Request, svc.User), apiKeyAuth)
|
||||
g.DELETE("/requests/:id", withdrawRequest(svc.Request, svc.User), apiKeyAuth)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
type paginatedResponse struct {
|
||||
Data interface{} `json:"data"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
func newAPIKeyAuth(userSvc *service.UserService) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
key := c.Request().Header.Get("X-API-Key")
|
||||
if key == "" {
|
||||
key = c.QueryParam("api_key")
|
||||
}
|
||||
if key == "" {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "API key required"})
|
||||
}
|
||||
user, err := userSvc.GetUserByAPIKey(c.Request().Context(), key)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid API key"})
|
||||
}
|
||||
c.Set("user", user)
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cacheControlMiddleware() echo.MiddlewareFunc {
|
||||
shortCache := map[string]bool{
|
||||
"/api/quality-profiles": true,
|
||||
"/api/download-clients": true,
|
||||
"/api/root-folders": true,
|
||||
"/api/tags": true,
|
||||
"/api/indexers": true,
|
||||
"/api/workers": true,
|
||||
}
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
err := next(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := c.Request().URL.Path
|
||||
if c.Request().Method == http.MethodGet {
|
||||
if shortCache[path] {
|
||||
c.Response().Header().Set("Cache-Control", "max-age=60")
|
||||
} else if path == "/api/dashboard" {
|
||||
c.Response().Header().Set("Cache-Control", "max-age=30")
|
||||
} else if path == "/api/calendar" || path == "/api/activity" {
|
||||
c.Response().Header().Set("Cache-Control", "max-age=15")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user