package server import ( "net/http" "time" _ "music-server/cmd/docs" // registers the OpenAPI spec "music-server/cmd/web" "music-server/internal/logging" "music-server/internal/server/middleware" "github.com/a-h/templ" "github.com/labstack/echo/v5" echoMiddleware "github.com/labstack/echo/v5/middleware" echoSwagger "github.com/swaggo/echo-swagger/v2" "github.com/swaggo/swag" "go.uber.org/zap" ) // @title MusicServer API // @version 1.0 // @description API for the MusicServer application // @termsOfService http://sanplex.xyz/terms/ // @contact.name Sebastian Olsson // @contact.email zarnor91@gmail.com // @license.name MIT // @license.url http://opensource.org/licenses/MIT // @BasePath / // handlers holds all HTTP handlers. type handlers struct { health *HealthHandler version *VersionHandler character *CharacterHandler download *DownloadHandler sync *SyncHandler music *MusicHandler library *LibraryHandler statistics *StatisticsHandler token *TokenHandler } func (s *Server) handlers() handlers { return handlers{ health: NewHealthHandler(s.db), version: NewVersionHandler(), character: NewCharacterHandler(s.characters), download: NewDownloadHandler(s.releases), sync: NewSyncHandler(s.sync), music: NewMusicHandler(s.music), library: NewLibraryHandler(s.library), statistics: NewStatisticsHandler(s.stats), token: NewTokenHandler(s.q, s.cfg.APIPassword, s.cfg.DisableSessionAuth), } } func (s *Server) RegisterRoutes() http.Handler { e := echo.New() e.HTTPErrorHandler = httpErrorHandler if len(s.cfg.TrustedProxies) > 0 { var trust []echo.TrustOption for _, ipNet := range s.cfg.TrustedProxies { trust = append(trust, echo.TrustIPRange(ipNet)) } e.IPExtractor = echo.ExtractIPFromXFFHeader(trust...) } e.Use(logging.RequestLogger()) e.Use(echoMiddleware.Recover()) e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{ AllowOrigins: []string{"https://*", "http://*"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, AllowCredentials: true, MaxAge: 300, })) e.GET("/openapi", serveOpenAPI) e.GET("/swagger/*", echoSwagger.WrapHandler) e.GET("/assets/*", echo.WrapHandler(http.FileServer(http.FS(web.Assets)))) e.GET("/search", echo.WrapHandler(templ.Handler(web.SearchForm()))) e.POST("/find", echo.WrapHandler(web.NewSearchHandler(s.search))) e.Static("/", s.cfg.FrontendPath) h := s.handlers() syncLock := middleware.SyncCheck(s.sync.IsSyncing) // Legacy endpoints (deprecated - use /api/v1/ instead) legacy := e.Group("", middleware.DeprecationMiddleware) registerCommon(legacy, h, syncLock) legacy.GET("/health", h.health.HealthCheck) legacy.GET("/characters", h.character.GetCharacterList) // API v1 apiV1 := e.Group("/api/v1") tokenGroup := apiV1.Group("/token") tokenGroup.POST("", h.token.CreateTokenHandler, middleware.RateLimit(10, time.Minute)) tokenGroup.DELETE("", h.token.DeleteTokenHandler) protected := apiV1.Group("", middleware.TokenAuth(s.q, s.cfg.DisableSessionAuth)) protected.POST("/token/cleanup", h.token.CleanupExpiredSessionsHandler) registerCommon(protected, h, syncLock) protected.GET("/character/list", h.character.GetCharacterList) protected.GET("/soundtracks", h.library.GetSoundtracks) protected.GET("/soundtracks/:uuid", h.library.GetSoundtrack) protected.GET("/songs/:uuid", h.library.GetSong) protected.GET("/songs/:uuid/file", h.library.GetSongFile, syncLock) stats := protected.Group("/statistics") stats.GET("/soundtracks/most-played", h.statistics.GetMostPlayedSoundtracks) stats.GET("/soundtracks/least-played", h.statistics.GetLeastPlayedSoundtracks) stats.GET("/soundtracks/never-played", h.statistics.GetNeverPlayedSoundtracks) stats.GET("/soundtracks/last-played", h.statistics.GetLastPlayedSoundtracks) stats.GET("/soundtracks/oldest-played", h.statistics.GetOldestPlayedSoundtracks) stats.GET("/songs/most-played", h.statistics.GetMostPlayedSongs) stats.GET("/songs/least-played", h.statistics.GetLeastPlayedSongs) stats.GET("/summary", h.statistics.GetStatisticsSummary) for _, r := range e.Router().Routes() { logging.GetLogger().Debug("Registered route", zap.String("method", r.Method), zap.String("path", r.Path)) } return e } // registerCommon registers the routes shared by the legacy and the v1 API. // syncLock is applied to routes that must not run during a sync. func registerCommon(g *echo.Group, h handlers, syncLock echo.MiddlewareFunc) { g.GET("/version", h.version.GetLatestVersion) g.GET("/version/history", h.version.GetVersionHistory) g.GET("/character", h.character.GetCharacter) g.GET("/download", h.download.CheckLatest) g.GET("/download/list", h.download.ListAssetsOfLatest) g.GET("/download/windows", h.download.DownloadLatestWindows) g.GET("/download/linux", h.download.DownloadLatestLinux) g.GET("/sync/progress", h.sync.SyncProgress) for _, path := range []string{"/sync", "/sync/new", "/sync/quick"} { g.POST(path, h.sync.SyncSoundtracksNewOnlyChanges, syncLock) } for _, path := range []string{"/sync/full", "/sync/new/full"} { g.POST(path, h.sync.SyncSoundtracksNewFull, syncLock) } g.POST("/sync/reset", h.sync.ResetDB, syncLock) music := g.Group("/music", syncLock) music.GET("", h.music.GetSong) music.GET("/soundTest", h.music.GetSoundCheckSong) music.GET("/reset", h.music.ResetMusic) music.GET("/rand", h.music.GetRandomSong) music.GET("/rand/low", h.music.GetRandomSongLowChance) music.GET("/rand/classic", h.music.GetRandomSongClassic) music.GET("/next", h.music.GetNextSong) music.GET("/previous", h.music.GetPreviousSong) music.GET("/all", h.music.GetAllSoundtracksRandom) music.GET("/all/order", h.music.GetAllSoundtracks) music.GET("/all/random", h.music.GetAllSoundtracksRandom) music.PUT("/played", h.music.PutPlayed) music.GET("/addQue", h.music.AddLatestToQue) music.GET("/addPlayed", h.music.AddLatestPlayed) // Info and list are read-only and stay available during a sync. g.GET("/music/info", h.music.GetSongInfo) g.GET("/music/list", h.music.GetPlayedSongs) } // serveOpenAPI serves the embedded OpenAPI spec. func serveOpenAPI(c *echo.Context) error { doc, err := swag.ReadDoc() if err != nil { return err } return c.Blob(http.StatusOK, echo.MIMEApplicationJSON, []byte(doc)) }