Files
MusicServer/internal/server/misc_handler.go
T
Sansan 973ebf224b
CI / integration (push) Successful in 49s
CI / docker (push) Successful in 1m0s
CI / test (push) Successful in 1m6s
CI / publish (push) Skipped
Generate the OpenAPI spec with Huma instead of swag
The spec is now built from the same calls that register the routes, so it
can't drift from the router like the swag comments did (all /api/v1
routes were missing).

- /api/v1 endpoints are typed Huma operations; the spec is OpenAPI 3.1 at
  /openapi.json (3.0 at /openapi-3.0.json, YAML too), docs at /docs.
  /openapi and /swagger/* redirect there
- Bearer token security is declared per operation; token creation and
  the new GET /api/v1/health are public
- Legacy routes use the same handlers, are hidden from the spec and keep
  the Deprecation header
- /sync/reset is hidden (debug only); the /sync/new, /sync/quick and
  /sync/new/full aliases are removed (use /sync and /sync/full)
- Invalid parameters return 422 (validation); every error is
  {"error": "..."}; empty lists are [] instead of null
- /sync/progress is documented as progress or result (oneOf)
- Tests validate the spec and check real responses of every documented
  operation against it (kin-openapi)
- Removed swag annotations, cmd/docs, echo-swagger and the swag tool
2026-10-06 16:31:06 +02:00

100 lines
3.9 KiB
Go

package server
import (
"context"
"net/http"
"music-server/internal/backend"
"github.com/danielgtaylor/huma/v2"
)
type characterInput struct {
Name string `query:"name" required:"true" doc:"File name of the character image, from the character list"`
}
type redirectOutput struct {
Status int
Location string `header:"Location" doc:"Download URL of the asset"`
}
type healthOutput struct {
Body map[string]string
}
func (s *Server) registerMisc(a apis) {
get := func(id, path, tag, summary, description string, errs ...int) huma.Operation {
return huma.Operation{
OperationID: id,
Method: http.MethodGet,
Path: path,
Summary: summary,
Description: description,
Tags: []string{tag},
Errors: errs,
}
}
register(s, a, route{op: get("get-health", "/health", "server", "Health check",
"Returns {\"status\": \"up\"} when the server and database work, otherwise status \"down\" and an error."),
public: true, legacyPath: "/health"},
func(ctx context.Context, _ *struct{}) (*healthOutput, error) {
return &healthOutput{Body: s.db.Health(ctx)}, nil
})
register(s, a, route{op: get("get-version", "/version", "server", "Latest server version",
"Returns the server version and its changelog."), legacyPath: "/version"},
func(ctx context.Context, _ *struct{}) (*jsonBody[backend.VersionData], error) {
return ok(backend.GetLatestVersion(), nil)
})
register(s, a, route{op: get("get-version-history", "/version/history", "server", "Version history",
"Returns all server versions with their changelogs, newest first."), legacyPath: "/version/history"},
func(ctx context.Context, _ *struct{}) (*jsonBody[[]backend.VersionData], error) {
return ok(backend.GetVersionHistory(), nil)
})
register(s, a, route{op: get("list-characters", "/character/list", "characters", "List characters",
"Returns the file names of all character images."), legacyPath: "/characters"},
func(ctx context.Context, _ *struct{}) (*jsonBody[[]string], error) {
return ok(s.characters.List())
})
image := get("get-character", "/character", "characters", "Character image",
"Returns a character image.", http.StatusBadRequest, http.StatusNotFound)
image.Responses = fileResponses("The image", "image/png", "image/jpeg")
register(s, a, route{op: image, legacyPath: "/character"},
func(ctx context.Context, in *characterInput) (*huma.StreamResponse, error) {
f, err := s.characters.Open(in.Name)
if err != nil {
return nil, apiError(err)
}
return streamOpenFile(f, "", false)
})
r := s.releases
register(s, a, route{op: get("get-latest-client-version", "/download", "download", "Latest client version",
"Returns the name of the latest MusicPlayer release.", http.StatusBadGateway), legacyPath: "/download"},
func(ctx context.Context, _ *struct{}) (*jsonBody[string], error) {
return ok(r.LatestVersion(ctx))
})
register(s, a, route{op: get("list-client-assets", "/download/list", "download", "Latest client assets",
"Returns the file names of the latest MusicPlayer release.", http.StatusBadGateway), legacyPath: "/download/list"},
func(ctx context.Context, _ *struct{}) (*jsonBody[[]string], error) {
return ok(r.AssetNames(ctx))
})
download := func(id, path, platform, suffix string) {
op := get(id, path, "download", "Download the latest client for "+platform,
"Redirects to the download URL of the latest MusicPlayer release for "+platform+".",
http.StatusNotFound, http.StatusBadGateway)
op.DefaultStatus = http.StatusFound
register(s, a, route{op: op, legacyPath: path},
func(ctx context.Context, _ *struct{}) (*redirectOutput, error) {
url, err := r.AssetURL(ctx, suffix)
if err != nil {
return nil, apiError(err)
}
return &redirectOutput{Status: http.StatusFound, Location: url}, nil
})
}
download("download-client-windows", "/download/windows", "Windows", ".exe")
download("download-client-linux", "/download/linux", "Linux", ".x86_64")
}