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 }) }