Files
MusicServer/internal/server/statistics_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

72 lines
3.0 KiB
Go

package server
import (
"context"
"net/http"
"music-server/internal/backend"
"github.com/danielgtaylor/huma/v2"
)
const maxStatsLimit = 100
type limitInput struct {
Limit int `query:"limit" default:"10" minimum:"1" doc:"Number of results. Values above 100 are capped to 100."`
}
func (in *limitInput) capped() int32 {
return int32(min(in.Limit, maxStatsLimit))
}
func (s *Server) registerStatistics(a apis) {
st := s.stats
op := func(id, path, summary, description string) route {
return route{op: huma.Operation{
OperationID: id,
Method: http.MethodGet,
Path: "/statistics" + path,
Summary: summary,
Description: description,
Tags: []string{"statistics"},
}}
}
soundtracks := func(id, path, summary, description string,
query func(context.Context, int32) ([]backend.SoundtrackWithSongs, error)) {
register(s, a, op(id, path, summary, description),
func(ctx context.Context, in *limitInput) (*jsonBody[[]backend.SoundtrackWithSongs], error) {
return ok(query(ctx, in.capped()))
})
}
songs := func(id, path, summary, description string,
query func(context.Context, int32) ([]backend.SongInfoForStats, error)) {
register(s, a, op(id, path, summary, description),
func(ctx context.Context, in *limitInput) (*jsonBody[[]backend.SongInfoForStats], error) {
return ok(query(ctx, in.capped()))
})
}
soundtracks("get-most-played-soundtracks", "/soundtracks/most-played", "Most played soundtracks",
"Returns the most played soundtracks with their songs.", st.MostPlayedSoundtracks)
soundtracks("get-least-played-soundtracks", "/soundtracks/least-played", "Least played soundtracks",
"Returns the least played soundtracks with their songs.", st.LeastPlayedSoundtracks)
soundtracks("get-last-played-soundtracks", "/soundtracks/last-played", "Recently played soundtracks",
"Returns the most recently played soundtracks with their songs.", st.LastPlayedSoundtracks)
soundtracks("get-oldest-played-soundtracks", "/soundtracks/oldest-played", "Least recently played soundtracks",
"Returns the soundtracks played longest ago (played at least once) with their songs.", st.OldestPlayedSoundtracks)
songs("get-most-played-songs", "/songs/most-played", "Most played songs",
"Returns the most played songs with their soundtrack.", st.MostPlayedSongs)
songs("get-least-played-songs", "/songs/least-played", "Least played songs",
"Returns the least played songs with their soundtrack.", st.LeastPlayedSongs)
register(s, a, op("get-never-played-soundtracks", "/soundtracks/never-played", "Never played soundtracks",
"Returns all soundtracks that have never been played, with their songs."),
func(ctx context.Context, _ *struct{}) (*jsonBody[[]backend.SoundtrackWithSongs], error) {
return ok(st.NeverPlayedSoundtracks(ctx))
})
register(s, a, op("get-statistics-summary", "/summary", "Statistics summary", "Returns overall play statistics."),
func(ctx context.Context, _ *struct{}) (*jsonBody[backend.StatisticsSummary], error) {
return ok(st.Summary(ctx))
})
}