diff --git a/backend/app.go b/backend/app.go index a7936b3..8837381 100644 --- a/backend/app.go +++ b/backend/app.go @@ -622,11 +622,11 @@ func (a *App) SavePlayQueueIfEnabled() { func (a *App) LoadSavedPlayQueue() error { queueFilePath := path.Join(a.configDir, savedQueueFile) playQueue, err := LoadPlayQueue(queueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer) + var unshuffledPlayQueue *SavedPlayQueue var shuffledPlayQueue *SavedPlayQueue isShuffe := a.Config.Playback.Shuffle - if isShuffe { unshuffledQueueFilePath := path.Join(a.configDir, savedUnshuffledQueueFile) unshuffledPlayQueue, err = LoadPlayQueue(unshuffledQueueFilePath, a.ServerManager, false) @@ -635,7 +635,7 @@ func (a *App) LoadSavedPlayQueue() error { return err } - shuffledQueueFilePath := path.Join(a.configDir, savedUnshuffledQueueFile) + shuffledQueueFilePath := path.Join(a.configDir, savedShuffledQueueFile) shuffledPlayQueue, err = LoadPlayQueue(shuffledQueueFilePath, a.ServerManager, false) if err != nil { @@ -653,7 +653,25 @@ func (a *App) LoadSavedPlayQueue() error { return nil } - a.PlaybackManager.LoadTracks(playQueue.Tracks, Replace, false) + if isShuffe { + serverStatePlayQueue := sharedutil.CopyTrackSliceToMediaItemSlice(playQueue.Tracks) + clientStatePlayQueue := sharedutil.CopyTrackSliceToMediaItemSlice(shuffledPlayQueue.Tracks) + + // Compare items by ID. This fails if any 2 elements don't match up. Two queues with the same items but different order will thus not count as same + if slices.EqualFunc(serverStatePlayQueue, clientStatePlayQueue, func(a, b mediaprovider.MediaItem) bool { + return (a.Metadata().ID == b.Metadata().ID) + }) { + a.PlaybackManager.SetQueueState(playQueue.Tracks, ShuffledPlayQueue) + a.PlaybackManager.SetQueueState(unshuffledPlayQueue.Tracks, PlayQueue) + } else { + a.PlaybackManager.SetShuffle(false) + a.PlaybackManager.SetQueueState(playQueue.Tracks, PlayQueue) + } + + } else { + a.PlaybackManager.SetQueueState(playQueue.Tracks, PlayQueue) + } + if playQueue.TrackIndex >= 0 && playQueue.TrackIndex < len(playQueue.Tracks) { // TODO: This isn't ideal but doesn't seem to cause an audible play-for-a-split-second artifact a.PlaybackManager.PlayTrackAt(playQueue.TrackIndex) @@ -662,23 +680,6 @@ func (a *App) LoadSavedPlayQueue() error { a.PlaybackManager.SeekSeconds(playQueue.TimePos) } - if isShuffe { - // check if queue was changed server side - if len(playQueue.Tracks) != len(unshuffledPlayQueue.Tracks) { - // queue was changed server side, no need to compare elements - a.PlaybackManager.SetShuffle(false) - } else { - serverStatePlayQueue := sharedutil.CopyTrackSliceToMediaItemSlice(playQueue.Tracks) - clientStatePlayQueue := sharedutil.CopyTrackSliceToMediaItemSlice(shuffledPlayQueue.Tracks) - - if slices.Equal(serverStatePlayQueue, clientStatePlayQueue) { - fmt.Println("Loading unshuffled items into playQueue") - a.PlaybackManager.LoadTracks(unshuffledPlayQueue.Tracks, Replace, false) - } else { - a.PlaybackManager.SetShuffle(false) - } - } - } return nil } diff --git a/backend/playbackcommands.go b/backend/playbackcommands.go index 2632fef..b6cdd58 100644 --- a/backend/playbackcommands.go +++ b/backend/playbackcommands.go @@ -25,6 +25,7 @@ const ( // arg2: InsertMode // arg3: bool (shuffle) cmdLoadItems + cmdSetQueueState cmdLoadRadioStation // arg: *mediaprovider.RadioStation, arg2: InsertQueueMode cmdForceRestartPlayback @@ -160,6 +161,17 @@ func (c *playbackCommandQueue) LoadItems(items []mediaprovider.MediaItem, insert c.cmdAvailable.Signal() } +func (c *playbackCommandQueue) SetQueueState(tracks []*mediaprovider.Track, queueType QueueType) { + c.mutex.Lock() + c.queue = append(c.queue, playbackCommand{ + Type: cmdSetQueueState, + Arg: tracks, + Arg2: queueType, + }) + c.mutex.Unlock() + c.cmdAvailable.Signal() +} + func (c *playbackCommandQueue) addCommand(command playbackCommand) { c.mutex.Lock() c.queue = append(c.queue, command) @@ -194,7 +206,7 @@ func (c *playbackCommandQueue) seekBackOrFwd(direction int) { switch cmd.Type { case cmdSeekFwdBackN: lastIdx = i - case cmdRemoveTracksFromQueue, cmdLoadItems, cmdPlayTrackAt, + case cmdRemoveTracksFromQueue, cmdLoadItems, cmdSetQueueState, cmdPlayTrackAt, cmdLoadRadioStation, cmdUpdatePlayQueue, cmdStopAndClearPlayQueue: // any queue-modifying command means we can't coalesce any // more seekFwdBackN commands before here diff --git a/backend/playbackengine.go b/backend/playbackengine.go index e38ef20..b04fe39 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -110,6 +110,7 @@ type playbackEngine struct { onSongChange []func(nowPlaying mediaprovider.MediaItem, justScrobbledIfAny *mediaprovider.Track) onPlayTimeUpdate []func(float64, float64, bool) onLoopModeChange []func(LoopMode) + onShuffleChange []func(bool) onVolumeChange []func(int) onSeek []func() onPaused []func() @@ -149,6 +150,8 @@ func NewPlaybackEngine( pm.loopMode = LoopOne } + pm.shuffle = playbackCfg.Shuffle + pm.registerPlayerCallbacks(p) s.OnLogout(func() { pm.StopAndClearPlayQueue() @@ -231,12 +234,13 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) error { // ======================= START PLAY QUEUE FUNCS =========================== func (p *playbackEngine) getPlayQueue() []mediaprovider.MediaItem { - if p.shuffle { - return p.shuffledPlayQueue - } return p.playQueue } +func (p *playbackEngine) getShuffledPlayQueue() []mediaprovider.MediaItem { + return p.shuffledPlayQueue +} + func (p *playbackEngine) getActivePlayQueue() []mediaprovider.MediaItem { if p.shuffle { return p.shuffledPlayQueue @@ -245,10 +249,12 @@ func (p *playbackEngine) getActivePlayQueue() []mediaprovider.MediaItem { } func (p *playbackEngine) getPlayQueueLength() int { - return len(p.getPlayQueue()) + return len(p.getActivePlayQueue()) } func (p *playbackEngine) clearPlayQueue() { + p.player.Stop(false) + p.nowPlayingIdx = -1 p.playQueue = nil p.shuffledPlayQueue = nil } @@ -262,7 +268,7 @@ func (p *playbackEngine) setShuffledPlayQueue(items []mediaprovider.MediaItem) { } func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { - return p.getPlayQueue()[idx] + return p.getActivePlayQueue()[idx] } func (p *playbackEngine) insertItemsIntoPlayQueueAt(items []mediaprovider.MediaItem, idx int, queueType QueueType) { @@ -282,7 +288,7 @@ func (p *playbackEngine) GetPlayQueueDeepCopy() []mediaprovider.MediaItem { } func (p *playbackEngine) GetShuffledPlayQueueDeepCopy() []mediaprovider.MediaItem { - return deepCopyMediaItemSlice(p.getPlayQueue()) + return deepCopyMediaItemSlice(p.getShuffledPlayQueue()) } func (p *playbackEngine) GetActivePlayQueueDeepCopy() []mediaprovider.MediaItem { @@ -351,23 +357,26 @@ func (p *playbackEngine) GetNowPlayingIdxFrom(items []mediaprovider.MediaItem) i } func (p *playbackEngine) SetShuffle(shuffle bool) { + for _, cb := range p.onShuffleChange { + cb(shuffle) + } + if p.shuffle == shuffle { return } - - var newNowPlayingIdx int + newNowPlayingIdx := 0 if shuffle { shuffledQueue := deepCopyMediaItemSlice(p.playQueue) rand.Shuffle(len(shuffledQueue), func(i, j int) { shuffledQueue[i], shuffledQueue[j] = shuffledQueue[j], shuffledQueue[i] }) - p.shuffledPlayQueue = sharedutil.ReorderItems(shuffledQueue, []int{p.GetNowPlayingIdxFrom(shuffledQueue)}, 0) - newNowPlayingIdx = 0 + p.setShuffledPlayQueue(sharedutil.ReorderItems(shuffledQueue, []int{p.GetNowPlayingIdxFrom(shuffledQueue)}, 0)) } else { newNowPlayingIdx = p.GetNowPlayingIdxFrom(p.playQueue) } p.shuffle = shuffle + if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { return } @@ -471,6 +480,23 @@ func (p *playbackEngine) Continue() error { return p.player.Continue() } +// Load items into the specified queue(s). This overrides the queue. For standard inserts or replaces use p.LoadItems +// This is used on program startup to populate both the playQueue and shuffledPlayQueue with the previously saved client state. +func (p *playbackEngine) SetQueueState(tracks []*mediaprovider.Track, queueType QueueType) error { + newTracks := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) + switch queueType { + case PlayQueue: + p.setPlayQueue(newTracks) + case ShuffledPlayQueue: + p.setShuffledPlayQueue(newTracks) + case Both: + p.setPlayQueue(newTracks) + p.setShuffledPlayQueue(newTracks) + } + p.invokeNoArgCallbacks(p.onQueueChange) + return nil +} + // 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 { @@ -489,8 +515,6 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu queueType := PlayQueue if insertQueueMode == Replace { - p.player.Stop(false) - p.nowPlayingIdx = -1 p.clearPlayQueue() } if nextChanged := len(items) > 0 && (insertQueueMode != Append || (p.nowPlayingIdx == p.getPlayQueueLength()-1)); nextChanged { @@ -528,8 +552,6 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, insertMode InsertQueueMode) { if insertMode == Replace { - p.player.Stop(false) - p.nowPlayingIdx = -1 p.clearPlayQueue() } if nextChanged := insertMode == InsertNext || (insertMode == Append && p.nowPlayingIdx == p.getPlayQueueLength()-1); nextChanged { @@ -546,9 +568,7 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins // Stop playback and clear the play queue. func (p *playbackEngine) StopAndClearPlayQueue() { changed := p.getPlayQueueLength() > 0 - p.player.Stop(false) p.clearPlayQueue() - p.nowPlayingIdx = -1 if changed { p.invokeNoArgCallbacks(p.onQueueChange) } @@ -557,7 +577,7 @@ func (p *playbackEngine) StopAndClearPlayQueue() { // Any time the user changes the favorite status of a track elsewhere in the app, // this should be called to ensure the in-memory track model is updated. func (p *playbackEngine) OnTrackFavoriteStatusChanged(id string, fav bool) { - if item := sharedutil.FindMediaItemByID(id, p.getPlayQueue()); item != nil { + if item := sharedutil.FindMediaItemByID(id, p.getActivePlayQueue()); item != nil { if tr, ok := item.(*mediaprovider.Track); ok { tr.Favorite = fav } @@ -567,7 +587,7 @@ func (p *playbackEngine) OnTrackFavoriteStatusChanged(id string, fav bool) { // Any time the user changes the rating of a track elsewhere in the app, // this should be called to ensure the in-memory track model is updated. func (p *playbackEngine) OnTrackRatingChanged(id string, rating int) { - if item := sharedutil.FindMediaItemByID(id, p.getPlayQueue()); item != nil { + if item := sharedutil.FindMediaItemByID(id, p.getActivePlayQueue()); item != nil { if tr, ok := item.(*mediaprovider.Track); ok { tr.Rating = rating } @@ -616,7 +636,7 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { // remove tracks by ID from playQueue ids := p.GetTrackIdsFromIdx(idxs) newPlayQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) - for _, tr := range p.getPlayQueue() { + for _, tr := range p.getActivePlayQueue() { if slices.Contains(ids, tr.Metadata().ID) { //remove id from id list, handles having the same track present multiple times in playQueue idx := slices.Index(ids, tr.Metadata().ID) @@ -664,7 +684,7 @@ func (p *playbackEngine) RemoveTracksFromQueueByIdx(idxs []int, newQueue *[]medi idxSet := sharedutil.ToSet(idxs) nowPlaying := p.NowPlayingIndex() - for i, tr := range p.getPlayQueue() { + for i, tr := range p.getActivePlayQueue() { if _, ok := idxSet[i]; ok { if i < nowPlaying { // if removing a track earlier than the currently playing one (if any), diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index d594cf5..504b55c 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -333,6 +333,11 @@ func (p *PlaybackManager) OnLoopModeChange(cb func(LoopMode)) { p.engine.onLoopModeChange = append(p.engine.onLoopModeChange, cb) } +// Registers a callback that is notified whenever the shuffle state changes. +func (p *PlaybackManager) OnShuffleChange(cb func(bool)) { + p.engine.onShuffleChange = append(p.engine.onShuffleChange, cb) +} + // Registers a callback that is notified whenever the volume changes. func (p *PlaybackManager) OnVolumeChange(cb func(int)) { p.engine.onVolumeChange = append(p.engine.onVolumeChange, cb) @@ -400,6 +405,12 @@ func (p *PlaybackManager) LoadItems(items []mediaprovider.MediaItem, insertQueue p.cmdQueue.LoadItems(items, insertQueueMode, shuffle) } +// Load items into the currently active queue. (shuffledPlayQueue/playQueue) +// If replacing the current queue (!appendToQueue), playback will be stopped. +func (p *PlaybackManager) SetQueueState(tracks []*mediaprovider.Track, queueType QueueType) { + p.cmdQueue.SetQueueState(tracks, queueType) +} + // Replaces the play queue with the given set of tracks. // 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. @@ -743,7 +754,7 @@ func (p *PlaybackManager) enqueueAutoplayTracks() { } // last 500 played items - queue := p.GetPlayQueue() + queue := p.GetActivePlayQueue() if l := len(queue); l > 500 { queue = queue[l-500:] } @@ -858,6 +869,12 @@ func (p *PlaybackManager) runCmdQueue(ctx context.Context) { c.Arg3.(bool), ) logIfErr("LoadItems", err) + case cmdSetQueueState: + err := p.engine.SetQueueState( + c.Arg.([]*mediaprovider.Track), + c.Arg2.(QueueType), + ) + logIfErr("SetQueueState", err) case cmdLoadRadioStation: p.engine.LoadRadioStation( c.Arg.(*mediaprovider.RadioStation), diff --git a/ui/bottompanel.go b/ui/bottompanel.go index 779424e..c54d668 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -106,6 +106,9 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr pm.OnLoopModeChange(func(lm backend.LoopMode) { fyne.Do(func() { bp.AuxControls.SetLoopMode(lm) }) }) + pm.OnShuffleChange(func(lm bool) { + fyne.Do(func() { bp.AuxControls.SetShuffle(lm) }) + }) pm.OnVolumeChange(func(vol int) { fyne.Do(func() { bp.AuxControls.VolumeControl.SetVolume(vol) }) }) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 97716b9..cebe339 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -461,7 +461,7 @@ func (a *NowPlayingPage) Reload() { a.relatedList.DisableRating = !a.canRate a.relatedList.DisableSharing = !a.canShare - a.queue = a.pm.GetPlayQueue() + a.queue = a.pm.GetActivePlayQueue() a.queueList.SetItems(a.queue) a.totalTime = 0.0 for _, tr := range a.queue { diff --git a/ui/controller/controller.go b/ui/controller/controller.go index ed80bb3..a9d4bd5 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -75,7 +75,7 @@ func New(app *backend.App, appVersion string, mainWindow fyne.Window) *Controlle c.initVisualizations() c.App.PlaybackManager.OnQueueChange(util.FyneDoFunc(func() { if c.popUpQueue != nil { - c.popUpQueueList.SetItems(c.App.PlaybackManager.GetPlayQueue()) + c.popUpQueueList.SetItems(c.App.PlaybackManager.GetActivePlayQueue()) } })) c.App.PlaybackManager.OnSongChange(func(track mediaprovider.MediaItem, _ *mediaprovider.Track) { @@ -192,7 +192,7 @@ func (m *Controller) ShowPopUpPlayQueue() { if m.popUpQueue == nil { m.popUpQueueList = widgets.NewPlayQueueList(m.App.ImageManager, false) m.popUpQueueList.Reorderable = true - m.popUpQueueList.SetItems(m.App.PlaybackManager.GetPlayQueue()) + m.popUpQueueList.SetItems(m.App.PlaybackManager.GetActivePlayQueue()) m.ConnectPlayQueuelistActions(m.popUpQueueList) title := widget.NewRichTextWithText(lang.L("Play Queue")) diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 7e11c4e..5df85dd 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -119,7 +119,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, }) }) app.PlaybackManager.OnQueueChange(func() { - fyne.Do(func() { m.Sidebar.SetQueueTracks(app.PlaybackManager.GetPlayQueue()) }) + fyne.Do(func() { m.Sidebar.SetQueueTracks(app.PlaybackManager.GetActivePlayQueue()) }) }) app.ServerManager.OnServerConnected(func(conf *backend.ServerConfig) { go m.RunOnServerConnectedTasks(conf, app, displayAppName)