package server import ( "context" "fmt" "net/http" "music-server/internal/backend" "github.com/danielgtaylor/huma/v2" "github.com/google/uuid" ) type uuidInput struct { UUID string `path:"uuid" format:"uuid" doc:"UUID of the soundtrack or song"` } func (in *uuidInput) id() uuid.UUID { // The format is validated by Huma before the handler runs. return uuid.MustParse(in.UUID) } // AlternativeNames is the body of PUT /soundtracks/{uuid}/alternative-names. type AlternativeNames struct { Names []string `json:"names" maxItems:"50" doc:"The new list of alternative names. Replaces the old list; send [] to remove them all."` } type alternativeNamesInput struct { UUID string `path:"uuid" format:"uuid" doc:"UUID of the soundtrack"` Body AlternativeNames } func (in *alternativeNamesInput) id() uuid.UUID { return uuid.MustParse(in.UUID) } func (s *Server) registerLibrary(a huma.API) { l := s.library op := func(id, path, summary, description string, errs ...int) huma.Operation { return huma.Operation{ OperationID: id, Method: http.MethodGet, Path: path, Summary: summary, Description: description, Tags: []string{"library"}, Errors: errs, } } register(s, a, route{op: op("list-soundtracks", "/soundtracks", "List soundtracks", "Returns all soundtracks, sorted by name.")}, func(ctx context.Context, _ *struct{}) (*jsonBody[[]backend.Soundtrack], error) { return ok(l.Soundtracks(ctx)) }) register(s, a, route{op: op("get-soundtrack", "/soundtracks/{uuid}", "Get soundtrack", "Returns a soundtrack with its songs, sorted by name.", http.StatusNotFound)}, func(ctx context.Context, in *uuidInput) (*jsonBody[backend.SoundtrackWithSongList], error) { return ok(l.Soundtrack(ctx, in.id())) }) register(s, a, route{op: op("get-song", "/songs/{uuid}", "Get song", "Returns information about a song.", http.StatusNotFound)}, func(ctx context.Context, in *uuidInput) (*jsonBody[backend.Song], error) { return ok(l.Song(ctx, in.id())) }) names := op("set-alternative-names", "/soundtracks/{uuid}/alternative-names", "Set alternative names", "Replaces the soundtrack's alternative names, which search and the cover search also use. "+ "Names are trimmed, and empty names, duplicates and the soundtrack's own name (ignoring case) are dropped.", http.StatusNotFound) names.Method = http.MethodPut register(s, a, route{op: names}, func(ctx context.Context, in *alternativeNamesInput) (*jsonBody[backend.Soundtrack], error) { for _, name := range in.Body.Names { if len(name) > backend.MaxNameLength { return nil, huma.Error422UnprocessableEntity( fmt.Sprintf("names can be at most %d bytes long", backend.MaxNameLength)) } } soundtrack, err := l.SetAlternativeNames(ctx, in.id(), in.Body.Names) if err != nil { return nil, apiError(err) } s.soundtracksChanged(ctx) return ok(soundtrack, nil) }) file := op("get-song-file", "/songs/{uuid}/file", "Stream song", "Returns the audio file of a song.", http.StatusNotFound) file.Responses = fileResponses("The song (MP3)", audioMPEG) register(s, a, route{op: file, locked: true}, func(ctx context.Context, in *uuidInput) (*huma.StreamResponse, error) { return streamSong(l.SongPath(ctx, in.id())) }) }