diff --git a/backend/playbackcommands.go b/backend/playbackcommands.go index 152a861..d0396da 100644 --- a/backend/playbackcommands.go +++ b/backend/playbackcommands.go @@ -3,24 +3,36 @@ package backend import ( "slices" "sync" + + "github.com/dweymouth/supersonic/backend/mediaprovider" ) -type PlaybackCommandType int +type playbackCommandType int const ( - CmdStop PlaybackCommandType = iota - CmdContinue - CmdPause - CmdPlayTrackAt - CmdSeekSeconds - CmdSeekFwdBackN - CmdVolume - CmdLoopMode + cmdStop playbackCommandType = iota + cmdContinue + cmdPause + cmdPlayTrackAt // arg: int + cmdSeekSeconds // arg: float64 + cmdSeekFwdBackN // arg: int + cmdVolume // arg: int + cmdLoopMode // arg: LoopMode + cmdStopAndClearPlayQueue + cmdUpdatePlayQueue // arg: []mediaprovider.MediaItem + cmdRemoveTracksFromQueue // arg: []int + // arg: []mediaprovider.MediaItem + // arg2: InsertMode + // arg3: bool (shuffle) + cmdLoadItems + cmdLoadRadioStation // arg: *mediaprovider.RadioStation, arg2: InsertQueueMode ) type PlaybackCommand struct { - Type PlaybackCommandType + Type playbackCommandType Arg any + Arg2 any + Arg3 any } type CommandQueue struct { @@ -42,36 +54,84 @@ func (c *CommandQueue) C() <-chan PlaybackCommand { } func (c *CommandQueue) Stop() { - c.filterCommandsAndAdd([]PlaybackCommandType{CmdContinue, CmdPause, CmdStop}, - PlaybackCommand{Type: CmdStop}) + c.filterCommandsAndAdd([]playbackCommandType{cmdContinue, cmdPause, cmdStop}, + PlaybackCommand{Type: cmdStop}) } func (c *CommandQueue) Continue() { - c.filterCommandsAndAdd([]PlaybackCommandType{CmdContinue, CmdPause, CmdStop}, - PlaybackCommand{Type: CmdContinue}) + c.filterCommandsAndAdd([]playbackCommandType{cmdContinue, cmdPause, cmdStop}, + PlaybackCommand{Type: cmdContinue}) } func (c *CommandQueue) Pause() { - c.filterCommandsAndAdd([]PlaybackCommandType{CmdContinue, CmdPause, CmdStop}, - PlaybackCommand{Type: CmdPause}) + c.filterCommandsAndAdd([]playbackCommandType{cmdContinue, cmdPause, cmdStop}, + PlaybackCommand{Type: cmdPause}) } -func (c *CommandQueue) Volume(vol int) { - c.filterCommandsAndAdd([]PlaybackCommandType{CmdVolume}, - PlaybackCommand{Type: CmdVolume, Arg: vol}) +func (c *CommandQueue) StopAndClearPlayQueue() { + c.filterCommandsAndAdd([]playbackCommandType{cmdContinue, cmdPause, cmdStop, cmdStopAndClearPlayQueue}, + PlaybackCommand{Type: cmdStopAndClearPlayQueue}) } -func (c *CommandQueue) LoopMode(mode LoopMode) { - c.filterCommandsAndAdd([]PlaybackCommandType{CmdLoopMode}, - PlaybackCommand{Type: CmdLoopMode, Arg: mode}) +func (c *CommandQueue) SetVolume(vol int) { + c.filterCommandsAndAdd([]playbackCommandType{cmdVolume}, + PlaybackCommand{Type: cmdVolume, Arg: vol}) +} + +func (c *CommandQueue) SetLoopMode(mode LoopMode) { + c.filterCommandsAndAdd([]playbackCommandType{cmdLoopMode}, + PlaybackCommand{Type: cmdLoopMode, Arg: mode}) } func (c *CommandQueue) SeekSeconds(s float64) { - c.filterCommandsAndAdd([]PlaybackCommandType{CmdVolume}, - PlaybackCommand{Type: CmdSeekSeconds, Arg: s}) + c.filterCommandsAndAdd([]playbackCommandType{cmdSeekSeconds}, + PlaybackCommand{Type: cmdSeekSeconds, Arg: s}) } -func (c *CommandQueue) filterCommandsAndAdd(excludeTypes []PlaybackCommandType, command PlaybackCommand) { +func (c *CommandQueue) SeekNext() { + c.seekBackOrFwd(1) +} + +func (c *CommandQueue) SeekBackOrPrevious() { + c.seekBackOrFwd(-1) +} + +func (c *CommandQueue) UpdatePlayQueue(items []mediaprovider.MediaItem) { + c.filterCommandsAndAdd([]playbackCommandType{cmdUpdatePlayQueue}, + PlaybackCommand{Type: cmdUpdatePlayQueue, Arg: items}) +} + +func (c *CommandQueue) RemoveItemsFromQueue(idxs []int) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.queue = append(c.queue, PlaybackCommand{ + Type: cmdRemoveTracksFromQueue, + Arg: idxs, + }) +} + +func (c *CommandQueue) LoadRadioStation(radio *mediaprovider.RadioStation, insertMode InsertQueueMode) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.queue = append(c.queue, PlaybackCommand{ + Type: cmdLoadRadioStation, + Arg: radio, + Arg2: insertMode, + }) +} + +func (c *CommandQueue) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.queue = append(c.queue, PlaybackCommand{ + Type: cmdLoadItems, + Arg: items, + Arg2: insertQueueMode, + Arg3: shuffle, + }) +} + +func (c *CommandQueue) filterCommandsAndAdd(excludeTypes []playbackCommandType, command PlaybackCommand) { c.mutex.Lock() defer c.mutex.Unlock() @@ -87,6 +147,26 @@ func (c *CommandQueue) filterCommandsAndAdd(excludeTypes []PlaybackCommandType, c.queue = append(c.queue, command) } +func (c *CommandQueue) seekBackOrFwd(direction int) { + c.mutex.Lock() + defer c.mutex.Unlock() + + j := 0 + n := 0 + for _, cmd := range c.queue { + if cmd.Type == cmdSeekFwdBackN { + n += cmd.Arg.(int) + } else { + c.queue[j] = cmd + j++ + } + } + c.queue = c.queue[:j] + c.queue = append(c.queue, PlaybackCommand{ + Type: cmdSeekFwdBackN, + Arg: n + direction}) +} + func (c *CommandQueue) chanWriter() { for { c.mutex.Lock() diff --git a/backend/playbackengine.go b/backend/playbackengine.go index a68f94d..0d69cc3 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -213,14 +213,14 @@ func (p *playbackEngine) Continue() error { // Load items into the play queue. // If replacing the current queue (!appendToQueue), playback will be stopped. func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error { - newItems := p.deepCopyMediaItemSlice(items) + newItems := deepCopyMediaItemSlice(items) return p.doLoaditems(newItems, insertQueueMode, shuffle) } // Load tracks into the play queue. // If replacing the current queue (!appendToQueue), playback will be stopped. func (p *playbackEngine) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) error { - newTracks := p.copyTrackSliceToMediaItemSlice(tracks) + newTracks := copyTrackSliceToMediaItemSlice(tracks) return p.doLoaditems(newTracks, insertQueueMode, shuffle) } @@ -288,7 +288,7 @@ func (p *playbackEngine) StopAndClearPlayQueue() { } func (p *playbackEngine) GetPlayQueue() []mediaprovider.MediaItem { - return p.deepCopyMediaItemSlice(p.playQueue) + return deepCopyMediaItemSlice(p.playQueue) } // Any time the user changes the favorite status of a track elsewhere in the app, @@ -315,7 +315,7 @@ func (p *playbackEngine) OnTrackRatingChanged(id string, rating int) { // Does not stop playback if the currently playing track is in the new queue, // but updates the now playing index to point to the first instance of the track in the new queue. func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error { - newQueue := p.deepCopyMediaItemSlice(items) + newQueue := deepCopyMediaItemSlice(items) newNowPlayingIdx := -1 if p.nowPlayingIdx >= 0 { nowPlayingID := p.playQueue[p.nowPlayingIdx].Metadata().ID @@ -591,7 +591,7 @@ func (p *playbackEngine) sendNowPlayingScrobble() { // creates a deep copy of the track info so that we can maintain our own state // (play count increases, favorite, and rating) without messing up other views' track models -func (p *playbackEngine) deepCopyMediaItemSlice(tracks []mediaprovider.MediaItem) []mediaprovider.MediaItem { +func deepCopyMediaItemSlice(tracks []mediaprovider.MediaItem) []mediaprovider.MediaItem { newTracks := make([]mediaprovider.MediaItem, len(tracks)) for i, tr := range tracks { newTracks[i] = tr.Copy() @@ -599,7 +599,7 @@ func (p *playbackEngine) deepCopyMediaItemSlice(tracks []mediaprovider.MediaItem return newTracks } -func (p *playbackEngine) copyTrackSliceToMediaItemSlice(tracks []*mediaprovider.Track) []mediaprovider.MediaItem { +func copyTrackSliceToMediaItemSlice(tracks []*mediaprovider.Track) []mediaprovider.MediaItem { newTracks := make([]mediaprovider.MediaItem, len(tracks)) for i, tr := range tracks { newTracks[i] = tr.Copy()