Echo errors, middleware and handlers used three different formats (message object, error object, bare string). One error handler now writes all of them the same way; status codes are unchanged.
180 lines
6.3 KiB
Go
180 lines
6.3 KiB
Go
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))
|
|
}
|