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

157 lines
6.3 KiB
Go

package server
import (
"context"
"net/http"
"music-server/internal/backend"
"music-server/internal/logging"
"github.com/danielgtaylor/huma/v2"
"go.uber.org/zap"
)
type songNumberInput struct {
Song int `query:"song" required:"true" doc:"Position of the song in the queue (SongNo)"`
}
const audioMPEG = "audio/mpeg"
func (s *Server) registerMusic(a apis) {
m := s.music
audio := func(id, path, summary, description string, errs ...int) route {
return route{
op: huma.Operation{
OperationID: id,
Method: http.MethodGet,
Path: path,
Summary: summary,
Description: description,
Tags: []string{"music"},
Responses: fileResponses("The song (MP3)", audioMPEG),
Errors: errs,
},
legacyPath: path,
locked: true,
}
}
status := func(id, method, path, summary, description string) route {
return route{
op: huma.Operation{
OperationID: id,
Method: method,
Path: path,
Summary: summary,
Description: description,
Tags: []string{"music"},
DefaultStatus: http.StatusOK,
},
legacyPath: path,
locked: true,
}
}
list := func(id, path, summary, description string, locked bool) route {
return route{
op: huma.Operation{
OperationID: id,
Method: http.MethodGet,
Path: path,
Summary: summary,
Description: description,
Tags: []string{"music"},
},
legacyPath: path,
locked: locked,
}
}
register(s, a, audio("get-queue-song", "/music", "Play a song from the queue",
"Jumps to the song at the given queue position (clamped to the queue) and returns it.", http.StatusNotFound),
func(ctx context.Context, in *songNumberInput) (*huma.StreamResponse, error) {
return streamSong(m.Song(in.Song))
})
register(s, a, audio("get-sound-test-song", "/music/soundTest", "Sound check song",
"Returns a random song for checking the volume.", http.StatusNotFound),
func(ctx context.Context, _ *struct{}) (*huma.StreamResponse, error) {
return streamSong(m.SoundCheckSong())
})
register(s, a, audio("get-random-song", "/music/rand", "Random song",
"Picks a random song from a random soundtrack. Add it to the queue with /music/addQue.", http.StatusNotFound),
func(ctx context.Context, _ *struct{}) (*huma.StreamResponse, error) {
return streamSong(m.RandomSong(ctx))
})
register(s, a, audio("get-random-song-low-chance", "/music/rand/low", "Random song, favouring rarely played",
"Picks a random song, favouring soundtracks that have been played less than average.", http.StatusNotFound),
func(ctx context.Context, _ *struct{}) (*huma.StreamResponse, error) {
return streamSong(m.RandomSongLowChance(ctx))
})
register(s, a, audio("get-random-song-classic", "/music/rand/classic", "Random song, every song equal",
"Picks a random song where every song has the same chance, regardless of soundtrack.", http.StatusNotFound),
func(ctx context.Context, _ *struct{}) (*huma.StreamResponse, error) {
return streamSong(m.RandomSongClassic(ctx))
})
register(s, a, audio("get-next-song", "/music/next", "Next song in the queue",
"Moves to the next song in the queue and returns it. Stays on the last song at the end.", http.StatusNotFound),
func(ctx context.Context, _ *struct{}) (*huma.StreamResponse, error) {
return streamSong(m.NextSong())
})
register(s, a, audio("get-previous-song", "/music/previous", "Previous song in the queue",
"Moves to the previous song in the queue and returns it. Stays on the first song at the start.", http.StatusNotFound),
func(ctx context.Context, _ *struct{}) (*huma.StreamResponse, error) {
return streamSong(m.PreviousSong())
})
register(s, a, list("get-song-info", "/music/info", "Current song", "Returns the current song in the queue.", false),
func(ctx context.Context, _ *struct{}) (*jsonBody[backend.SongInfo], error) {
return ok(m.SongInfo(ctx), nil)
})
register(s, a, list("get-played-songs", "/music/list", "Queue", "Returns all songs in the queue.", false),
func(ctx context.Context, _ *struct{}) (*jsonBody[[]backend.SongInfo], error) {
return ok(m.PlayedSongs(ctx), nil)
})
randomNames := func(ctx context.Context, _ *struct{}) (*jsonBody[[]string], error) {
return ok(m.SoundtrackNamesRandom(ctx))
}
register(s, a, list("get-soundtrack-names-random", "/music/all", "Soundtrack names, random order",
"Returns the names of all soundtracks in random order.", true), randomNames)
register(s, a, list("get-soundtrack-names-random-alias", "/music/all/random", "Soundtrack names, random order",
"Same as /music/all.", true), randomNames)
register(s, a, list("get-soundtrack-names", "/music/all/order", "Soundtrack names, sorted",
"Returns the names of all soundtracks in alphabetical order.", true),
func(ctx context.Context, _ *struct{}) (*jsonBody[[]string], error) {
return ok(m.SoundtrackNames(ctx))
})
register(s, a, status("reset-queue", http.MethodGet, "/music/reset", "Reset the queue",
"Clears the queue and reloads the soundtrack list."),
func(ctx context.Context, _ *struct{}) (*noBody, error) {
if err := m.Reset(ctx); err != nil {
return nil, apiError(err)
}
return &noBody{}, nil
})
register(s, a, status("add-latest-to-queue", http.MethodGet, "/music/addQue", "Add the last random song to the queue",
"Adds the song returned by the last random song request to the end of the queue."),
func(ctx context.Context, _ *struct{}) (*noBody, error) {
m.AddLatestToQueue()
return &noBody{}, nil
})
register(s, a, status("mark-current-played", http.MethodGet, "/music/addPlayed", "Mark the current song as played",
"Increments the play counters of the current song and its soundtrack."),
func(ctx context.Context, _ *struct{}) (*noBody, error) {
if err := m.AddLatestPlayed(ctx); err != nil {
return nil, apiError(err)
}
return &noBody{}, nil
})
register(s, a, status("mark-song-played", http.MethodPut, "/music/played", "Mark a queued song as played",
"Increments the play counters of the song at the given queue position. Positions outside the queue are ignored."),
func(ctx context.Context, in *songNumberInput) (*noBody, error) {
logging.GetLogger().Info("Marking song as played", zap.Int("song_no", in.Song))
if err := m.SetPlayed(ctx, in.Song); err != nil {
return nil, apiError(err)
}
return &noBody{}, nil
})
}