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
157 lines
6.3 KiB
Go
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
|
|
})
|
|
}
|