Store each client's play queue in the database
The queue lived in memory and was shared by all clients, so a restart or a finished sync cleared it. Every client now has its own queue in the new queue and queue_song tables (added to the undeployed migration 000006). The music endpoints return the queue ID in the X-Queue-Id header and expect it back. A request without an ID, or with an ended one, starts a new queue. /music/reset ends the queue and returns a new ID, and a sync ends all running queues. Ended queues are kept as history. The deprecated legacy routes without the /api/v1 prefix are removed.
This commit is contained in:
1 parent
b5dc42b654
commit
a705d7b476
28 files changed
+1103
-320
No files matched your search
+47
-35
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"music-server/internal/backend"
|
||||
"music-server/internal/logging"
|
||||
"music-server/internal/server/middleware"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humaecho"
|
||||
@@ -50,47 +51,42 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
// apis holds the documented /api/v1 API and the hidden, deprecated legacy routes.
|
||||
type apis struct {
|
||||
v1 huma.API
|
||||
legacy huma.API
|
||||
}
|
||||
|
||||
func newAPIs(e *echo.Echo) apis {
|
||||
func newAPI(e *echo.Echo) huma.API {
|
||||
config := huma.DefaultConfig("MusicServer API", backend.GetLatestVersion().Version)
|
||||
// No $schema fields or Link headers in responses.
|
||||
config.CreateHooks = nil
|
||||
config.DocsRenderer = huma.DocsRendererScalar
|
||||
config.Info.Description = "API for the MusicServer application.\n\n" +
|
||||
"Get a token with `POST /api/v1/token` and send it as `Authorization: Bearer <token>`.\n\n" +
|
||||
"The same endpoints without the `/api/v1` prefix (legacy routes) are deprecated, need no token " +
|
||||
"and are not documented here. They will be removed."
|
||||
"Every client has its own play queue. The music endpoints return its ID in the `X-Queue-Id` header; " +
|
||||
"send it back in the same header to keep using that queue. Without it, or when the queue has ended " +
|
||||
"(after a reset or a sync), a new queue is started."
|
||||
config.Components.SecuritySchemes = map[string]*huma.SecurityScheme{
|
||||
bearerAuthName: {Type: "http", Scheme: "bearer", Description: "Token from POST /api/v1/token"},
|
||||
}
|
||||
|
||||
legacyConfig := huma.DefaultConfig("MusicServer legacy API", backend.GetLatestVersion().Version)
|
||||
legacyConfig.CreateHooks = nil
|
||||
legacyConfig.OpenAPIPath = ""
|
||||
legacyConfig.DocsPath = ""
|
||||
legacyConfig.SchemasPath = ""
|
||||
|
||||
return apis{v1: humaecho.New(e, config), legacy: humaecho.New(e, legacyConfig)}
|
||||
return humaecho.New(e, config)
|
||||
}
|
||||
|
||||
// route describes an endpoint registered on /api/v1 and, optionally, as a legacy route.
|
||||
// route describes an endpoint registered on /api/v1.
|
||||
type route struct {
|
||||
op huma.Operation
|
||||
// public endpoints need no token.
|
||||
public bool
|
||||
// legacyPath is the deprecated path without /api/v1 (empty: no legacy route).
|
||||
legacyPath string
|
||||
// locked endpoints answer 423 while a sync is running.
|
||||
locked bool
|
||||
// queue endpoints work on the client's play queue, see middleware.Queue.
|
||||
queue bool
|
||||
}
|
||||
|
||||
// register adds the route's operation to the v1 API and, if it has one, its hidden legacy route.
|
||||
func register[I, O any](s *Server, a apis, r route, handler func(context.Context, *I) (*O, error)) {
|
||||
var queueHeaderParam = &huma.Param{
|
||||
Name: middleware.QueueHeader,
|
||||
In: "header",
|
||||
Description: "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
Schema: &huma.Schema{Type: huma.TypeString, Format: "uuid"},
|
||||
}
|
||||
|
||||
// register adds the route's operation to the API.
|
||||
func register[I, O any](s *Server, a huma.API, r route, handler func(context.Context, *I) (*O, error)) {
|
||||
op := r.op
|
||||
op.Path = apiPrefix + r.op.Path
|
||||
if !r.public {
|
||||
@@ -98,23 +94,39 @@ func register[I, O any](s *Server, a apis, r route, handler func(context.Context
|
||||
op.Errors = append(op.Errors, http.StatusUnauthorized)
|
||||
}
|
||||
if r.locked {
|
||||
op.Middlewares = append(op.Middlewares, s.syncLock(a.v1))
|
||||
op.Middlewares = append(op.Middlewares, s.syncLock(a))
|
||||
op.Errors = append(op.Errors, http.StatusLocked)
|
||||
}
|
||||
huma.Register(a.v1, op, handler)
|
||||
if r.queue {
|
||||
op.Middlewares = append(op.Middlewares, middleware.Queue(a, s.music.ResolveQueue))
|
||||
op.Parameters = append(op.Parameters, queueHeaderParam)
|
||||
}
|
||||
huma.Register(a, op, handler)
|
||||
if r.queue {
|
||||
documentQueueHeader(a, op)
|
||||
}
|
||||
}
|
||||
|
||||
if r.legacyPath == "" {
|
||||
return
|
||||
// documentQueueHeader adds the X-Queue-Id header to the operation's success responses.
|
||||
func documentQueueHeader(a huma.API, op huma.Operation) {
|
||||
path := a.OpenAPI().Paths[op.Path]
|
||||
registered := map[string]*huma.Operation{
|
||||
http.MethodGet: path.Get, http.MethodPut: path.Put, http.MethodPost: path.Post, http.MethodDelete: path.Delete,
|
||||
}[op.Method]
|
||||
for status, resp := range registered.Responses {
|
||||
if !strings.HasPrefix(status, "2") {
|
||||
continue
|
||||
}
|
||||
if resp.Headers == nil {
|
||||
resp.Headers = map[string]*huma.Param{}
|
||||
}
|
||||
if _, ok := resp.Headers[middleware.QueueHeader]; !ok {
|
||||
resp.Headers[middleware.QueueHeader] = &huma.Param{
|
||||
Description: "ID of the client's play queue. Send it with the following requests.",
|
||||
Schema: &huma.Schema{Type: huma.TypeString, Format: "uuid"},
|
||||
}
|
||||
}
|
||||
}
|
||||
legacy := r.op
|
||||
legacy.OperationID = "legacy-" + r.op.OperationID
|
||||
legacy.Path = r.legacyPath
|
||||
legacy.Hidden = true
|
||||
legacy.Deprecated = true
|
||||
if r.locked {
|
||||
legacy.Middlewares = append(legacy.Middlewares, s.syncLock(a.legacy))
|
||||
}
|
||||
huma.Register(a.legacy, legacy, handler)
|
||||
}
|
||||
|
||||
// syncLock answers 423 Locked while a sync is running.
|
||||
|
||||
Reference in new issue
Block a user