package server import ( "context" "net/http" "music-server/internal/backend" "github.com/danielgtaylor/huma/v2" "github.com/google/uuid" ) type coverUploadInput struct { UUID string `path:"uuid" format:"uuid" doc:"UUID of the soundtrack"` RawBody []byte `contentType:"image/*" doc:"The image: JPEG, PNG or WebP, at most 10 MB"` } // CoverSource is the body of PUT /soundtracks/{uuid}/cover/source. type CoverSource struct { Source string `json:"source" enum:"steamgriddb,anilist" doc:"Where the cover comes from"` ID string `json:"id" minLength:"1" maxLength:"50" doc:"The game's ID on SteamGridDB, or the anime's ID on AniList"` } type coverSourceInput struct { UUID string `path:"uuid" format:"uuid" doc:"UUID of the soundtrack"` Body CoverSource } func (s *Server) registerCovers(a huma.API) { op := func(id, method, path, summary, description string, errs ...int) huma.Operation { return huma.Operation{ OperationID: id, Method: method, Path: path, Summary: summary, Description: description, Tags: []string{"covers"}, Errors: errs, } } // covers returns the cover service, or ErrCoversDisabled when COVERS_PATH is not set. covers := func() (*backend.CoverService, error) { if s.covers == nil { return nil, apiError(backend.ErrCoversDisabled) } return s.covers, nil } get := op("get-soundtrack-cover", http.MethodGet, "/soundtracks/{uuid}/cover", "Soundtrack cover", "Returns the soundtrack's cover image. cover_modified in the soundtrack responses tells when it last changed.", http.StatusNotFound) get.Responses = fileResponses("The cover image", "image/jpeg", "image/png", "image/webp") register(s, a, route{op: get}, func(ctx context.Context, in *uuidInput) (*huma.StreamResponse, error) { if s.covers == nil { return nil, huma.Error404NotFound("Not Found") } path, err := s.covers.Path(ctx, in.id()) if err != nil { return nil, apiError(err) } return streamFile(path, "", false) }) upload := op("upload-soundtrack-cover", http.MethodPut, "/soundtracks/{uuid}/cover", "Upload a cover", "Stores the image as the soundtrack's cover. The automatic search never replaces it; delete it to go back to automatic.", http.StatusBadRequest, http.StatusNotFound, http.StatusServiceUnavailable) upload.MaxBodyBytes = backend.MaxCoverBytes upload.DefaultStatus = http.StatusNoContent register(s, a, route{op: upload}, func(ctx context.Context, in *coverUploadInput) (*struct{}, error) { c, err := covers() if err != nil { return nil, err } if err := c.Upload(ctx, uuid.MustParse(in.UUID), in.RawBody); err != nil { return nil, apiError(err) } return nil, nil }) source := op("set-soundtrack-cover-source", http.MethodPut, "/soundtracks/{uuid}/cover/source", "Pick a cover", "Downloads the cover of the given SteamGridDB game or AniList anime and stores it as the soundtrack's cover. "+ "The automatic search never replaces it. SteamGridDB needs STEAMGRIDDB_API_KEY on the server.", http.StatusBadRequest, http.StatusNotFound, http.StatusBadGateway, http.StatusServiceUnavailable) source.DefaultStatus = http.StatusNoContent register(s, a, route{op: source}, func(ctx context.Context, in *coverSourceInput) (*struct{}, error) { c, err := covers() if err != nil { return nil, err } if err := c.FromSource(ctx, uuid.MustParse(in.UUID), in.Body.Source, in.Body.ID); err != nil { return nil, apiError(err) } return nil, nil }) remove := op("delete-soundtrack-cover", http.MethodDelete, "/soundtracks/{uuid}/cover", "Remove the cover", "Deletes the soundtrack's cover, including one set by hand, so the automatic search looks for a new one.", http.StatusNotFound, http.StatusServiceUnavailable) remove.DefaultStatus = http.StatusNoContent register(s, a, route{op: remove}, func(ctx context.Context, in *uuidInput) (*struct{}, error) { c, err := covers() if err != nil { return nil, err } if err := c.Remove(ctx, in.id()); err != nil { return nil, apiError(err) } return nil, nil }) }