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
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"music-server/internal/backend"
|
||||
"music-server/internal/logging"
|
||||
"music-server/internal/server/middleware"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"go.uber.org/zap"
|
||||
@@ -15,9 +16,14 @@ type songNumberInput struct {
|
||||
Song int `query:"song" required:"true" doc:"Position of the song in the queue (SongNo)"`
|
||||
}
|
||||
|
||||
// resetOutput returns the ID of the new queue.
|
||||
type resetOutput struct {
|
||||
QueueID string `header:"X-Queue-Id" doc:"ID of the new queue"`
|
||||
}
|
||||
|
||||
const audioMPEG = "audio/mpeg"
|
||||
|
||||
func (s *Server) registerMusic(a apis) {
|
||||
func (s *Server) registerMusic(a huma.API) {
|
||||
m := s.music
|
||||
audio := func(id, path, summary, description string, errs ...int) route {
|
||||
return route{
|
||||
@@ -31,8 +37,7 @@ func (s *Server) registerMusic(a apis) {
|
||||
Responses: fileResponses("The song (MP3)", audioMPEG),
|
||||
Errors: errs,
|
||||
},
|
||||
legacyPath: path,
|
||||
locked: true,
|
||||
locked: true,
|
||||
}
|
||||
}
|
||||
status := func(id, method, path, summary, description string) route {
|
||||
@@ -46,8 +51,8 @@ func (s *Server) registerMusic(a apis) {
|
||||
Tags: []string{"music"},
|
||||
DefaultStatus: http.StatusOK,
|
||||
},
|
||||
legacyPath: path,
|
||||
locked: true,
|
||||
locked: true,
|
||||
queue: true,
|
||||
}
|
||||
}
|
||||
list := func(id, path, summary, description string, locked bool) route {
|
||||
@@ -60,54 +65,59 @@ func (s *Server) registerMusic(a apis) {
|
||||
Description: description,
|
||||
Tags: []string{"music"},
|
||||
},
|
||||
legacyPath: path,
|
||||
locked: locked,
|
||||
locked: locked,
|
||||
}
|
||||
}
|
||||
// queued marks a route that works on the client's play queue.
|
||||
queued := func(r route) route {
|
||||
r.queue = true
|
||||
return r
|
||||
}
|
||||
queue := middleware.QueueID
|
||||
|
||||
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),
|
||||
register(s, a, queued(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))
|
||||
return streamSong(m.Song(ctx, queue(ctx), 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),
|
||||
register(s, a, queued(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))
|
||||
return streamSong(m.RandomSong(ctx, queue(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),
|
||||
register(s, a, queued(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))
|
||||
return streamSong(m.RandomSongLowChance(ctx, queue(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),
|
||||
register(s, a, queued(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))
|
||||
return streamSong(m.RandomSongClassic(ctx, queue(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),
|
||||
register(s, a, queued(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())
|
||||
return streamSong(m.NextSong(ctx, queue(ctx)))
|
||||
})
|
||||
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),
|
||||
register(s, a, queued(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())
|
||||
return streamSong(m.PreviousSong(ctx, queue(ctx)))
|
||||
})
|
||||
|
||||
register(s, a, list("get-song-info", "/music/info", "Current song", "Returns the current song in the queue.", false),
|
||||
register(s, a, queued(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)
|
||||
return ok(m.SongInfo(ctx, queue(ctx)))
|
||||
})
|
||||
register(s, a, list("get-played-songs", "/music/list", "Queue", "Returns all songs in the queue.", false),
|
||||
register(s, a, queued(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)
|
||||
return ok(m.PlayedSongs(ctx, queue(ctx)))
|
||||
})
|
||||
randomNames := func(ctx context.Context, _ *struct{}) (*jsonBody[[]string], error) {
|
||||
return ok(m.SoundtrackNamesRandom(ctx))
|
||||
@@ -123,23 +133,30 @@ func (s *Server) registerMusic(a apis) {
|
||||
})
|
||||
|
||||
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 {
|
||||
"Ends the queue, starts a new one and reloads the soundtrack list. "+
|
||||
"The new queue's ID is returned in the X-Queue-Id header; send it with the following requests."),
|
||||
func(ctx context.Context, _ *struct{}) (*resetOutput, error) {
|
||||
id, err := m.NewQueue(ctx, queue(ctx))
|
||||
if err != nil {
|
||||
return nil, apiError(err)
|
||||
}
|
||||
return &noBody{}, nil
|
||||
if err := m.ReloadSoundtracks(ctx); err != nil {
|
||||
return nil, apiError(err)
|
||||
}
|
||||
return &resetOutput{QueueID: id.String()}, 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()
|
||||
if err := m.AddLatestToQueue(ctx, queue(ctx)); err != nil {
|
||||
return nil, apiError(err)
|
||||
}
|
||||
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 {
|
||||
if err := m.AddLatestPlayed(ctx, queue(ctx)); err != nil {
|
||||
return nil, apiError(err)
|
||||
}
|
||||
return &noBody{}, nil
|
||||
@@ -148,7 +165,7 @@ func (s *Server) registerMusic(a apis) {
|
||||
"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 {
|
||||
if err := m.SetPlayed(ctx, queue(ctx), in.Song); err != nil {
|
||||
return nil, apiError(err)
|
||||
}
|
||||
return &noBody{}, nil
|
||||
|
||||
Reference in new issue
Block a user