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
87 lines
2.8 KiB
Go
87 lines
2.8 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"reflect"
|
|
|
|
"music-server/internal/backend"
|
|
"music-server/internal/logging"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
)
|
|
|
|
func (s *Server) registerSync(a apis) {
|
|
registry := a.v1.OpenAPI().Components.Schemas
|
|
progress := huma.Operation{
|
|
OperationID: "get-sync-progress",
|
|
Method: http.MethodGet,
|
|
Path: "/sync/progress",
|
|
Summary: "Sync progress or result",
|
|
Description: "While a sync is running, returns its progress. Otherwise returns the result of the last sync.",
|
|
Tags: []string{"sync"},
|
|
Responses: map[string]*huma.Response{
|
|
"200": {
|
|
Description: "Progress of the running sync, or the result of the last sync",
|
|
Content: map[string]*huma.MediaType{"application/json": {Schema: &huma.Schema{OneOf: []*huma.Schema{
|
|
registry.Schema(reflect.TypeFor[backend.ProgressResponse](), true, ""),
|
|
registry.Schema(reflect.TypeFor[backend.SyncResponse](), true, ""),
|
|
}}}},
|
|
},
|
|
},
|
|
}
|
|
register(s, a, route{op: progress, legacyPath: "/sync/progress"},
|
|
func(ctx context.Context, _ *struct{}) (*jsonBody[any], error) {
|
|
if s.sync.IsSyncing() {
|
|
return ok[any](s.sync.Progress(), nil)
|
|
}
|
|
return ok[any](s.sync.Result(), nil)
|
|
})
|
|
|
|
start := func(id, path, summary, description string, full bool) {
|
|
register(s, a, route{
|
|
op: huma.Operation{
|
|
OperationID: id,
|
|
Method: http.MethodPost,
|
|
Path: path,
|
|
Summary: summary,
|
|
Description: description,
|
|
Tags: []string{"sync"},
|
|
Errors: []int{http.StatusLocked},
|
|
},
|
|
legacyPath: path,
|
|
}, func(ctx context.Context, _ *struct{}) (*jsonBody[string], error) {
|
|
message := "Start syncing soundtracks"
|
|
if full {
|
|
message += " full"
|
|
}
|
|
if !s.sync.Start(full) {
|
|
return nil, huma.Error423Locked("Syncing is in progress")
|
|
}
|
|
logging.GetLogger().Info(message)
|
|
return ok(message, nil)
|
|
})
|
|
}
|
|
start("start-sync", "/sync", "Sync changed soundtracks",
|
|
"Starts syncing the music folder in the background, skipping soundtracks that have not changed. "+
|
|
"Poll /sync/progress for the result.", false)
|
|
start("start-full-sync", "/sync/full", "Sync all soundtracks",
|
|
"Starts a full sync in the background that re-reads every soundtrack and removes songs whose files are gone. "+
|
|
"Poll /sync/progress for the result.", true)
|
|
|
|
reset := huma.Operation{
|
|
OperationID: "reset-library",
|
|
Method: http.MethodPost,
|
|
Path: "/sync/reset",
|
|
Summary: "Delete all soundtracks and songs",
|
|
Description: "For debugging only.",
|
|
Tags: []string{"sync"},
|
|
Hidden: true,
|
|
}
|
|
register(s, a, route{op: reset, legacyPath: "/sync/reset", locked: true},
|
|
func(ctx context.Context, _ *struct{}) (*jsonBody[string], error) {
|
|
logging.GetLogger().Info("Resetting soundtracks database")
|
|
return ok("Soundtracks and songs are deleted from the database", s.sync.ResetDB(ctx))
|
|
})
|
|
}
|